JavaScript 作用域链与闭包作用域链
在 js 中当一个函数执行时发生了什么?
刚开始所有的代码都在全局环境里,当调用一个函数的时候就进入了函数的执行环境里。(执行环境也叫作用域),在作用域内部可以访问到外部的属性,这多亏了一条作用域链把全局环境和局部环境连在一块。
作用域链到底是什么呢,本质上来说它是一个指针列表,包含一些对象的引用,作用域内部可以访问这些对象的属性(按顺序来)。
也就是说,只有在作用域链中存在的对象在函数执行时才能访问到。
最外层是全局对象 window, 这个对象有哪些属性呢?基本上我们定义的函数和属性都是在 window 对象上。
现在考虑一个普通的函数
- var x = 10;
- function y() {
- alert(x);
- }
- y();
变量 x, 函数 y 都定义在 window 对象上。当调用 y 函数时,进入 y 的作用域,这个作用域中会有两个对象的引用:window 对象和 y 函数的活动对象,可以把活动对象看作是当前执行环境的代表。当在 y 内调用 x 变量时,首先找一下作用域最前端对象(也就是 y 函数的活动对象)上是否有这个变量,没有,下一个,window 上,有,OK,就你了。这就是作用域链的作用,存储作用域(执行环境)内能访问的对象引用。
这是在函数被调用的时候发生的事情,可是函数在执行时怎么知道自己的作用域链是啥呢?其实在函数定义时就已经将函数外部的作用域链存储起来,在函数执行时只需要简单的在最前端加上函数的活动对象就 OK 了。
那当函数执行完之后呢,没错,它的活动对象就会从作用域链中消失(被垃圾回收);当然这是在没有其他对象引用活动对象的时候,有的话肯定不能回收啊。由此就产生了闭包。
闭包
闭包其实很简单,就是指能访问其他函数作用域中的变量的函数。也就是一个函数而已,只不过这个函数能访问其他函数作用域中的变量。
至于它为什么能访问,这就归功于作用域链了。由前面对作用域的叙述完全可以自己推出来。
看例子:
- function y() {
- var x = 100;
- return function() {
- return x;
- }
- }
- var z = y();
- alert(z());
y 函数内部的匿名函数可以访问到 y 函数的 x 变量。为什么?考虑调用 z 函数的时候:进入 z 函数的作用域,它的作用域链里有什么呢?之前的作用域链 + z 的活动对象。之前的作用域链是什么呢。这就要考虑 z 函数创建的时候,调用 y 创建了 z,z 创建的时候将 y 的作用域链复制了一份保存下来,注意,这份作用域链列表里是两个指针,一个指向 window 对象,一个指向 y 的活动对象。所以 z 的作用域链里总共有三个指针,比 y 的多了一个指向 z 的活动对象的指针。既然 y 对象能通过 z 的作用域链找到,那我们当然可以在 z 中访问它的 x 属性了。
闭包的原理就这么多。完全是依靠作用域链的理论。知道作用域链就算不知道有闭包这个东西也没影响。只不过我们想给这种函数起个名字罢了。
闭包的影响
所谓的影响就是我们要注意的地方,不能想当然的地方。
考查闭包就是考查作用域链,一个经典的题目:
- function y() {
- var result = new Array();
- for (var i = 0; i < 10; i++) {
- result[i] = function() {
- return i;
- };
- }
- return result;
- }
x 函数会返回一个函数数组,函数数组每一项都是一个函数,乍一看你可能以为这十个函数肯定依次会返回 0~9;
那我们来细细分析一下:当我们调用这十个函数中第一个函数 result[0] 的时候进入它的作用域,它会在作用域链中找有 i 变量的对象,很明显有一个,那就是 y 函数的活动对象,ok,问题就是这个 y 活动对象上的变量 i 是多少,别忘了,我们 result[0] 作用域链列表中保存的是指向这个对象的指针,我们不是直接把这个对象复制过来了,这个对象后来已经发生了变化,它的 i 已经加到 10 了!当我们在 result[0] 函数的作用域中去找 i 的时候,它已经是 10 了。所以 result[0] 函数会返回 10!其他的九个函数也都是如此。
另外就是我们知道因为闭包的存在,也就是内部函数要访问外部函数中的参数,所以在外部函数执行完后其活动对象不能被垃圾回收。由此就可能因为大量闭包的存在而浪费内存,正确的做法是不滥用闭包,而且在不需要调用内部函数之后手动断开内部函数对外部函数活动对象的引用。
闭包的利用
闭包的好处就在于内部函数可以访问外部函数中的属性。
而函数再执行时会有两个特殊的属性 this 和 arguments,特殊在哪呢,他们不会像普通属性一样沿着作用域链向上寻找,为什么呢,可以说他们不需要,因为当前函数的活动对象中一定会存在着两个属性。可是这两个属性可能并不是我们想要的,因为一个函数如果没有显式被一个对象调用的话,它的 this 就会指向全局对象 window。
而我们有时候就是想改变调用内部函数的对象,让内部函数和外部函数是同一个对象调用的(这很常用),这时候闭包就起作用了,通过闭包,内部函数可以访问外部函数的属性,就像传递参数一样。
- function outer() {
- var that = this;
- return function() {
- inner.call(that);
- };
- }
- var x = new Object();
- x.outer()();
如果不用闭包 inner 就默认被 window 对象调用。用了之后就被 x 调用了。
arguments 也是同样的道理。这在我的另一篇重写 jQuery 中的 toggle 方法博客中有实例来展示这个应用。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: http://www.92to.com/bangong/2017/03-17/18939530.html