这篇文章主要重点是 闭包,只需要读懂 闭包 那节的例1、例2闭包你就懂了。
文章讲解思路:首先我们讲解下 不同语言的作用域解析方式,其中js用的是静态作用域(其实就是作用域链),然后我们讲讲作用域链,最后我们再开始正题“闭包及其应用”。
一、两种解析
变量作用域两种解析方式:
- 静态作用域:js使用
- 动态作用域:java、c++等使用
1、静态作用域(作用域链):
又称词法作用域。使用词法环境来管理。
一段代码执行前,先初始化词法环境。
下面代码若使用静态作用域:
var x = 10;
function foo(){
console.log(x);
}
function bar(){
var x = 20;
foo();
}
bar();//10
看下面作用域链:执行bar函数时,在bar的作用域(scope)中找不到foo函数,然后再到全局作用域(global scope)中找,正好全局作用域中有,然后执行foo;
执行foo时又找不到x变量,然后到全局作用域中找,发现x=10,然后打印 10
2、动态作用域:
程序运行时决定。动态作用域一般是使用动态栈来管理。
下面代码若使用动态作用域:
var x = 10;
function foo(){
console.log(x);
}
function bar(){
var x = 20;
foo();
}
bar();//20
看下图动态栈:在执行过程中,
(1)首先var x = 10,把x:10放入栈内;
(2)之后再依次执行foo,bar函数的声明,我们再把foo,bar函数放入栈内
(3)在执行bar()时,首先执行var x = 20,我们把x:20放入栈内
(4)然后执行bar函数里面的foo函数,由于foo要打印x变量,所以我们在栈里找到最近的x值。为20,所以打印结果为20.
二、作用域链
作用域链的实质就是一个指向 变量对象/活动对象 的指针列表,它只是引用 不包含实际对象。
# 一些概念:
执行环境:执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。全局环境是最外围的执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样(Web浏览器中,全局执行环境是window对象)。
某个执行环境中所有代码执行完后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。变量对象 / 活动对象:每个执行环境都有一个变量对象,环境中定义的所有变量和函数都保存在这个对象中。如果这个环境是函数,则将其 活动对象 作为变量对象。
# 作用域链:
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问
。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其 活动对象 作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象
(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
例1:
var color = "blue";
function changeColor(){
if (color === "blue"){
color = "red";
} else {
color = "blue";
}
}
console.log(changeColor());
在这个简单的例子中,函数 changeColor() 的作用域链包含两个对象:它自己的变量对象(其中定义着 arguments 对象)和全局环境的变量对象。可以在函数内部访问变量 color,就是因为可以在这个作用域链中找到它。
例2:
var color = "blue";
function changeColor(){
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问color、anotherColor和tempColor
}
// 这里可以访问color和anotherColor,但不能访问tempColor
swapColors();
}
// 这里只能访问color
changeColor();
以上代码共涉及3个执行环境:全局环境、changeColor() 的局部环境和 swapColors() 的局部环境。全局环境中有一个变量 color 和一个函数 changeColor()。changeColor() 的局部环境中有一个名为 anotherColor 的变量和一个名为 swapColors() 的函数,但它也可以访问全局环境中的变量 color。swapColors() 的局部环境中有一个变量 tempColor,该变量只能在这个环境中访问到。无论全局环境还是 changeColor() 的局部环境都无权访问 tempColor。然而,在 swapColors() 内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。下图形象地展示了前面这个例子的作用域链。
上图中的矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。对于这个例子中的 swapColors() 而言,其作用域链中包含3个对象:swapColors() 的变量对象、changeColor() 的变量对象和全局变量对象。swapColors() 的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。changeColor() 的作用域链中只包含两个对象:它自己的变量对象和全局变量对象。这也就是说,它不能访问 swapColors() 的环境。函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
# 延长作用域链:
- with
- try-catch 的catch
with
//作用域链:全局 -> aaa -> o
var o = {b:1}
function aaa(){
var x = 2;
with(o){
var sum = b + x; //b相当于o.b
}
return sum;
}
console.log(aaa()); //3
try-catch
//作用域链:全局 -> catch
try{
throw Error('sss');
}catch(e){
console.log(e); //Error: sss
}
三、闭包
读懂例1、例2,闭包你就懂了!
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量(作用域链)
,另一个就是让这些变量的值始终保持在内存中
。
怎么来理解这句话呢?请看下面的代码。
例1:
function add(){ //function add(){ //一般这么写
var i = 0; // var i = 0;
function add2(){ // return function(){
console.log(++i); // console.log(++i);
} // }
return add2; //}
}
var f = add();
f();//1
f();//2
在这段代码中,f 实际上就是函数 add2。它一共运行了两次,第一次的值是 1,第二次的值是 2。这证明了,函数 add 中的局部变量 i 一直保存在内存中,并没有在 add 调用后被自动清除。
为什么会这样呢?原因就在于add 是 add2 的父函数,而 add2 被赋给了一个全局变量 f ,这导致 add2 始终在内存中,而 add2 的存在依赖于 add,因此 add 也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
注:并不是add 的所有变量都在内存里,而是 add2 所用到的 add 中的变量(或add的参数)保存在内存中。
(add2自己的变量不保存在内存中)
看下面例子:
例2:
function add(){ //function add(){ //一般这么写
var i = 0; // var i = 0;
function add2(){ // return function(){
var a = 0; // var a = 0;
console.log('a:',++a); // console.log('a:',++a);
console.log('i:',++i); // console.log('i:',++i);
} // }
return add2; //}
}
var f = add();
f();//a:1 i:1
f();//a:1 i:2
我们发现并不是 add 的所有变量都保存在内存中(eg: 变量a),而是 add2 所用到的 add 中的变量 i 保存在内存中。
例3
function aaa(d){
var i = 0;
var inout = 0;
console.log('inout:',++inout);
return function(){
var inin = 0;
console.log('i:',++i,' d:',++d,' inin:',++inin);
}
}
var a = aaa(0);
a();
a();
输出:
inout: 1
i: 1 d: 1 inin: 1
i: 2 d: 2 inin: 1
使用闭包的注意点:
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。
解决方法是,在退出函数之前,将不使用的局部变量全部删除或设置为 null,断开变量和内存的联系
。- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。
思考题:为什么这个输出就都是1?
var f = function(){
var i = 0;
return function(){
console.log(++i);
};
};
f()();//1
f()();//1
解答:
上面很明显没实现闭包。这里的变量 f 保存的并不是return 的函数, 我们需要将 return 的函数 赋给一个全局变量,然后分析同上。
闭包应当是一个封闭
function内的数据保留,然后由内向外提供一个外部调用接口。这里的问题是外部接口并不是由封闭区域提供。
可以正确工作的例子
var f = (function(){ //这里将return 的函数赋与了全局变量f
var i = 0;
return function(){
console.log(++i);
};
})();
f();//1
f();//2
或(脑子抽筋了才用下面的写法)
var f = (function(){
var i= 0;
return function(){
return function(){
console.log(++i);
}
}
})()
f()() ;
f()() ;
四、闭包应用
1、保存变量现场
有个需求,我们想绑定一堆DOM节点,然后点击任意节点都能弹出对于的i的值,告诉我们这是第几个节点。
看下面代码:我们会发现无论点击第几个节点,弹出都是nodes.length的值,这时我们就要用闭包了。
<li>0</li>
<li>1</li>
<li>2</li>
<script type="text/javascript">
var aLi = document.getElementsByTagName('li');
var handle = function(nodes){
for(var i = 0;i < nodes.length;i++){
nodes[i].onclick = function(){
alert(i);
}
}
};
handle(aLi);
</script>
改进:我们写个闭包closer来保存变量现场就ok了
写法一:
var aLi = document.getElementsByTagName('li');
var handle = function(nodes){
function closer(i){
return function(){
alert(i);
};
}
for(var i = 0;i < nodes.length;i++){
nodes[i].onclick = closer(i);
}
};
handle(aLi);
写法二:
var aLi = document.getElementsByTagName('li');
var handle = function(nodes){
for(var i = 0;i < nodes.length; i++){
nodes[i].onclick = (function(i){
return function(){
alert(i);
};
})(i);
}
};
handle(aLi);
2、封装
封装也就是将可见的 和 不可见的分开,就像面向对象编程,有些变量方法是可以暴露给外部的(public),而有些是不能暴露给外部的。
封装还有一个好处就是能避免内部变量污染外部环境
var observer = (function(){
var observerList = [];
return {
add:function(obj){
observerList.push(obj);
},
empty:function(){
observerList:[];
},
getCount:function(){
return observerList.length;
}
};
})();
上面代码:
(function(){})就是一个闭包,而(function(){})()就是闭包return的对象,我们用observer来保存这个对象。
在外部:
- 我们不能单独访问数组observerList,这就是不能暴露在外面的。
- 我们必须通过observer对象的方法,才能访问或操作数组observerList,这些就是能暴露在外面的。
最后我们再来两个检测的例子,看看你是否真的懂了~
练习1:
scope = "stone";
function Func() {
var scope = "sophie";
function inner() {
console.log(scope);
}
scope = "tommy";
return inner;
}
var ret = Func();
ret(); // ?
练习2:
scope = "stone";
function Bar() {
console.log(scope);
}
function Func() {
var scope = "sophie";
return Bar;
}
var ret = Func();
ret(); // ?
答案输出是·····························
············
····························
············
··········
···············
·········
·········
············
···········
·········
········
练习1:tommy
练习2:见下图,在Func中调用Bar,因为Func的作用域没找到Bar,所以到全局作用域中寻找Bar;找到后函数Bar又调用了变量scope,因为Bar作用域中没有变量scope,所以又回到全局作用域中寻找,发现scope=”stone”.
所以答案为 : stone