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();}// 这里只能访问colorchangeColor();
以上代码共涉及3个执行环境:全局环境、() 的局部环境和 () 的局部环境 。全局环境中有一个变量 color 和一个函数 () 。() 的局部环境中有一个名为的变量和一个名为 () 的函数,但它也可以访问全局环境中的变量 color 。() 的局部环境中有一个变量 ,该变量只能在这个环境中访问到 。无论全局环境还是 () 的局部环境都无权访问。然而,在 () 内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境 。下图形象地展示了前面这个例子的作用域链 。

js小记:作用域链 和 闭包

文章插图
上图中的矩形表示特定的执行环境 。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数 。这些环境之间的联系是线性、有次序的 。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境 。对于这个例子中的 () 而言,其作用域链中包含3个对象:() 的变量对象、() 的变量对象和全局变量对象 。() 的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链 。() 的作用域链中只包含两个对象:它自己的变量对象和全局变量对象 。这也就是说,它不能访问 () 的环境 。函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同 。
# 延长作用域链:
with
//作用域链:全局 -> aaa -> ovar 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
//作用域链:全局 -> catchtry{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();//1f();//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:1i:1f();//a:1i:2
我们发现并不是 add 的所有变量都保存在内存中(eg: 变量a),而是 add2 所用到的 add 中的变量 i 保存在内存中 。
例3
【js小记:作用域链 和 闭包】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();//1f();//2
或(脑子抽筋了才用下面的写法)
var f = (function(){var i= 0; return function(){return function(){console.log(++i);}}})() f()() ;f()() ;
四、闭包应用 1、保存变量现场
js小记:作用域链 和 闭包

文章插图
有个需求,我们想绑定一堆DOM节点,然后点击任意节点都能弹出对于的i的值,告诉我们这是第几个节点 。
看下面代码:我们会发现无论点击第几个节点,弹出都是nodes.的值,这时我们就要用闭包了 。
  • 0
  • 1
  • 2

  • 改进:我们写个闭包来保存变量现场就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
    js小记:作用域链 和 闭包

    文章插图