深入理解闭包

6次阅读

共计 2365 个字符,预计需要花费 6 分钟才能阅读完成。

闭包

  • 概念

闭包是一种特殊的对象。

它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数(代号 B)。

当执行 B 时,如果访问了 A 中的变量对象中的值,那么闭包就会产生。

有时候以函数 B 的名字代指这里生成的闭包。而在 Chrome 中,则以执行上下文 A 的函数名代指闭包。

只需要知道一个闭包对象,由 A、B 共同组成即可。

// demo1
function foo() {
    var a = 100;
    var b = 200;
    
    function bar() {return a + b;}
    
    return bar;
}

var bar = foo();
bar();

上面例子中,首先执行上下文 foo,在 foo 中定义了函数 bar,而后通过对外返回 bar 的方式让 bar 得以执行。当 bar 执行时,访问了 foo 内部的变量 a 和 b。因此这个时候 闭包 产生。

在 Chrome 中通过断点调试的方式可以逐步分析该过程,此时 闭包 产生,用 foo 代指,如下图:

上图中,箭头所指的正是 闭包。其中 Call Stack 为当前的函数执行栈,Scope 为当前正在被执行函数的作用域,Local 为当前活动对象。

来看一个非常有意思的例子:

// demo2
function add(x) {return function _add(y) {return x + y;}
}

add(2)(3); // 5

上面的例子有 闭包 产生吗?
当然有。当内部函数_add 被调用执行时,访问了 add 函数变量对象中的 x,这个时候,闭包 就会产生,如下图,一定要记住,函数参数的变量传递给函数之后也会加到变量对象中。

下面代码会产生闭包吗?

// demo3
var name = "window";

var person = {
    name: "perter",
    getName: function() {return function() {return this.name;};
    }
};

var getName = person.getName();
var _name = getName();
console.log(_name);

getName 在执行时,它的 this 其实指向的是 window 对象,而这个时候并没有形成 闭包 的环境,因此这个例子没有 闭包

如果按照下面的方式进行改动呢?

// demo4
// 改动一
var name = "window";

var person = {
    name: "perter",
    getName: function() {return function() {return this.name;};
    }
};

var getName = person.getName();
// 利用 call 的方式让 this 指向 person 对象
var _name = getName.call(person);
console.log(_name);




// demo5
// 改动二
var name = "window";

var person = {
    name: "perter",
    getName: function() {
        // 利用变量保存的方式保证其访问的是 person 对象
        var self = this;
        return function() {return self.name;};
    }
};

var getName = person.getName();
var _name = getName();
console.log(_name);

分别利用 call 与变量保存的方式保证 this 指向的都为 person 对象。所以 demo4(由于 Chrome 已做优化,所以在 Chrome 调试工具中没有显示闭包)和 demo5 都产生了 闭包

  • 闭包与垃圾回收机制

了解垃圾回收机制原理都知道当一个值失去引用之后就会被标记,然后被垃圾回收机制回收并释放空间。

当一个函数的执行上下文运行完毕之后,内部的所有内容都会失去引用而被垃圾回收机制回收。

闭包的本质就是在函数的外部保持了内部变量的引用,因此闭包会不止垃圾回收机制进行回收

下面用一个例子来证明这一点:

function foo1() {
    var n = 99;
    
    nAdd = function() {n += 1;};
    
    return foo2() {console.log(n);
    };
}

var result = foo1();
result(); // 99

nAdd();

result(); // 100

从上面的例子可以看出,因为 nAdd 都访问了 foo1 中的 n,因此它们斗鱼 foo1 形成了 闭包 。这个时候变量 n 的引用被保留了下来。因为 foo2(result) 与 nAdd 执行时都访问了 n,aAdd 每运行一次就会将 n 加 1,所以上例的执行结果非常符合我们的认知。

认识到 闭包 中保存的内容不会被释放之后,我们在使用 闭包 时就要保持足够的警惕性。如果滥用 闭包,很可能会因为内存的原因导致程序性能过差。

  • 闭包与作用域链

结合下面的例子思考一下,闭包会导致函数的作用域链发生改变吗?

var fn = null;

function foo() {
    var a = 2;
    
    function innerFoo() {console.log(a);
    }
    
    fn = innerFoo; // 将 innerFoo 的引用赋值给全局变量中的 fn
}

function bar() {
    var a = 3;
    fn(); // 此处保留 innerFoo 的引用}

foo();
bar(); // 2

在上面的例子中,foo 内部的 innerFoo 访问了 foo 的变量 a。因此当 innerFoo 执行时会有 闭包 产生。全局变量 fn 在 foo 内部获取了 innerFoo 的引用,并在 bar 中执行。

innerFoo 断点调试图如下:

在这里需要特别注意的地方是函数调用栈(Call Stack)与作用域链(Scope)的区别。

因为函数调用栈其实是在代码执行时才确定的,而作用域规则在代码编译阶段就已经确定,虽然作用域链是在代码执行时才生成的,但是它的规则并不会在执行时发生改变。

所以,闭包的存在并不会导致作用域链发生变化。


参考资料:
JavaScript 高级程序设计

JavaScript 核心技术开发揭秘

正文完
 0