再谈闭包,闭包为什么可以保存变量以及参数的原理。
首先复习一下execution context这个object创建的时候都做了啥:
executionContextObj = {
variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ }, this: {} }
而variableObject中包括活动对象activation object(http://programmers.stackexchange.com/questions/189967/what-is-an-activation-object-in-javascript)以及全局对象global object。比较重要的一点是:全局变量对象始终存在,局部环境的变量对象函数中用完即销毁除非这个变量对象还在被引用或者说它的引用计数不为0.
还拿下面这个小闭包的例子做分析
for (var i = 0; i<5; i++) { (function () { var a = i; console.log(i) setTimeout(() => { console.log(i); }) })() }
做了闭包的情况下,这个IIFE里的setTimeout的callback在执行的时候会创建execution context:(不考虑console.log本身的情况)
variableObject 没啥关系,不分析 | 2:-----指向----->全局变量对象: i (最外层)
scopeChain ---------会指向------> 作用域链: |1:-----指向------> IIFE的活动对象:arguments(没有), a(指向i).
this 没啥关系,不分析 | 0:-----指向-----> 没有啥东西.
这么看的话,callback function需要用到a, 而a又需要i, 所以每个i的值都需要被引用(所以引用计数不为0)所以就这么被保存下来。
不过研究到这里似乎又出现了一个大大的问题,不是说赋值基本类型值的时候不是指针吗啊啊啊?为什么书上这里写的是箭头???
于是我又写了两个小例子:
var a = 2; function foo() { var b = a; a++; (function foo1() { console.log(a); console.log(b); })() } foo(); // 输出结果为:3和2</span></span>
如果这么写:
var a = [1,3]; function foo() { var b = a; a[0]++; (function foo1() { console.log(a); console.log(b); })() } foo(); // 输出结果为 [2,3] 和 [2,3]</span></span>
所以其实书里用箭头有那么一点点误导,所以我的理解是:如果采用闭包,activation object中的基本类型值(number string boolean)仍旧采用复制一块新内存的方式来保存,例如上面第一个小程序,每次IIFE都在新的memory中复制出了一个当前循环中i的值来供里面的setTimeout的callback来用,这几个memory对应的引用计数都为1,所以不会被当做垃圾被回收掉。第二个例子中使用的是array,是assign一个引用类型值(object),这个时候,我们复制的其实是复制了一个指针存在memory中并指向那个array,所以a的第一项变了,那么b也跟着一起变了。
下面的这个例子呢?
var arr = [0, 5]; for (arr[0]; arr[0]<arr[1]; arr[0]++) { (function () { var a = arr; console.log(arr) setTimeout(() => { console.log(a[0]); }) })() }
这一次的输出结果呢?5次5!!!!!利用闭包存储的这个a指针仍然存在,只不过它指向的这个本来是[0,5]的数组变成了[5,5]; (然后... 什么? 怎么是5? 为什么不是4?思考一下还是挺有意思的)
关于运算优先级的问题:
突然想起一个特别有意思的问题,
var foo = {a:1} var bar = foo; foo.b = foo = {c:2} console.log(foo.b); console.log(bar.b);
结果是undefined和object{c:2}
感觉如果不了解运算机制可能很难推导出正确的结果。
通常计算过程是从右向左的,但是如果有 ‘.‘ 的话,会优先 ‘.’ 的部分,因此在
foo.b = foo = {c:2}
的时候,会优先生成b这个foo所指向的object(即{a:1})的property,因此这个b的出现导致 {a:1}变成了{a:1, b:undefined} ----》然后开始进行连续赋值的过程:第一步就是让a指向{n:2}, 然后再让a.x指向{n:2}, 可是这个时候由于上一步的时候已经将foo.b解析为{a:1, b:undefined}这个object的b, 所以第二个赋值过程(也就是第一个等号)实际上是把{c:2}赋值给{a:1,
b:undefined}的b然后变成了{a:1, b:{c:2}}
因此 console.log(foo.b)的时候解析foo.b发现并不存在, 所以是undefined,
而console.log(bar.b),由于b始终指向{a:1}(后来变成了{a:1, b:{c:2}}),因此输出为 object {c: 2}
总而言之,当给一个‘指向object的variable来增加/改写这个object的property’的时候,始终要记得增加改写的其实没有变过,都是那个object本身。
另外很重要的是,遇到连等这种赋值,如果有‘.‘ 这种操作符,会优先并且只进行一次解析,就像小程序中foo.b被解析为指向{a:1}这个object,就不会更改了(应该是? 但如果以后看到黑魔法我回来做update)。