JavaScript 中的闭包

参考https://github.com/mqyqingfeng/Blog/issues/9 参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

定义

MDN 对闭包的定义为

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

《JavaScript 权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

使用闭包的目的

利用闭包实现数据私有化或模拟私有方法。这个方式也称为模块模式(module pattern)
部分参数函数(partial applications)柯里化(currying)

example:

var a = 1

function foo() {
	console.log(a)
}

foo()
1
2
3
4
5
6
7

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 由此构成了闭包。

ECMAScript 中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  • 在代码中引用了自由变量

analysis

var scope = 'global scope'
function checkscope() {
	var scope = 'local scope'
	function f() {
		return scope
	}
	return f
}

var foo = checkscope()
foo()
1
2
3
4
5
6
7
8
9
10
11

这个结果显然是 local scope。

在 checkscope 函数执行过程中,我们执行了函数 f。在 f 执行时,checkscope 的函数上下文已经被销毁,此时函数 f 用过 f 的函数的作用域链找到了 scope。这就是闭包的实现。

question

var data = []

for (var i = 0; i < 3; i++) {
	data[i] = function() {
		console.log(i)
	}
}

data[0]()
data[1]()
data[2]()
1
2
3
4
5
6
7
8
9
10
11

答案都是 3。

当执行到 data[0]() 之前,

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
1
2
3
4
5
6

当执行到 data[0]时,

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
1
2
3

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

改成闭包的

var data = []

for (var i = 0; i < 3; i++) {
	data[i] = (function(i) {
		return function() {
			console.log(i)
		}
	})(i)
}

data[0]()
data[1]()
data[2]()
1
2
3
4
5
6
7
8
9
10
11
12
13

答案 0,1,2

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

闭包中的 this 对象

var name = 'window';
var obj = {
	name : 'object',
	getName:function(){
		return function(){
			return this.name;
		}
	}
}
console.log(obj.getName()());
1
2
3
4
5
6
7
8
9
10

在上面这段代码中,obj.getName()()实际上是在全局作用域中调用了匿名函数,this 指向了 window。 window 才是匿名函数功能执行的环境。

如果使 this 指向外部函数的执行环境

var name = 'window';
var obj = {
	name : 'object',
	getName:function(){
		var self = this
		return function(){
			return self.name;
		}
	}
}
console.log(obj.getName()())
1
2
3
4
5
6
7
8
9
10
11

在闭包中,arguments 与 this 也有相同的问题。下面的情况也要注意:

var name = 'window';
var obj = {
	name :'object',
	getName:function(){
		return this.name;
	}
};
obj.getName();//object
(obj.getName = obj.getName)();//window 非严格模式下
1
2
3
4
5
6
7
8
9

内存泄漏

闭包会引用包含函数的整个变量对象,如果闭包的作用域链中保存着一个 HTML 元素,那么就意味着该元素无法被销毁。所以我们有必要在对这个元素操作完之后主动销毁

function handler(){
	var element = document.getElementById('someElement');
	var id = element.id;
	element.onclick=function(){
		alert(id);
	};
	element = null;
}
1
2
3
4
5
6
7
8

函数内部的定时器

当函数内部的定时器引用了外部函数的变量对象时,该变量对象不会被销毁。

(function(){
	var a = 0;
	setInterval(function(){
		console.log(a++);
	}, 1000);
})();
1
2
3
4
5
6

闭包的应用

应用闭包的主要场合是:设计私有的方法和变量。 任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数内定义的其他函数。

闭包的缺陷

闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

练习

function fun(n,o){
	console.log(o);
	return {
		fun:function(m){
			return fun(m,n);
		}
	}
}
var a = fun(0); //undefind
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0

var b = fun(0).fun(1).fun(2).fun(3);
// undefind 0 1 2

var c = fun(0).fun(1); // undefind 0
c.fun(2); // 1
c.fun(3); // 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
上次更新时间: 10/12/2019, 8:35:23 PM