作用域
词法作用域就是定义在词法阶段的作用域, 换句话说词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的, 因此当词法分析器处理代码时会保持作用域不变
//1
function foo(a){
var b = a*2 //2
function bar(c){ //3
console.log(a,b,c);
}
bar(b*3)
}
foo(2)
- 包含这整个全局作用域, 其中只有一个标识符: foo,
- 包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b
- 包含着 bar 所创建的作用域,其中只有一个标识符:c
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。函数作用域是在函数定义的时候产生的。
区分函数声明与函数表达式
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位 置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
(function(){})——函数表达式
function——函数声明
提升——函数优先
函数声明和变量声明都会被提升。但是,函数会首先被提升,然后才是变量。
闭包
基本概念
当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行(你不知道的 JavaScript 上)
闭包是指有权访问另一个函数作用域中的变量的函数(红宝书)——缩句,闭包是一个函数,是一个什么样的函数?是一个能够访问另一个函数作用域里边的变量的函数
闭包的形式
- 分配给全局变量
var fn
function foo() {
var a = 1
function bar() {
console.log(a)
}
fn = bar
}
foo()
fn() // 1
- 通过内部返回函数方法——常用
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2
- 回调函数
function foo(callback) {
var a = 3
callback(a)
}
foo(function (p) {
console.log(p)
}) // 3
闭包举例1——return 一个函数
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果
- 闭包是在函数定义时产生的,这里function bar()是闭包,符合上面两个定义
- return bar的引用,使得变量baz能够调用bar函数,从而访问到foo函数作用域中的变量a;这是闭包产生的效果
- 在外部函数中return一个内部函数的引用,使得内部函数能够在其它作用域中执行,这是闭包的常用形式
分析
-
在foo()执行后,其内部作用域依然存在,没有被垃圾回收器回收,原因就是闭包可以阻止这种行为。谁在使用这个内部作用域?bar() 本身在使用。 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。 bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
-
bar持有foo的活动对象。在一个函数内部定义的函数,会将包含函数的活动对象添加到它的作用域链中。当foo()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象依然会留在内存中,直到解除对bar的引用后,活动对象才会被销毁。——具体看红宝书7.2节
闭包举例2——异步回调
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
//函数作用域是在定义时产生的,而非调用时。因此定时器中的回调函数有一个作用域,wait函数有一个作用域,setTimeOut()没有作用域
在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
闭包举例3——循环和闭包
for (var i = 0; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
正常情况下, 我们对这段代码行为的预期是分别输出 1~5, 每秒一次, 每次一个;
但实际上这段代码在运行时会以每秒一次的频率输出五次6;
是什么导致了如此它的行为同语义暗示的不一致呢?
- 首先, 我们知道,
var
的声明下,for
循环没有自己的块作用域, 也就是说,i
位于全局作用域中, 整段程序只有唯一一个i
; setTimeout
作为异步函数, 在程序执行过程中, 会被推到任务队列中, 等待所有同步函数执行完毕后再执行, 所以, 当for
完成循环后, 全局变量i
已经变成了 6, 自然在执行异步函数时输出的都是6了.- 尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。 这样说的话,当然所有函数共享一个 i 的引用。
如何使用闭包解决这个问题?
IIFE
函数会通过声明立即执行一个函数来创建函数作用域, 通过这个特性, 我们可以在每次循环时, 将当前状态的 i
传递到 IIFE
函数中, 在内部为 setTimeout
创建一个新的作用域;
错误示例:
for (var i = 0; i <= 5; i++) {
(function() {
setTimeout(function() {
console.log(j)
}, j * 1000)
})()
}
这样不行,创建的IIFE虽然产生了闭包作用域,却是一个空作用域,没有传入实质内容。
正确示例:
// 第一种
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j)
}, j * 1000)
})(i)
}
// 第二种
for (var i = 0; i <= 5; i++) {
// i 被劫持了, 现在每次循环都形成一个封闭的块作用域
let j = i
setTimeout(function() {
console.log(j)
}, j * 1000)
}
// 第三种
for (let i = 0; i <= 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
以上三种方法的实质都是 为每个迭代生成一个新的作用域,并在新的作用域中保存此次迭代的值,传给内部作用域。
另:在第三种方法中,for循环有一个特殊之处,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。这点与函数作用域不同
闭包应用——模块
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother };
}
var foo = CoolModule();
foo.doSomething(); //
cool foo.doAnother(); // 1 ! 2 ! 3
doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包(通过调用 CoolModule() 实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。
模块模式需要具备两个必要条件:
-
必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
-
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
参考文献
1:你不知道JS上——作用域和闭包
2: JavaScript高级程序设计——闭包小节
以上两本书的闭包小节从不同的方面阐述JS闭包,殊途同归,结合起来理解,能对闭包有一个更清晰的认识