本帖最后由 webman 于 2017-12-25 11:45 编辑
JavaScript闭包就如同汽车的功能——不同的位置都有对应那辆车的不同组件。
JavaScript中的每一个函数都构成一个闭包,这也是JavaScript最酷的特点之一。因为没有闭包的话,实现像回调函数或者事件句柄这样的公共结构就会很困难。
不管你什么时候定义了一个函数,你都创建了一个闭包。然后当你执行这些函数时,他们的闭包能够让他们访问他们作用域内的数据。
这有点像生产一辆带有一些像start, accelerate, decelerate之类功能的汽车。司机每次操纵他们的车时执行这些功能。定义这些函数的闭包就像汽车一样,并且‘闭合’了需要操作的变量。
让我们拿accelerate函数做一个简单的类比,当汽车被制造的时候,函数也就被定义了:
[JavaScript] 纯文本查看 复制代码 function accelerate(force) {
// Is the car started?
// Do we have fuel?
// Are we in traction control mode?
// Many other checks...
// If all good, burn more fuel depending on
// the force variable (how hard we’re pressing the gas pedal)
}
每次司机踩下油门,这个方法就被执行。注意这个函数需要访问很多变量才能执行,包括它自己的force变量。但是更重要的是,它需要自己作用域外被其它汽车功能控制的变量。这就是accelerate函数的闭包(我们从汽车本身获得到的)的用处。
以下是accelerate函数的闭包对加速函数所作出的承诺:
当你执行时accelerate函数的时候,你可以访问你的_force_变量,你可以访问_isCarStarted_变量,也可以访问_fuelLevel_变量和_isTractionControlOn_变量。 你也可以控制我们发送给引擎的_currentFuelSupply_变量。
请注意,闭包不会为这些变量赋予acceleration函数确切的值,而是允许在accelerate函数执行时访问这些值。
闭包与函数作用域密切相关,因此理解这些作用域如何工作将有助于理解闭包。 简而言之,了解作用域最重要的就是了解当你执行一个函数时,一个私有函数作用域被创建并用于执行该函数的过程。
然后当你内部函数开始执行函数时,这些函数作用域就会形成嵌套。
当你定义一个函数时就创建了一个闭包,而不是当你执行它的时候。然后,每当你执行这个函数,其已经定义的闭包使它可以访问所有对它可用的函数作用域。
在某种程度上,你可以认为作用域是临时的(全局作用域除外),而把闭包是永久的。
想要真正了解闭包在JavaScript里扮演的角色,你首先需要明白几个简单的JavaScript函数和作用域的概念。
1 -- 按引用分配函数当你把一个函数赋值给一个变量,就像这样:
[JavaScript] 纯文本查看 复制代码 function sayHello() {
console.log("hello");
};
var func = sayHello;
你正在给变量func赋予一个sayHello的引用,而不是复制。这使得func仅仅是sayHello的一个别名,你在这个别名上做的任何事,其实都是在原来的函数上操作的。比如:
[JavaScript] 纯文本查看 复制代码 func.answer = 42;
console.log(sayHello.answer); // prints 42
属性的answer是直接在func上设置的,然后使用sayHello进行读取,这依然是有效的。你还可以通过执行func别名来执行sayHello:
[JavaScript] 纯文本查看 复制代码 func() // prints "hello"
2 -- 作用域有生命周期
当你调用一个函数时,在执行该函数期间创建一个作用域,函数执行完毕,作用域消失。
当你第二次调用该函数时,在第二个执行期间创建一个新的不同的作用域,当函数执行完毕,第二个作用域也随之消失。
[JavaScript] 纯文本查看 复制代码 function printA() {
console.log(answer);
var answer = 1;
};
[JavaScript] 纯文本查看 复制代码 printA(); // 创建一个作用域,当函数执行完毕,作用域被销毁
[JavaScript] 纯文本查看 复制代码 printA(); // 创建另外一个不同的作用域,当函数执行完毕,作用域也会被销毁
在上面的示例中创建的这两个作用域是不同的。这里的变量answer在它们两个之间完全是不共享的。
每个函数作用域都有一个生命周期。它们会被创建出来,然后又立刻被丢弃。惟一的例外是全局作用域,只要应用程序在运行,它就不会消失。
3 -- 闭包跨越多个作用域
当你定义一个函数,也就创建了一个闭包。和作用域不同,闭包是当你定义一个函数时创建的,而不是你执行函数的时候。闭包在你执行完函数后也不会消失。
在定义了一个函数很久以后,你依然可以访问闭包里的数据,即使它执行了也是一样。
一个闭包包含所有定义好的函数可以访问的数据。这意味着定义函数的作用域,全局作用域和定义函数作用域之间嵌套的作用域,以及全局作用域本身。
[JavaScript] 纯文本查看 复制代码 var G = 'G';
// Define a function and create a closure
function functionA() {
var A = 'A'
// Define a function and create a closure
function functionB() {
var B = 'B'
console.log(A, B, G);
}
functionB(); // prints A, B, G
// functionB closure does not get discarded
A = 42;
functionB(); // prints 42, B, G
}
functionA();
当我们定义一个functionB所创建的闭包,允许我们访问functionB的作用域,functionA的作用域以及全局作用域。
每次我们执行functionB,我们都可以通过先前创建好的闭包访问变量B, A, 和 G。然而,闭包并不是复制了这些变量,而是引用它们。
例如,functionB的闭包被创建之后,变量A的值会在某些时候发生变化,当我们执行functionB之后,我们会看到新的值,而不是旧的值。functionB的第二个调用打印42、B、G,因为变量A的值被更改为42,闭包给我们提供了一个引用,而不是一个副本。
不要将闭包和作用域混淆把闭包与作用域混淆是很常见的,所以让我们确保不要这样做。
[JavaScript] 纯文本查看 复制代码 // scope: global
var a = 1;
function one() {
// scope: one
// closure: [one, global]
var b = 2;
function two() {
// scope: two
// closure: [two, one, global]
var c = 3;
function three() {
// scope: three
// closure: [three, two, one, global]
var d = 4;
console.log(a + b + c + d); // prints 10
}();
}();
}();
在上面的简单例子中,我们定义并立即调用了三个函数,所以他们都创建了作用域和闭包。
函数one()的作用域就是它自己,它的闭包让我们有访问它和全局作用域的权利。
函数two()的作用域就是它自己,它的闭包让我们有访问它和函数one(),还有全局作用域的权利。
同样,函数three()的闭包给我们访问所有作用域的权力。这就是为什么我们可以在函数three()中访问所有变量的原因。
但是作用域和闭包的关系不总是如此。在不同作用域里定义和调用函数时,情况又会变得不一样。让我通过一个例子来解释:
[JavaScript] 纯文本查看 复制代码 var v = 1;
var f1 = function () {
console.log(v);
}
var f2 = function() {
var v = 2;
f1(); // Will this print 1 or 2?
};
f2();
你认为上面的例子中会打印1还是2?代码很简单,函数f1()打印v的值,是全局作用域的1。但是我们在有不同的值等于2的v的函数f2()里执行f1(),然后再执行f2()。
这段代码将会打印1还是2?
如果你想说2,那么你将会感到惊讶,这段代码实际上会打印1。原因是作用域和闭包并不相同。console.log方法会使用当我们定义f1()时所创建的f1()闭包,这意味着f1()的闭包值允许我们访问f1()和全局的作用域。
我们执行f1()的地方的作用域并不会影响闭包。实际上,f1()的闭包并不会给我们访问函数f2()作用域的权力。如果你删除全局变量v,然后执行这段代码,你将会得到错误消息:
[JavaScript] 纯文本查看 复制代码 var f1 = function () {
console.log(v);
}
var f2 = function() {
var v = 2;
f1(); // ReferenceError: v is not defined
};
f2();
4 -- 闭包有读和写的权限
由于闭包给我们提供了在作用域中的变量的引用,所以意味着它们给我们的权限包括读和写,而且不仅仅是读。
[JavaScript] 纯文本查看 复制代码 function outer() {
let a = 42;
function inner() {
a = 43;
}
inner();
console.log(a);
}
outer();
我们定义了一个inner()函数,创建了一个可以让我们访问变量a的闭包。我们可以读写这个变量,并且如我们我们真的改变了它的值,我们会改变outer()作用域里变量a的值。
这段代码会打印43,因为我们用inner()函数的闭包改变了outer()函数的变量
这就是为什么我们可以在任何地方改变全局变量。所有闭包都给我们提供了对所有全局变量的读写权限。
5 -- 闭包可以分享作用域
因为在定义函数时,闭包就给我们访问嵌套作用域的权力,所以当我们在同一个作用域中定义多个函数时,这个作用域就被其中的闭包共享。由于这个原因,全局作用域总是被所有闭包共享。
[JavaScript] 纯文本查看 复制代码 function parent() {
let a = 10;
function double() {
a = a+a;
console.log(a);
};
function square() {
a = a*a;
console.log(a);
}
return { double, square }
}
let { double, square } = parent();
double(); // prints 20
square(); // prints 400
double(); // prints 800
在上面的例子中,我们有一个设置变量a的值为10的函数parent(),我们在函数parent()的作用域里定义了两个函数,double() 和 square()。定义函数double() 和 square()时所创建的闭包共享函数double() 的作用域。
因为double() 和 square()都会改变变量a,当我们执行最后3行代码时,我们先把a相加(让a = 20),然后把相加后的值相乘(让a = 400),然后把相乘后的值相加(让a = 800)。
最后一个测试
让我们来测试到目前为止你对闭包的理解。在你执行下面的代码之前,先猜猜它会打印什么:
[JavaScript] 纯文本查看 复制代码 let a = 1;
const function1 = function() {
console.log(a);
a = 2
}
a = 3;
const function2 = function() {
console.log(a);
}
function1();
function2();
希望这个简单的概念能帮你真正理解函数闭包在JavaScript里扮演的重要角色。
|