目录
什么是作用域?
作用域是一套规则,用于确定在何处以及如何查找变量。——《你不知道的JavaScript上卷》
当一个块或者函数嵌套在另一个块或者函数中时,就发生了作用域嵌套
查找变量的规则是:从当前作用域开始查找变量,如果找不到,继续向上级查找,直到全局作用域停止。由当前作用域到全局作用域中发生的嵌套就形成了作用域链。
JS的词法作用域
作用域分词法作用域和动态作用域
JS采用的就是词法作用域!
词法作用域意味着作用域由写代码时函数所声明的位置来决定的,也就说,我们不用管函数在哪里调用或者怎样调用,它的作用域在声明的时候就决定了。
函数作用域和块作用域
JS中的作用域单元包含函数作用域和块级作用域。
函数作用域
将一段代码用一个函数包裹起来,就形成了一个新的作用域———函数作用域
函数作用域隐藏内部实现,规避了命名冲突,也是ES6之前模块管理的一种解决方案。
理想情况下,我们希望的是,函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行。
答案是:立即执行函数表达式(IIFE)
(function foo(){
})()
// 或者
(function foo(){
}())
函数表达式中,foo被绑定在自身函数中,而不是所在作用域中,foo变量名不会污染外部作用域。
块级作用域
ES6之前js不支持块级作用域,但是,ES3规范中规定try/catch的catch分句会创建一个块级作用域,声明的变量仅在catch内部有效
ES6中的关键字let和const,使用这两个关键字声明的变量,会被绑定在当前块级({…})作用域中
变量提升
提升:所有声明(变量和函数)都会被移动到各自作用域的最顶端
如:
bar() // 1
console.log(a) // undefined 而不是报错
var a = 2
var bar;
function bar(){
console.log(1)
}
bar = function(){
console.log(2)
}
var a=2
可以分为两步var a
和a=2
当重复声明的代码,函数会首先被提升,这里的var bar
会被忽略掉,被function bar
覆盖。
闭包
当函数可以记住并访问所在词法作用域,即使函数是在当前作用域之外执行,这时就产生了闭包。 ———《你不知道的JavaScript上卷》
funciton foo(){
var a = 2
function bar(){
console.log(a)
}
return bar;
}
var baz = foo()
bar(); // 2 这里就是闭包的效果!
前面我们了解了作用域,知道函数bar
只能在其所在的词法作用域(foo)中被调用,在foo函数之外调用会报错
在执行var baz = foo()
后,原本根据垃圾回收机制会释放不再使用的内存空间,拜bar()
所声明的位置所赐,无论通过何种方式将内部函数传递到所在词法作用域以外(这里我们通过return
将函数bar
传递出去了), 它依然持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。
由此,闭包需要必备的两个条件是:
- 函数能够记住并访问所在词法作用域,将函数当做值进行传递(比如return、赋值到外部作用域等)
- 函数必须在它本身的词法作用域之外执行
闭包无处不在
看下面一个例子:
for(var i=0; i<5; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
// 想要的结果为:0 1 2 3 4
// 控制台输出 5个5
这是由于每次循环延时函数都被封闭在共享的全局作用域中,实际上共享一个i值,setTimeout会在循环结束后才执行,而此时i的值为5。
解决方案:
- 通过一个立即执行函数形成的闭包
for (var i = 0; i < 5; i++) {
(function (i) {
var j = i
setTimeout(function () {
console.log(j)
}, 1000)
})(i)
}
// 0 1 2 3 4
通过IIFE,每次循环都会创建各自的词法作用域并捕获一个i的副本,setTimeout通过引用了所在词法作用域中的变量j形成了一个闭包。循环结束后,执行setTimeout都会根据自己的作用域找到i的值。
- 使用块作用域
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
// 0 1 2 3 4
let 声明有个特殊的行为:每次迭代都会重新声明,将上一次迭代结束的值用来初始化变量。
以上两种解决方案,本质上就是将一个块转换成一个可以被关闭的作用域。
用闭包来实现模块
模块模式需要具备的两个必要条件:
- 必须要有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
一个简单的模块例子:
var foo = (function moudle() {
var a = 1
var b = [1, 2, 3];
function foo() {
console.log("foo");
}
function bar() {
console.log(b.join(" ! "));
}
return {
foo,
bar
};
})();
foo.foo(); // foo
foo.bar(); // 1 ! 2 ! 3
模块有两个主要特征:
- 为创建内部作用域调用了一个包装函数
- 包装函数的返回值必须至少包括一个对内部函数的引用,这样就创建涵盖整个包装函数内部作用域的闭包
现有的模块工具库,如define、require等都是将这种模块定义封装。
ES6将文件当作独立的模块来处理。