您的位置:首页 > Web前端 > JavaScript

JS闭包

2015-09-09 14:20 726 查看
[转自]

闭包基本上是前端面试中必问的一个问题,网上也有很多相关的文章,但没有相对标准的解释。本文是根据我个人的理解,从闭包的原理、特性、应用等角度,讲解下JS中的闭包。
JS闭包定义
JS闭包产生条件
JS闭包原理
JS闭包应用
总结


1. JS闭包定义

JS闭包并没有规范的定义,我个人的理解是:
JS闭包是绑定了定义时环境中变量的函数。


定义总是晦涩难懂的,且看下文分解。


2. JS闭包产生条件

我们先来看一个典型的闭包的例子,然后来总结闭包的产生条件。
function makeAdder(x) {
function add(y) {
return x + y;
}
return add;
}

var add1 = makeAdder(1);
add1(5); // 6


以上的代码首先定义了一个makeAdder函数,在makeAdder函数中定义了一个内嵌的add函数,add函数中使用了makeAdder函数的参数x。 然后调用了
makeAdder(1)
,让add1引用返回的内嵌函数add。函数add1的功能是将一个数字加1,这时你调用
add1(5)
,将返回6. add1其实就是调用
makeAdder(1)
时绑定了环境中变量(x=1)的那个add函数。是不是很熟悉的一句话,我们来看下JS闭包的定义“JS闭包是绑定了定义时环境中变量的函数”。原来上面的代码产生了一个闭包。
现在我们来总结下,JS中闭包的产生条件:
1. 内嵌函数使用了外部函数的局部变量。
2. 外部变量引用内嵌函数。


针对上面的代码案例:
内嵌函数add使用了外部函数makeAdder的局部变量x。
add1引用了调用
makeAdder(1)
时定义的内嵌函数add。

本节中的两个条件可能会被吐槽,因为从理想主义的角度看“只要出现内嵌函数就产生了闭包”。但从实用主义的角度看,针对第一个条件,如果内嵌函数没有使用外部函数的局部变量,就没有必要绑定定义时环境中的变量,事实上浏览器也是这样实现的(后面JS闭包原理中会看到); 针对第二个条件,如果没有外部变量引用内嵌函数,当定义内嵌函数的外部函数执行完后,因为没有对内嵌函数的引用,内嵌函数会被垃圾回收,形成的闭包也没法用了。


3. JS闭包原理

JS闭包绑定定义时环境中的变量是通过JS的函数作用域链实现的,理解闭包原理,先要理解JS的函数作用域链。

在JS中,函数的作用域遵循一下规则:
函数的作用域等于定义函数时的执行环境。
函数的执行环境等于函数执行时的变量环境(局部变量、参数、...)链上函数的作用域。

不少文章都是根据规范,绘制作用域链图来说明闭包产生的原理。因为chrome的调试工具,已经暴露的足够的信息,来说明闭包的产生过程。我们来使用chrome的调试工具来说明闭包产生的原理。我们还是以上文的代码为例:

当进入script代码块时,如下图所示:



此时的执行环境是Global域(如上图右侧第一条红线处标识)。我们在Global上看到了makeAdder函数对象。根据规则1,makeAdder函数的作用域等于此致的执行环境(定义函数时的执行环境),也就是Global(如上图右侧第二条红线处标识)。

当进入makeAdder函数时,如下图所示:



根据规则2,此时的执行环境是makeAdder的变量环境Local,链上makeAdder的作用域:Global(如上图右侧第一条红线处标识)。根据规则2, add函数的作用域等于此时的执行环境(定义函数时的执行环境),也就是makeAdder的变量环境Local链上Global。由于add函数里面只使用了makeAdder变量环境中的变量x,在实现时,add函数的作用域实际是只保存了使用到的变量x的Closure域链上Global(如上图右侧第二条红线处标识)。

当makeAdder函数返回后,如下图所示:



因为add1引用了makeAdder返回的add函数,add函数不会被回收。此时add1就是一个闭包,它是一个函数,并且通过它的作用域绑定了它定义时环境中的变量x(如上图右侧第二条红线处标识)。

至此闭包已经产生,大功告成!

下面我们可以再看下,闭包如何使用绑定的定义时环境中的变量。

当add1执行时,如下图所示:



根据规则2,此时的执行环境是add1的变量环境Local,链上add1的作用域:Closure->Global(如上图右侧第一条红线处标识)。add1中通过变量的作用域链查找,就能够查找到变量x(定义时环境中的变量)。


4. JS闭包应用


4.1 封装

因为闭包绑定的变量,只有闭包可以使用,对外部是隐藏的,很适合用来做封装。比如我们实现一个队列,可以这样:
var queue = (function(){
var arr = [];
var front = -1;
var rear = -1;

return {
enqueue: function(){
},
dequeue: function(){
},
isEmpty: function(){
}
}
})();


用户只能访问暴露的接口(enqueue、dequeue、isEmpty),而无法访问隐藏的细节(arr、front、rear)。


4.2 保存现场

因为闭包绑定的是定义时的变量,可以通过闭包保存定义时的现场,日后在用。加入我们想实现下面的需求:



当我点击上图歌曲列表中的一首歌曲时,打印歌曲的信息。如点击“我爱你中国”,打印“第二首:我爱你中国,演唱者:平安”。

我们可以用下面的代码实现:
function readerList(musics, parent) {
for (var i = 0, length = musics.length; i < length; i++) {
var musicNode = render(music, parent);
// 通过闭包,把歌曲的序号以及歌曲信息绑定到对应的响应函数上
musicNode.onclick = (function(m, index) {
return function() {
alert('第' + index + '首歌:' +  m.name + ', 演唱者:' + m.singer);
};
})(i, music);
}
}
// 根据音乐数据music, 生成歌曲借点,并添加到parent节点下
function render(music, parent) {}
// 渲染歌曲列表
renderList([{
name: '生日快乐歌'
}, {
name: '我爱你中国',
singer: '平安'
}, {
name: '伤不起',
singer: '王麟'
}, {
name: '洋葱',
singer: '平安'
}], musicsNode);


因为在渲染歌曲列表的时候,我们已经有每个歌曲的序号和数据。而在每个歌曲的点击响应函数里面也需要使用这个歌曲的序号和数据,我们可以通过闭包保存现场信息(歌曲的序号和数据),等日后(点击该歌曲时)再用。


5. 总结

1. JS闭包是绑定了定义时环境中变量的函数。2. JS闭包产生的条件是使用了外部函数局部变量的函数被外部引用。
3. JS闭包是通过作用域链实现的。
4. JS闭包常用来封装和保存现场。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  js闭包 绑定