js小记:作用域链 和 闭包
这篇文章主要重点是 闭包,只需要读懂 闭包 那节的例1、例2闭包你就懂了。
文章讲解思路:首先我们讲解下 不同语言的作用域解析方式,其中js用的是静态作用域(其实就是作用域链),然后我们讲讲作用域链,最后我们再开始正题“闭包及其应用”。
一、两种解析
变量作用域两种解析方式:
1、静态作用域(作用域链):
又称词法作用域。使用词法环境来管理。
一段代码执行前,先初始化词法环境。
下面代码若使用静态作用域:
var x = 10;
function foo(){console.log(x);
}
function bar(){var x = 20;foo();
}
bar();//10
看下面作用域链:执行bar函数时,在bar的作用域(scope)中找不到foo函数,然后再到全局作用域( 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.
二、作用域链
作用域链的实质就是一个指向 变量对象/活动对象 的指针列表,它只是引用 不包含实际对象。
# 一些概念: # 作用域链:
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其 活动对象 作为变量对象。活动对象在最开始时只包含一个变量,即 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
例1:
var color = "blue";function changeColor(){if (color === "blue"){color = "red";} else {color = "blue";}
}console.log(changeColor());
在这个简单的例子中,函数 () 的作用域链包含两个对象:它自己的变量对象(其中定义着 对象)和全局环境的变量对象。可以在函数内部访问变量 color,就是因为可以在这个作用域链中找到它。
例2:
var color = "blue";function changeColor(){var anotherColor = "red";function swapColors(){var tempColor = anotherColor;anotherColor = color;color = tempColor;// 这里可以访问color、anotherColor和tempColor}// 这里可以访问color和anotherColor,但不能访问tempColorswapColors();
}// 这里只能访问color
changeColor();
以上代码共涉及3个执行环境:全局环境、() 的局部环境和 () 的局部环境。全局环境中有一个变量 color 和一个函数 ()。() 的局部环境中有一个名为 的变量和一个名为 () 的函数,但它也可以访问全局环境中的变量 color。() 的局部环境中有一个变量 ,该变量只能在这个环境中访问到。无论全局环境还是 () 的局部环境都无权访问 。然而,在 () 内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。下图形象地展示了前面这个例子的作用域链。
上图中的矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。对于这个例子中的 () 而言,其作用域链中包含3个对象:() 的变量对象、() 的变量对象和全局变量对象。() 的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。() 的作用域链中只包含两个对象:它自己的变量对象和全局变量对象。这也就是说,它不能访问 () 的环境。函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
# 延长作用域链:
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:1f();//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
使用闭包的注意点:
思考题:为什么这个输出就都是1?
var f = function(){var i = 0;return function(){console.log(++i);}; };f()();//1f()();//1
解答:
上面很明显没实现闭包。这里的变量 f 保存的并不是 的函数, 我们需要将 的函数 赋给一个全局变量,然后分析同上。
闭包应当是一个封闭内的数据保留,然后由内向外提供一个外部调用接口。这里的问题是外部接口并不是由封闭区域提供。
可以正确工作的例子
var f = (function(){ //这里将return 的函数赋与了全局变量fvar 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.的值,这时我们就要用闭包了。
<li>0li>
<li>1li>
<li>2li>
<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>
改进:我们写个闭包来保存变量现场就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、封装
封装也就是将可见的 和 不可见的分开,就像面向对象编程,有些变量方法是可以暴露给外部的(),而有些是不能暴露给外部的。
封装还有一个好处就是能避免内部变量污染外部环境
var observer = (function(){var observerList = [];return {add:function(obj){observerList.push(obj);},empty:function(){observerList:[];},getCount:function(){return observerList.length;}};
})();
上面代码:
((){})就是一个闭包,而((){})()就是闭包的对象,我们用来保存这个对象。
在外部:
最后我们再来两个检测的例子,看看你是否真的懂了~
练习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