作用域相关的知识点:闭包、执行上下文、LHS/RHS、词法作用域

简介: 作用域相关的知识点:闭包、执行上下文、LHS/RHS、词法作用域

作用域相关的知识点:闭包、执行上下文、LHS/RHS、词法作用域


最近在看大神写的专栏,很精辟,笔者想通过总结的方式加深理解,不一定准确,只是笔者自己的想法,欢迎指正。

TL;DR

  • 作用域:存储和访问变量的规则
  • 作用域链:寻找变量形成的路
  • 变量提升:console.log(a);var a,编译器先var a,之后才是 JS 引擎执行console.log(a)
  • 暂时性死区:let 命令声明变量之前,该变量都是不可用的。上面的换成let就会报错。
  • 执行上下文:常常是函数调用的时候,JS 引擎先做一些执行前的准备工作。
  • 闭包:一个函数,使用了非自己作用域的变量。常用来私有化变量、柯里化
  • LHS/RHS:变量出现在赋值的左边,就是变量就进行了LHS,否则就是RHS(就是读取啦)
  • 词法作用域:作用域链沿着它定义的位置往外延伸
  • 欺骗词法作用域:eval和with

作用域

每一种编程语言,它最基本的能力都是能够存储变量当中的值、并且允许我们对这个变量的值进行访问修改

那么有了变量之后,应该把它放在哪里、程序如何找到它们?

这就需要我们提前约定好一套存储变量、访问变量的规则?这套规则,就是我们常说的作用域。

作用域的本质:是程序存储和访问变量的规则。

举个例子说明下规则

var a = 1;
// 相当于:var a;a=1
  • var a,让编译器先在当前作用域寻找,有没有 a 的变量,有则忽略,没有则增加 a 变量
  • a=1,让JS 执行引擎先在当前作用域寻找,有没有 a 的变量,有则赋值,没有则继续向上找,直到顶层的全局作用域,找不到就报错。

以上就是规则,也就是作用域限制了存储和赋值。

作用域链

没有则继续向上找。

这句话,一层层向上,像链条一样的,就是作用域链。

顶级作用域:是全局作用域。 局部作用域:是函数作用域和块作用域。

顶级作用域就是顶层,在局部作用域里肯定可以访问顶层的作用域的变量。局部作用域则看其嵌套关系。

举个例子说下作用域链:

function addABC() {
  var a = 1;
  var b = 2;
  function add() {
    return a + b + c;
  }
  return add;
}
var c = 3;
var globalAdd = addABC();
console.log(globalAdd()); // 6

全局作用域:c、globalAdd => addABC 函数作用域:a、b => add 函数作用域:没有变量。

像不像链子?链子的尽头是全局作用域。

add 函数作用域的直接上层是 addABC 函数作用域,再往上是全局作用域。

所以 add 函数作用域,可以读取到a/b/c变量。

闭包

add 函数作用域,直接访问自己作用域的a/b/c变量,这就是闭包。

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

写高阶函数常常用到。

执行上下文

引用不写 bug 的米公子的话:

es3 中,函数被调用,在执行具体的函数代码之前,创建了执行上下文,从而进入执行上下文的创建阶段:

  • 初始化作用域链
  • 创建 arguments object 检查上下文中的参数,初始化名称和值并创建引用副本
  • 扫描上下文找到所有函数声明:对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针。如果函数名称已经存在了,属性的引用指针将会被覆盖
  • 扫描上下文找到所有 var 的变量声明:对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined 来初始化。如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
  • 确定 this 值

做完这些准备工作之后,才开始真正执行函数中的代码。

来个稍微复杂的例子

foo();
var foo = function foo() {
  console.log("foo1");
};
var a = 1;
function foo() {
  console.log(a);
  var a = 2;
  console.log(a);
}
foo();
console.log(a);

上面的代码,可以理解为

function foo() {
  var a;
  console.log(a);
  a = 2;
  console.log(a);
}
var a;
// 此时foo执行的时候,第一个是undefined,第二个才是2。注意这只是局部变量。
foo();
// foo被重新赋值了
foo = function foo() {
  console.log("foo1");
};
// a赋值了
a = 1;
// 没有悬念的foo1
foo();
// 没有悬念的1
console.log(a);

LHS/RHS

听起来就感觉好高深。

其实没啥,最开始说的编辑器查找变量,如果查找的目的是对变量进行赋值, 那么就会使用 LHS 查询; 如果目的是获取变量的值, 就会使用 RHS 查询。赋值操作会导致 LHS 查询。

快速速记法:L 就是 Left,R 就是 Read。(不过这两本身是 left hand side 和 right hand side。)

// name出现在左边,进行赋值操作,就是对name进行了LHS
var name = 2;
// name被读取,就是对name进行了RHS
console.log(name);
// name仍然是被读取,就是对name进行了RHS
// newName出现在左边,进行赋值操作,就是对newName进行了LHS
var newName = name;

此文写的很细致了:LHS 和 RHS----你所不知道的JavaScript系列(1)

词法作用域和动态作用域

词法作用域和动态作用域的区别其实在于划分作用域的时机:

  • 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
  • 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸
var name = "yan";
function showName() {
  console.log(name);
}
function changeName() {
  var name = "BigBear";
  showName();
}
changeName();

js 遵循的是词法作用域,所以 showName 的外层作用域是全局作用域,会打印yan如果是动态作用域,那么 showName 的外层作用域就是 changeName 函数作用域了,会打印BigBear

如何欺骗词法作用域

其实欺骗就是改变的意思,听起来又很玄学,主要就是下面的一句话。

evalwith可以修改词法作用域,但因为这种特性,所以尽可能不要用这两个语句。

举个例子:

function showName(str) {
  eval(str);
  console.log(name);
}
var name = "xiuyan";
var str = 'var name = "BigBear"';
showName(str); // 输出 BigBear

eval 拿到一个字符串入参后,它会把这段字符串的内容当做一段 js 代码(不管它是不是一段 js 代码),插入自己被调用的那个位置。导致了 showName 作用域多了一个变量 name,这个是在运行时才创建的变量,所以欺骗了词法作用域。

with 用法有类似的功能,不再赘述(笔者偷懒不想研究了)~

作用域的经典题目

经典的一个题目,循环体和作用域的组合:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
console.log(i);

结合之前的学到的,上面的代码,虚拟的等价替换下:

var i;
for (i = 0; i < 5; i++) {}
console.log(i);
// 5个setTimeout在1s后依次执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);

所以很明显,打印了 6 个 5。

三种改造方式:让 i 从 0 到 4 依次被输出

本质:将 i 由全局变量变成局部变量,以此互不相关。

上面的知易行难啊。。。。。

1. setTimeout 的第三个参数

setTimeout 从第三个入参位置开始往后,是可以传 入无数个参数的。这些参数会作为回调函数的附加参数存在。

j 当前的作用域,是函数作用域。

for (var i = 0; i < 5; i++) {
  setTimeout(
    function(j) {
      console.log(j);
    },
    1000,
    j
  );
}

2. 包个函数

虽然 i 在当前的作用域没有找到变量,但是在其外层增加一个函数作用域,也是同样的道理。

for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, 1000);
  })(i);
}

3. let

局部作用域还有一个块级作用域,异曲同工之妙。

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

作用域特训题目

function test() {
  var num = [];
  var i;
  for (i = 0; i < 10; i++) {
    num[i] = function() {
      console.log(i);
    };
  }
  return num[9];
}
test()();
var test = (function() {
  var num = 0;
  return () => {
    return num++;
  };
})();
for (var i = 0; i < 10; i++) {
  test();
}
console.log(test());
var a = 1;
function test() {
  a = 2;
  return function() {
    console.log(a);
  };
  var a = 3;
}
test()();
function foo(a, b) {
  console.log(b);
  return {
    foo: function(c) {
      return foo(c, a);
    }
  };
}
var func1 = foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2 = foo(0)
  .foo(1)
  .foo(2)
  .foo(3);
var func3 = foo(0).foo(1);
func3.foo(2);
func3.foo(3);

闭包应用

变量只在特定的局部作用域,外部的作用域并不能访问到该变量。

私有化变量

密码不希望被人知道user.password

// 利用闭包生成IIFE,返回 User 类
const User = (function() {
  // 定义私有变量_password
  let _password;
  class User {
    constructor(username, password) {
      // 初始化私有变量_password
      _password = password;
      this.username = username;
    }
    login() {
      // 这里我们增加一行 console,为了验证 login 里仍可以顺利拿到密码
      console.log(this.username, _password);
      // 使用 fetch 进行登录请求,同上,此处省略
    }
  }
  return User;
})();
let user = new User("xiuyan", "xiuyan123");
console.log(user.username);
// undefined
console.log(user.password);
// undefined
console.log(user._password);
user.login();

偏函数和柯里化

柯里化:是把接受 n 个参数的 1 个函数改造为只接受 1 个参数的 n 个互相嵌套的函数的过程。也就是 fn(a,b,c)会变成 f(a)(b)(c) 。

function generateName(prefix) {
  return function(type) {
    return function(itemName) {
      return prefix + type + itemName;
    };
  };
}
// 啥也不记,直接生成一个商品名
var itemFullName = generateName("洗菜网")("生鲜")("菠菜");

偏函数:和柯里化相似,但仅仅是把函数的入参拆解为两部分。

function generateName(prefix) {
  return function(type, itemName) {
    return prefix + type + itemName;
  };
}
// 把3个参数分两部分传入
var itemFullName = generateName("大卖网")("母婴", "奶瓶");


目录
相关文章
|
1月前
|
JavaScript 前端开发 Python
函数与作用域
编程中的函数与作用域概念。函数是可重用的代码块,能提高代码的可读性、可维护性和复用性。基础用法包括定义、调用和返回值。高级用法涉及函数嵌套、匿名函数(lambda函数)和装饰器。装饰器能在不修改原函数代码的情况下添加功能。 作用域决定了变量的可见范围,从内到外是局部、嵌套、全局和内置作用域。闭包是能访问外部函数变量的内部函数,即使外部函数执行完毕,闭包仍能保留其状态。闭包常用于实现特殊功能,如记忆化和延迟执行。 立即执行函数表达式(IIFE)是JavaScript中的模式,用于创建私有作用域和防止变量污染全局。IIFE常用于封装变量、避免命名冲突以及实现模块化和函数作为参数传递。
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解作用域、作用域链和闭包
在 JavaScript 中,作用域是指变量在代码中可访问的范围。理解 JavaScript 的作用域和作用域链对于编写高质量的代码至关重要。本文将详细介绍 JavaScript 中的词法作用域、作用域链和闭包的概念,并探讨它们在实际开发中的应用场景。
|
12月前
|
Linux 网络架构
暂时性死区以及函数作用域
暂时性死区以及函数作用域
126 0
|
设计模式 自然语言处理 JavaScript
一篇文章帮你真正理解javascsript作用域闭包
一篇文章帮你真正理解javascsript作用域闭包
67 0
|
消息中间件 存储 自然语言处理
兄台: 作用域、执行上下文了解一下
• 作用域(Scopes) • 词法环境(Lexical environments) • 作用域链 • 执行上下文 • 调用栈
|
自然语言处理 JavaScript 前端开发
词法作用域
词法作用域
69 0
词法作用域
|
自然语言处理 前端开发 JavaScript
作用域闭包
作用域闭包
67 0
|
存储 JavaScript 前端开发
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)
|
存储 缓存 JavaScript
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
|
自然语言处理 JavaScript 前端开发
这次写的不只是函数作用域,而是。。。。
这次写的不只是函数作用域,而是。。。。
90 0
这次写的不只是函数作用域,而是。。。。