女兒吃大雞巴-跟我学习javascript的闭包
函数
石景数学网
引用
2021-09-07

JavaScript 女兒吃大雞巴究竟是什么?

用JavaScript一年多了,女兒吃大雞巴总是让人二丈和尚摸不着头脑。陆陆续续接触了一些女兒吃大雞巴的知识,也犯过几次因为不理解女兒吃大雞巴导致的错误,一年多了资料也看了一些,但还是不是非常明白,最近偶然看了一下 jQuery基础教程 的附录,发现附录A对JavaScript的女兒吃大雞巴的介绍简单易懂,于是借花献佛总结一下。

1、定义

女兒吃大雞巴:是指有权访问另外一个女兒吃大雞巴作用域中的变量的女兒吃大雞巴。创建女兒吃大雞巴的常见方式就是在一个女兒吃大雞巴内部创建另外一个女兒吃大雞巴。

直接上例子

function a(){ var i=0; function b(){ alert(++i); } return b; } var c = a(); c();

这段代码有两个特点:

1)、女兒吃大雞巴b嵌套在女兒吃大雞巴a内部;

这样在执行完var c=a()后,变量c实际上是指向了女兒吃大雞巴b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个女兒吃大雞巴,为什么?因为女兒吃大雞巴a外的变量c引用了女兒吃大雞巴a内的女兒吃大雞巴b,就是说:

当女兒吃大雞巴a的内部女兒吃大雞巴b被女兒吃大雞巴a外的一个变量引用的时候,就创建了一个女兒吃大雞巴。

我猜想你一定还是不理解女兒吃大雞巴,因为你不知道女兒吃大雞巴有什么作用,下面让我们继续探索。

2、女兒吃大雞巴有什么作用?

简而言之,女兒吃大雞巴的作用就是在a执行完并返回后,女兒吃大雞巴使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部女兒吃大雞巴b的执行需要依赖a中的变量。这是对女兒吃大雞巴作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解女兒吃大雞巴需要循序渐进的过程。

在上面的例子中,由于女兒吃大雞巴的存在使得女兒吃大雞巴a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。

那么我们来想象另一种情况,如果a返回的不是女兒吃大雞巴b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引用,因此女兒吃大雞巴a和b互相引用但又不被外界打扰(被外界引用),女兒吃大雞巴a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)

3、女兒吃大雞巴内的微观世界

如果要更加深入的了解女兒吃大雞巴以及女兒吃大雞巴a和嵌套女兒吃大雞巴b的关系,我们需要引入另外几个概念:女兒吃大雞巴的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以女兒吃大雞巴a从定义到执行的过程为例阐述这几个概念。

1)、当定义女兒吃大雞巴a的时候,js解释器会将女兒吃大雞巴a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局女兒吃大雞巴,则scope chain中只有window对象。

2)、当女兒吃大雞巴a执行的时候,a会进入相应的执行环境(excution context)。

3)、在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。

4)、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。

5)、下一步是在活动对象上添加一个arguments属性,它保存着调用女兒吃大雞巴a时所传递的参数。

6)、最后把所有女兒吃大雞巴a的形参和内部的女兒吃大雞巴b的引用也添加到a的活动对象上。在这一步中,完成了女兒吃大雞巴b的的定义,因此如同第3步,女兒吃大雞巴b的作用域链被设置为b所被定义的环境,即a的作用域。

到此,整个女兒吃大雞巴a从定义到执行的步骤就完成了。此时a返回女兒吃大雞巴b的引用给c,又女兒吃大雞巴b的作用域链包含了对女兒吃大雞巴a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和女兒吃大雞巴。女兒吃大雞巴b被c引用,女兒吃大雞巴b又依赖女兒吃大雞巴a,因此女兒吃大雞巴a在返回后不会被GC回收。

当女兒吃大雞巴b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,当在女兒吃大雞巴b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索女兒吃大雞巴a的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果女兒吃大雞巴b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。

4、女兒吃大雞巴的应用场景

1)、保护女兒吃大雞巴内的变量安全。以最开始的例子为例,女兒吃大雞巴a中i只有女兒吃大雞巴b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。

2)、在内存中维持一个变量。依然如前例,由于女兒吃大雞巴,女兒吃大雞巴a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。

5、Javascript的垃圾回收机制

在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为女兒吃大雞巴a被b引用,b又被a外的c引用,这就是为什么女兒吃大雞巴a执行后不会被回收的原因。

在javascript中没有块级作用域,一般为了给某个女兒吃大雞巴申明一些只有该女兒吃大雞巴才能使用的局部变量时,我们就会用到女兒吃大雞巴,这样我们可以很大程度上减少全局作用域中的变量,净化全局作用域。

使用女兒吃大雞巴有如上的好处,当然这样的好处是需要付出代价的,代价就是内存的占用。

如何理解上面的那句话呢?

每个女兒吃大雞巴的执行,都会创建一个与该女兒吃大雞巴相关的女兒吃大雞巴执行环境,或者说是女兒吃大雞巴执行上下文。这个执行上下文中有一个属性 scope chain(作用域链指针),这个指针指向一个作用域链结构,作用域链中的指针又都指向各个作用域对应的活动对象。正常情况,一个女兒吃大雞巴在调用开始执行时创建这个女兒吃大雞巴执行上下文及相应的作用域链,在女兒吃大雞巴执行结束后释放女兒吃大雞巴执行上下文及相应作用域链所占的空间。

//声明女兒吃大雞巴 function test(){ var str = "hello world"; console.log(str); } //调用女兒吃大雞巴 test();

在调用女兒吃大雞巴的时候会在内存中生成如下图的结构:

但是女兒吃大雞巴的情况就有点特殊了,由于女兒吃大雞巴女兒吃大雞巴可以访问外层女兒吃大雞巴中的变量,所以外层女兒吃大雞巴在执行结束后,其作用域活动对象并不会被释放(注意,外层女兒吃大雞巴执行结束后执行环境和对应的作用域链就会被销毁),而是被女兒吃大雞巴女兒吃大雞巴的作用域链所引用,直到女兒吃大雞巴女兒吃大雞巴被销毁后,外层女兒吃大雞巴的作用域活动对象才会被销毁。这也正是女兒吃大雞巴要占用内存的原因。

所以使用女兒吃大雞巴有好处,也有坏处,滥用女兒吃大雞巴会造成内存的大量消耗。

使用女兒吃大雞巴还有其他的副作用,可以说是bug,也可以说不是,相对不同的业务可能就会有不同的看法。

这个副作用是女兒吃大雞巴女兒吃大雞巴只能取到外层女兒吃大雞巴变量的最终值。

测试代码如下:(这里使用了jquery对象)

/*女兒吃大雞巴缺陷*/ (function($){ var result = new Array(), i = 0; for(;i

上面的代码先通过匿名女兒吃大雞巴表达式开辟了一块私有作用域,这个匿名女兒吃大雞巴就是我们上面所说的外层女兒吃大雞巴,该外层女兒吃大雞巴有一个参数$,同时还定义了变量result和 I , 通过for循环给数组result赋值一个匿名女兒吃大雞巴,这个匿名女兒吃大雞巴就是女兒吃大雞巴,他访问了外层女兒吃大雞巴的变量I , 理论上数组resulti 会返回相应的数组下标值,实际情况却不如所愿。

如上代码 $.RES10 的执行结果是10.

为什么会这样呢,因为i的最终值就是10.

下面我们通过下图来详细说明下,上面的那段代码执行时在内存中到底发生了什么:

那么这个副作用有没有办法可以修复呢?当然可以!

我们可以通过下面的代码来达到我们的预期。

/*修复女兒吃大雞巴缺陷*/ (function($){ var result = new Array(), i = 0; for(;i

上面的代码又在内存中发生了什么?我们同样用下面的一幅图来详细解释。看懂了上面的图,我们也就不难理解下面的图。

6.简单的例子

首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码

0 1 2 3 0 1 2 3 $(document).ready(function() { var spans = $("#divTest span"); for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } } });

很简单的功能可是却偏偏出错了,每次alert出的值都是4,简单的修改就好使了

var spans2 = $("#divTest2 span"); $(document).ready(function() { for (var i = 0; i < spans2.length; i++) { (function(num) { spans2[i].onclick = function() { alert(num); } })(i); } });

7.内部女兒吃大雞巴

让我们从一些基础的知识谈起,首先了解一下内部女兒吃大雞巴。内部女兒吃大雞巴就是定义在另一个女兒吃大雞巴中的女兒吃大雞巴。例如:

function outerFn () { functioninnerFn () {} }

innerFn就是一个被包在outerFn作用域中的内部女兒吃大雞巴。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会导致一个JavaScript错误:

function outerFn() { document.write("Outer function"); function innerFn() { document.write("Inner function"); } } innerFn();//Uncaught ReferenceError: innerFn is not defined

不过在outerFn内部调用innerFn,则可以成功运行:

function outerFn() { document.write("Outer function"); function innerFn() { document.write("Inner function"); } innerFn(); } outerFn();

8、伟大的逃脱(内部女兒吃大雞巴如何逃脱外部女兒吃大雞巴)

JavaScript允许开发人员像传递任何类型的数据一样传递女兒吃大雞巴,也就是说,JavaScript中的内部女兒吃大雞巴能够逃脱定义他们的外部女兒吃大雞巴。

逃脱的方式有很多种,例如可以将内部女兒吃大雞巴指定给一个全局变量:

//定义全局变量逃脱 var globalVar; function outerFn() { document.write("Outer function"); function innerFn() { document.write("Inner function"); } globalVar = innerFn; } outerFn(); //Outer function Inner function globalVar(); //Outer function Inner function innerFn(); //ReferenceError: innerFn is not defined

调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部女兒吃大雞巴虽然通过把引用保存在全局变量中实现了逃脱,但这个女兒吃大雞巴的名字依然只存在于outerFn的作用域中。

也可以通过在父女兒吃大雞巴的返回值来获得内部女兒吃大雞巴引用

function outerFn() { document.write("Outer function"); function innerFn() { document.write("Inner function"); } return innerFn; } var fnRef = outerFn(); fnRef();

这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。

这种即使离开女兒吃大雞巴作用域的情况下仍然能够通过引用调用内部女兒吃大雞巴的事实,意味着只要存在调用内部女兒吃大雞巴的可能,JavaScript就需要保留被引用的女兒吃大雞巴。而且JavaScript运行时需要跟踪引用这个内部女兒吃大雞巴的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间(红色部分是理解女兒吃大雞巴的关键)。

说了半天总算和女兒吃大雞巴有关系了,女兒吃大雞巴是指有权限访问另一个女兒吃大雞巴作用域的变量的女兒吃大雞巴,创建女兒吃大雞巴的常见方式就是在一个女兒吃大雞巴内部创建另一个女兒吃大雞巴,就是我们上面说的内部女兒吃大雞巴,所以刚才说的不是废话,也是女兒吃大雞巴相关的 ^_^

9、变量的作用域

内部女兒吃大雞巴也可以有自己的变量,这些变量都被限制在内部女兒吃大雞巴的作用域中:

function outerFn() { document.write("Outer function"); function innerFn() { var innerVar = 0; innerVar++; document.write("Inner function\t"); document.write("innerVar = "+innerVar+""); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();

每当通过引用或其它方式调用这个内部女兒吃大雞巴时,就会创建一个新的innerVar变量,然后加1,最后显示

Outer function Inner function innerVar = 1 Inner function innerVar = 1 Outer function Inner function innerVar = 1 Inner function innerVar = 1

内部女兒吃大雞巴也可以像其他女兒吃大雞巴一样引用全局变量:

var globalVar = 0; function outerFn() { document.write("Outer function"); function innerFn() { globalVar++; document.write("Inner function\t"); document.write("globalVar = " + globalVar + ""); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();

现在每次调用内部女兒吃大雞巴都会持续地递增这个全局变量的值:

Outer function Inner function globalVar = 1 Inner function globalVar = 2 Outer function Inner function globalVar = 3 Inner function globalVar = 4

但是如果这个变量是父女兒吃大雞巴的局部变量又会怎样呢?因为内部女兒吃大雞巴会引用到父女兒吃大雞巴的作用域(有兴趣可以了解一下作用域链和活动对象的知识),内部女兒吃大雞巴也可以引用到这些变量

function outerFn() { var outerVar = 0; document.write("Outer function"); function innerFn() { outerVar++; document.write("Inner function\t"); document.write("outerVar = " + outerVar + ""); } return innerFn; } var fnRef = outerFn(); fnRef(); fnRef(); var fnRef2 = outerFn(); fnRef2(); fnRef2();

这一次结果非常有意思,也许或出乎我们的意料

Outer function Inner function outerVar = 1 Inner function outerVar = 2 Outer function Inner function outerVar = 1 Inner function outerVar = 2

我们看到的是前面两种情况合成的效果,通过每个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次女兒吃大雞巴调用的作用域创建并绑定了一个一个新的outerVar实例,两个计数器完全无关。

当内部女兒吃大雞巴在定义它的作用域的外部被引用时,就创建了该内部女兒吃大雞巴的一个女兒吃大雞巴。这种情况下我们称既不是内部女兒吃大雞巴局部变量,也不是其参数的变量为自由变量,称外部女兒吃大雞巴的调用环境为封闭女兒吃大雞巴的环境。从本质上讲,如果内部女兒吃大雞巴引用了位于外部女兒吃大雞巴中的变量,相当于授权该变量能够被延迟使用。因此,当外部女兒吃大雞巴调用完成后,这些变量的内存不会被释放(最后的值会保存),女兒吃大雞巴仍然需要使用它们。

10.女兒吃大雞巴之间的交互

当存在多个内部女兒吃大雞巴时,很可能出现意料之外的女兒吃大雞巴。我们定义一个递增女兒吃大雞巴,这个女兒吃大雞巴的增量为2

function outerFn() { var outerVar = 0; document.write("Outer function"); function innerFn1() { outerVar++; document.write("Inner function 1\t"); document.write("outerVar = " + outerVar + ""); } function innerFn2() { outerVar += 2; document.write("Inner function 2\t"); document.write("outerVar = " + outerVar + ""); } return { "fn1": innerFn1, "fn2": innerFn2 }; } var fnRef = outerFn(); fnRef.fn1(); fnRef.fn2(); fnRef.fn1(); var fnRef2 = outerFn(); fnRef2.fn1(); fnRef2.fn2(); fnRef2.fn1();

我们映射返回两个内部女兒吃大雞巴的引用,可以通过返回的引用调用任一个内部女兒吃大雞巴,结果:

Outer function Inner function 1 outerVar = 1 Inner function 2 outerVar = 3 Inner function 1 outerVar = 4 Outer function Inner function 1 outerVar = 1 Inner function 2 outerVar = 3 Inner function 1 outerVar = 4

innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。我们也看到对outerFn的后续调用还会创建这些女兒吃大雞巴的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而女兒吃大雞巴就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。

11.解惑

现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会alert 4了。

for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } }

上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个span的onclick方法这时候为内部女兒吃大雞巴,所以i被女兒吃大雞巴引用(女兒吃大雞巴引用传的是引用),内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick女兒吃大雞巴销毁(主动把女兒吃大雞巴赋为null或者页面卸载)时才会被回收。这样每次我们点击span的时候,onclick女兒吃大雞巴会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。

而第二种方式是使用了一个立即执行的女兒吃大雞巴又创建了一层女兒吃大雞巴,女兒吃大雞巴声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,女兒吃大雞巴立即执行,num保存每次i的值。

这一通下来想必大家也和我一样,对女兒吃大雞巴有所了解了吧,当然完全了解的话需要把女兒吃大雞巴的执行环境和作用域链搞清楚。