JavaScript中的闭包

闭包的含义

  • 闭包是定义在函数内部的函数。可以定义成全局变量让外界可访问该子函数,也可以通过return返回给外界。(这是个不完全正确的定义,详见最后闭包运行机制)
  • 父函数内部的子函数才能读取父函数的局部变量 => 在父函数外部,通过该子函数读取父函数的局部变量。

  • 由于子函数依赖于父函数而存在,所以当在父函数外面将return回来的子函数赋值给某个全局变量时,这个子函数就会始终存在与内存中,其父函数也会始终存在与内存中,达到保存变量始终在内存中的效果。

看几段代码

1 闭包只能取得包含函数中任何变量的最后一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
function closure(){
var result = [];
for(var i = 0; i < 10; i++)
result[i] = function(){ //直接把闭包赋值给数组元素
console.log(i);
}
return result;
}
var resultarr = closure();
resultarr[0](); //10
resultarr[1](); //10

函数数组每个都返回10,原因是,数组里函数保存着closure的活动对象,他们都引用保存变量i的同一个活动对象,当closure执行完毕后,i变为10,因此,每个函数数组都返回10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过另一个匿名立即执行函数强制让闭包的行为符合预期
function closure(){
var result = [];
for(var i = 0; i < 10; i++){
result[i] = function(num){ //匿名立即执行函数
return function(){
console.log(num);
};
}(i)
}
return result;
}
var resultarr = closure();
resultarr[1](); //1

2 闭包中的this关键字

  • 代码一
1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //在浏览器环境下 alert弹出'The Window'
console.log(object.getNameFunc()()); //在node环境下,console.log()打印undefined。(因为this在不同环境下有差异,但这个差异不是我们关注的重点)

只有在函数执行时,this才会确定到底指向谁。这里函数object.getNameFunc()()执行时指向全局的this.

  • 代码二
1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(object.getNameFunc()()); //'My Object'

这里函数object.getNameFunc()()执行时,其中的that变量存储了对象objectthis,所以可以访问到object对象中的name属性。

3 下面代码的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
function Foo() {
var i = 0;
return function() {
console.log(i++);
}
}
var f1 = Foo(),
f2 = Foo();
f1(); //0
f1(); //1
f2(); //0

(1) function是引用类型,保存在堆中;f1, f2是变量,保存在栈中。首先返回的function函数赋值给全局变量f1,因此闭包函数就被储存在了内存中,因为Foo函数是闭包函数的父函数,于是Foo函数和局部变量i也被存在了内存。

(2)f1 指向子函数f1()=function(){.....}, 因为子函数没有定义i,所以向上找到父函数定义的 i 并执行子函数 输出 i = 0, 再自加 i = 1 覆盖了父函数Fooi 值。

闭包的作用

JavaScript没有块级作用域,用匿名函数可以用来模仿块级作用域并避免出现命名参数冲突的问题:

1
2
3
4
(function(){ //第一对括号,将函数声明变成函数表达式。
//块级作用域
})();
//这种技术可以限制向全局作用域中添加过多的变量和函数,从而减少闭包的内存占用。

可以利用闭包创建访问私有变量的公用方法:在构造函数中定义特权方法;使用原型创建静态私有变量

主要应用闭包场合主要是为了:设计私有的方法和变量。有些方法和属性只是运算逻辑过程中的使用的,不想让外部修改这些属性,因此就可以设计一个闭包来只提供方法获取。

(1)匿名自执行函数:有的函数只需要执行一次,其内部变量无需维护,我们创了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。

(2)缓存:设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

(3)实现封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = function() {
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
}();
console.log(person.name); //直接访问,结果为undefined
console.log(person.getName()); //default
person.setName("abruzzi");
console.log(person.getName()); //abruzzi

(4)实现面向对象中的对象,模拟传统的面向对象的语言的模板机制。不同的对象(类的实例)拥有独立的成员及状态,互不干涉.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person() {
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
};
var john = Person();
console.log(john.getName()); //default
john.setName("john");
console.log(john.getName()); //john
var jack = Person();
console.log(jack.getName()); //default
jack.setName("jack");
console.log(jack.getName()); //jack
//john和jack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的。

闭包可能导致的问题

内存泄漏

通常来说,函数的活动对象会随着执行期上下文一起销毁,但是,由于闭包引用另外一个函数的活动对象,因此这个活动对象无法被销毁,这意味着,闭包比一般的函数需要更多的内存消耗。由于IE使用非原生JavaScript对象实现DOM对象,因此不正确地使用闭包会导致内存泄漏问题:

1
2
3
4
5
6
7
8
function A(){
var a = document.createElement("div"),
msg = "Hello";
a.onclick = function(){
alert(msg);
}
}
A();

以上闭包的作用域的循环引用问题:假设A()执行时创建的作用域对象ScopeA,ScopeA引用了DOM对象a,DOM对象a引用了function(alert(msg)),函数function(alert(msg))引用了ScopeA,这是一个循环引用,在IE会导致内存泄露。


(2017.9.13更新)
面试京东广告部的时候,面试官给我提出了对闭包的运行机制的理解,我才发现自己之前的理解太片面了。闭包是跟作用域紧密相连的,光有函数不能定义为闭包,要加上对作用域和垃圾回收的理解。

闭包底层运行机制

这里是重点,值得反复细看。推荐这篇文章:闭包底层运行机制,这里摘抄几个重要的点:

(重点:作用域对象,作用域链,垃圾回收)

在C、C++中,本地变量被保存在中。在JavaScript中,作用域对象是在中被创建的(至少表现出来的行为是这样的),所以在函数返回后它们也还是能够被访问到而不被销毁。

在作用域链中查找变量的过程和原型继承(prototypal inheritance)有着非常相似之处。但是,非常不一样的地方在于,当你在原型链(prototype chain)中找不到一个属性的时候,并不会引发一个错误,而是会得到undefined。但是如果你试图访问一个作用域链中不存在的属性的话,你就会得到一个 ReferenceError

函数在定义时,标识符被添加到当前的作用域中。标识符指向的对象包含:函数的源代码;[[scope]]内部属性(指向函数在定义时,我们所能“直接访问”到的作用域对象,“直接访问”的意思是:在当前作用域链中,该作用域对象处于最底层,没有子作用域对象)。

函数在调用时,会创建一个新的作用域对象,该对象包含该函数能访问的本地变量及arguments。并且,此时形成了作用域链。

当引用一个作用域对象的最后一个引用被解除的时候,并不代表垃圾回收器会立刻回收它,只是它现在可以被回收了。

定义嵌套函数时,嵌套函数的[[scope]]就会引用外围函数的当前作用域对象,如果将这个嵌套函数返回,并被另一个地方的标识符所引用,则这个嵌套函数及其[[scope]]所引用的作用域对象就不会被垃圾回收销毁。

作用域链是不会被复制的。每次函数调用只会往作用域链下面新增一个作用域对象。所以,如果在函数调用的过程当中对作用域链中的任何一个作用域对象的变量进行修改的话,那么同时作用域链中也拥有该作用域对象的函数对象也是能够访问到这个变化后的变量的。这就解释了下面这个例子为什么不能产出我们想要的结果(类似本文代码一):

1
2
3
4
5
6
7
8
9
"use strict";
var elems = document.getElementsByClassName("myClass"), i;
for (i = 0; i < elems.length; i++) {
elems[i].addEventListener("click", function () {
this.innerHTML = i;
});
}

在上面的循环中创建了多个函数对象,所有的函数对象的 [[scope]]都保存着对当前作用域对象的引用。而变量 i 正好就在当前作用域链中,所以循环每次对 i 的修改,对于每个函数对象都是能够看到的。

参考文章

ruanyf/闭包

闭包的用途

闭包总结

分享
0%