【JavaScript由浅入深】深入理解作用域、作用域链与闭包
一、作用域
- 作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。
- 如果一个变量或表达式不在当前的作用域中,那么它是不可用的;
- 作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行;
作用域就是一个独立的地盘,让变量不会外泄、暴露出去。作用域最大的用处就是隔离变量,不同作用域下的同名变量不会有冲突。
JavaScript 的作用域分以下三种:
-
全局作用域:
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
-
函数作用域:
- 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
-
模块作用域:模块模式中运行代码的作用域
二、作用域链
- 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。
- 如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
- 当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)
- 作用域链是一个对象列表,用于变量标识符的求值;
- 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
三、闭包
3.1 对闭包的理解
- 这里先来看一下闭包的定义,分成两个:在计算机科学中和在JavaScript中。
- 在计算机科学中对闭包的定义(维基百科):
- 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);
- 是在支持头等函数的编程语言中,实现词法绑定的一种技术;
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
- 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
- MDN对JavaScript闭包的解释:
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
3.2 闭包经典面试题
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 7; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}
for循环属于同步,setTimeout
属于异步函数,同步代码执行完才会去执行异步,这时候 i
就是 8了,所以会输出一堆 8
实际循行的顺序是:
for(var i = 1; i <=7; i++) {
}
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
setTimeout(function () {
console.log(i);
});
解决办法有三种:
1)使用闭包
for (var i = 1; i <= 7; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
},j * 1000)
})(i)
}
在上述代码中,首先使用了立即执行函数将 i
传入函数内部,这个时候值就被固定在了参数 j
上面不会改变,当下次执行 timer
这个闭包的时候,就可以使用外部函数的变量 j
,从而达到目的。
2)使用 setTimeout
的参数
第三个及之后的参数是
setTimeout()
函数的可选参数,是作为参数传给setTimeout()
方法里面的匿名函数或者调用的函数,IE9 及其更早版本不支持第三个及之后的参数。
for (var i = 1; i <= 7; i++) {
setTimeout(function timer(j) {
console.log(j);
}, i * 1000, i)
}
3)使用 let
定义 i
,最为推荐的方式
for (let i = 1; i <= 7; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}