闭包共分为三种:函数闭包,对象闭包(with)与异常闭包(catch)。函数闭包已经被人说烂了,不想重复了,异常闭包在某些浏览器中实现得不太完美,而且实用价值不大,也不说了。对象闭包,我只是偶尔用来集体修改样式,用得也不多。我不常用它,是因为网上关于它的风闻一向不太好,什么效率太低啦,修改作用链啦,等等。精彩论战看这里。
我们看一道题,出处无忧。
var b=15; function foo() { var a={b:20}; b=10; with(a) { var b=30;//★★★ } alert(a.b); alert(b); } foo(); alert(b);
问题的焦点在星号标出的位置,通常我们直接为对象的属性赋值就是,现在多出个var,会产生很奇怪的副作用。
首先,它还是先修改a对象的同名属性b,这是在运行期发生的。
其次,它在预编译阶段带来了些少不同,多出了一个默认值为undefined的变量,换言之,变量声明凑效了。但接着的问题是,这个变量究竟是放在哪一层的作用域之内?放在a元素中,不可能,这就和b=30没什么两样,放在函数foo的作用域外,也不可能,这会导致在运行期时,b= 10会覆盖 b= 15,而我们想看到的结果为15。这就说明,它是位于foo的作用域之内,a对象之外。我们还可以通过一些手段来证明其在运行期是不赋值。鉴于一般人看小说都是喜欢看完结局再欣赏过程,我就先给出答案再解释吧。这上面的脚本基本上等价于下面这个:
var b=15; function foo() { var a={b:20}; a.b = 30; //①★★★ var b;//②★★★ b=10; alert(a.b); alert(b); } foo(); alert(b);
①与②的顺序并不重要,重要的是它们肯定是紧跟于a对象的后面。我们可以用以下方法来证明我的结论。将原先的脚本改为下面这个,为了方便,我们叫它做“调整者”:
//调整者 var b=15; function foo() { b=10; //把b调到最前面。 var a={b:20}; with(a) { var b=30;//★★★ } alert(a.b); alert(b); } foo(); alert(b);
如果我的结论是正确的,它alert弹出的结果应该来下面的“调整者2”是相同的
//调整者2 var b=15; function foo() { b=10; //把b调到最前面。 var a={b:20}; a.b = 30; var b; alert(a.b); alert(b); } foo(); alert(b);
反正①与②是同时生成,肯定是放在一块的,如果将它们放到最前端,将会报错。
//调整者3 var b=15; function foo() { var b; alert(a.b); b=10; //把b调到最前面。 var a={b:20}; a.b = 30; alert(b); } foo(); alert(b);
放到最后面(alert(b)),得出结果也不一致。
接着我们来证实为什么是var b而不是var b = 30。
//调整者4 var b=15; function foo() { b=10; var a={b:20}; a.b = 30; var b = 30; alert(a.b); alert(b); } foo(); alert(b);
弹出两个30与一个15,与“调整者”的结果存在差异,说明var b是只声明不赋值。当然,这样的穷举法很劣拙,不过对于这样简单的脚本来说够用了。
通过以上脚本我们得出一个事实,对象闭包的作用域是位于原对象的下方而非上方或内部,而在with内部用var来声明变量,此变量也不会在闭包的作用域内。
我们来回忆一下闭包的概念。闭包,是指语法域位于某个特定的区域,具有持续参照(读写)位于该区域内自身范围之外的执行域上的非持久型变量值能力的段落。这些外部执行域的非持久型变量神奇地保留它们在闭包最初定义(或创建)时的值(深连结)。我们再看一下那个作用域,它引用的是它旁边的a对象,换言之,只要它不消失,a对象所占的内存就不能释放。因此那个作用域完全有资格被称之为“闭包”!只不过是对象闭包里面装载的是永远它引用的那个对象的属性,如果我们用delete等手段删除这些属性,和其他作用域一样,当JS引擎找不到这属性,它就会往外围的作用域去找与这属性同名的变量。这个我已经在《javascript变量的作用域》中提过了。下面我们一同做一道题,复习刚才学到的知识。(感谢winter-cn 提供这么好的题目)
function empty(){} empty.prototype={a:0}; var o=new empty(); o.a=1 a=2; with(o) { alert(a); var a=3; alert(a); delete o.a; alert(a); delete o.a; alert(a); delete empty.prototype.a; alert(a); }
我们很轻松就很画出其在预编译阶段的变量图:
下面我们一步步来分析此题。
- 第1行定义一个empty类。
- 第2行为其原型添加一个属性a,值为0。
- 第3行初始化此类,赋给一个o变量。
- 第4行极晚绑定一个实例属性a。
- 第5行为变量a赋值为2(a是预编译时留下的空壳)。
- 第6行开始我们就进行对象闭包的作用域内了。
- 第8行的alert,要求弹出a的值。JS先从对象的属性找起,如果没有才从闭包外找。那么究竟是极晚绑定的那个实例属性,还是原型属性呢?!要知道原型属性也是实例属性,那个原型可以看作是其“父类”的实例。那个极晚绑定的属性怎么也算是“子类”的实例属性吧,因此既然有子类的,它就不往上找了!弹出的是1。
- 第9行是个赋值语句,上面已经分析过了,在对象闭包中进行var变量的赋值,会有一个副作用,就是在闭包外产生一个同名的var变量声明,但对o对象的属性的修改还是进行的。换言之,到这里时,那个极晚绑定的实例属性a的值已变为3了。
- 第10行,有了上面的分析,不难得出答案。
- 第11行,删除o对象的实例属性a。
- 第12行,由于o对象已经不存在a属性了,于是沿着原型链往上找,我的图表示为o的作用域的外围作用域。发现原型中存在同名属性,alert为0。
- 第13行,删除o对象的实例属性a,由于a早就没有了,因此是没有意义的行为。
- 第14行,继续弹出o对象的原型属性a的值。
- 第15行,删除o对象的原型属性a。
- 第16行,由于o对象已经没有a这个属性了,换言之,闭包中没有a这个变量了,于是迫使JS引擎跳出闭包往外找。在预编译时,外面已经有一个a变量了,而在运行期执行到第5行时重新赋值为2。于是一找就找到它,弹出2。
对象闭包的作用域。