天天看点

深入浅出之JS闭包

一 定义:

MDN对闭包的定义为:

闭包是指那些能访问到自由变量的函数
           

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量
           

举个例子:

在函数作用域中使用的变量x,却没有在函数作用域中声明(即在其他作用域中声明的),对于函数作用域来说,x就是一个自由变量

let x = 10;
function fn() {
      let b = 5;
      console.log(x + b)	
}
           

由此可以看出闭包由两部分组成:

闭包 = 函数 + 函数能够访问到的自由变量
           

举个例子:

var a = 1;

function foo() {
    console.log(a);
}

foo();
           

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

还真是这样的!

所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?

别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:

    a. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)

    b. 在代码中引用了自由变量

二 分析

让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();
           

首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。

另一个与这段代码相似的例子,在《JavaScript深入之执行上下文》中有着非常详细的分析。如果看不懂以下的执行过程,建议先阅读这篇文章。

这里直接给出简要的执行过程:

1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈

2. 全局执行上下文初始化

3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈

4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等

5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈

7. f 执行上下文初始化,创建变量对象、作用域链、this等

8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
           

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……)

然而 JavaScript 却是可以的!

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
           

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)

2. 在代码中引用了自由变量

           

在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings)
in which the function’s variables are resolved is called a closure in the 
computer science literature.
           

闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。

三 实现

最经典的两种实现方式:

1. 函数作为返回值

2. 函数作为参数传递

函数作为返回值:

function fn() {
    let  x = 10;
    return function() {
        console.log(x)
    }
}    

fn()()
           

函数作为参数传递:

function fn(x) {
   
    console.log(x)
}    

(function (f) {
    var x = 5;
    f(x)
})(fn)
           

其他例子:

函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。

function adder(x) {
    return function(y) {
        console.log(x + y)
    }
}

var add1 = adder(5)
var add2 = adder(2)

console.log(add1(2)) // 7
console.log(add2(10)) // 12

//释放对闭包的引用
add1 = add2 = null
           

从上述代码可以看到add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。最后通过 null 释放了 add5 和 add10 对闭包的引用。

另外一个例子:

var name = "window"
var obj = {
    name: "object",
    getName() {
        return function fn () {
            console.log(this.name)
        }
    }
}

obj.getName()() //window
           

闭包函数的this指向,this指向永远都是指向其运行的上下文环境,并不是定义的上下文环境,所以这里obj.getName()执行完后得到的是一个函数fn此时的上文环境已经变成了全局上下文,所以指向的是window

四 应用

主要应用:设计私有的方法和变量,定时器,事件函数等

var myNamespace = (function () {

  var myPrivateVar, myPrivateMethod;

  // A private counter variable
  myPrivateVar = 0;

  // A private function which logs any arguments
  myPrivateMethod = function( foo ) {
      console.log( foo );
  };

  return {

    // A public variable
    myPublicVar: "foo",

    // A public function utilizing privates
    myPublicFunction: function( bar ) {

      // Increment our private counter
      myPrivateVar++;

      // Call our private method using bar
      myPrivateMethod( bar );

    }
  };

})();
           

上面的自执行函数返回的是一个对象,而这个对象中的方法,就产生了一个闭包自执行函数的上下文执行完后会被销毁,但是因为返回了一个对象中的方法引用了自由变量,所以产生了闭包

五 缺陷

闭包的缺点就是因为其变量不会被及时回收,自由当不再引用这个闭包时才会被垃圾回收掉,所以会增大内存使用量,并且使用不当很容易造成内存泄露。

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

引用:

染陌同学-JavaScript深入之闭包

王福朋-深入理解javascript原型和闭包(15)——闭包

MDN闭包

继续阅读