什么叫闭包(closure)?
那要这么说:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
怎么每个都认得的字组合在一起就看不懂了呢 🤣 ?
先别被它所惑,思考下下列代码的输出:
function sayHello() {
var sentence = "Hello,world!";
function say() {
console.log(sentence);
}
say();
}
sayHello();
显然,代码的输出是:Hello,world!。可以梳理下过程:
- 执行
sayHello
方法 ( sayHello(); ) - 定义变量
var sentence = "Hello,world!"
,并且,sentence
变量的作用域局限于sayHello
( sayHello 方法外部无法访问)。( var sentence = "Hello,world!"; ) - 调用
sayHello
内部的方法say
方法。( say(); ) - 因为
say
方法定义于sayHello
内部,所以它无疑可以访问到同样定义在sayHello
方法中的变量sentence
。这样看来,代码似乎没有任何问题。
可这样呢?
function sayHello() {
var sentence = "Hello,world!";
function say() {
console.log(sentence);
}
return say;
}
var fun = sayHello();
fun();
我们可能会认为过程是:
- 调用
sayHello
方法,并用fun
变量保存其返回值。( var fun = sayHello(); ) sayHello
的返回值是个函数对象:say
( return say; )。所以我们可以在外部调用say
方法了。( fun(); )- 进入
say
方法,原来要向控制台输出 sentence 啊 ( console.log(sentence);,那我们找找 sentence 在哪 —— 哦,原来在var sentence = "Hello,world!";
这。但是 sayHello 已经执行完了啊,sentence
变量怎么还会存在?
如此看来,sentence
是未定义的?可结果不然,输出依旧是:Hello,world!。
怎么会这样!既然如此,我们只能推测,代码中 sayHello
返回的方法实例 say
并不简单的是个函数,它肯定用某种方式保存了 sentence
变量,得以执行它时能正常访问到 sentence
变量。
原来,JavaScript 中的函数会形成 闭包 。而闭包是由 函数 以及声明该函数的 词法环境 组合而成的。该环境包含了这个闭包创建时作用域内的 任何局部变量 。
果然如我们所推测!:
say
是 sayHello
的内部方法,当我们取得 say
方法的实例(var fun = sayHello()
) 时,say
的实例确保其内部所有使用过的变量能被正常访问。所以,当我们调用 say
方法的实例 fun
时,能正常访问到被 say
方法使用了的变量 sentence
也就不奇怪了。
坏和好……
闭包看起来很奇怪,我们可以预料到,如果每次创建一个函数实例,那么其内部方法岂不都要被重新定义一次?
以构造函数为例:
function Person(name,age) {
this.name = name;
this.sayHello = function(){console.log("My name is ",name)};
}
根据闭包的原理,每次实例化 Person
,sayHello
都将重复定义。如果实例对象很多,重复的方法将占用大量空间,幸好在此可以运用原型解决:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function(){console.log("My name is ",this.name)};
那闭包有什么好处?那是当然,闭包自然有其好处。
简要分析,闭包将外部变量和方法实例绑定在了一起,算不算是一种 封装 ?
我们可以:
var names = ["chao","zhao","tao","yao"];
var say = function(str){
return function(){
console.log("My name is ",str);
};
};
var funs =[];
for(var i in names)
funs[i] = say(names[i]);
for(var f of funs)
f();
say
的内部方法维持了自身的词法环境,保存了变量 str
。所以,尽管 str
不断变化(每个 str
都是新的 str
),但方法实例都一一维持了,故最终调用的数组 funs
里每个方法实例输出都不同:
My name is chao
My name is zhao
My name is tao
My name is yao
👌,大概就这样了,各位大佬赏个赞 👍 bie!
————————————
最后一个例子如果不用内部函数:
var names = ["chao","zhao","tao","yao"];
var say = function(str){
console.log("My name is ",str);
};
var funs =[];
for(var i in names)
funs[i] = function(){console.log("My name is "+names[i])};
for(var f of funs)
f();
输出:
My name is yao
My name is yao
My name is yao
My name is yao
这是因为函数虽然还是维持了每个闭包的词法环境,但是 i
是不变的。维持来维持去,总是那个最后等于 3 的 i
(如果用 for(var i = 0;i < names.Length;i++)
最后 i
为 4 )。用 let i 代替 var i 又不同了,原因可以自己推推看😆。