javascript 闭包详解及作用

  

  在理解 javascript 闭包前,我觉得咱们需要弄清楚执行环境、函数调用栈、作用域链等这些枯燥的术语在 javascript 这门语言中的意义。也许你和我一样,一直都在鞭笞自己学习、应用和总结!

一、执行环境

   执行环境(也叫做执行上下文,Execution Context)是 javascript 中最为重要的一个概念。执行环境定义了变量或函数有权访问其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理时会在后台使用它。

  全局执行环境是最外围的一个执行环境。根据 ECMAScript(以下简称 ES,通常我们都统称为 javascript)实现所在的宿主环境不同,表示执行环境的对象也不一样。在web浏览器中,全局执行环境被认为是 window 对象,因此所有全局对象和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器时才被销毁)。

  每个函数都有自己的执行环境。当执行流,其实也就是线程进入一个函数时,函数的环境就会被推入到一个  环境栈(也叫做函数调用栈)中,同样遵循先进后出、后进先出的存取方式。而在函数执行之后,栈将起环境弹出,把控制权返回给之前的执行环境。ES 程序中的执行流正是由这个方便的机制控制着。

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

  以上代码共涉及三个执行环境:全局环境、changeColor的局部环境、swapColors的局部环境。

  我们很容易知道:

  1. 第一步,首先是全局环境入栈。
  2. 第二步,全局环境入栈之后,执行流将其中的可执行代码开始执行,直到遇到了changeColor() ,这一句激活函数changeColor创建它自己的执行环境,因此执行流将changeColor的执行环境入栈。
  3. 第三步,changeColor的执行环境入栈之后,执行流开始执行其中的可执行代码,遇到swapColors()之后又激活了一个执行环境,因此是执行流将swapColors的执行环境入栈。
  4. 第四步,在swapColors的可执行代码中,再没有遇到其他能生成执行环境的情况,因此这段代码顺利执行完毕,swapColors的执行环境中弹出。
  5. 第五步,swapColors的执行环境弹出之后,继续执行changeColor的可执行代码,也没有再遇到其他执行环境,顺利执行完毕之后弹出,因此此ECStack中就只剩下全局环境了。
  6. 第六步,全局环境在浏览器窗口关闭后出栈。

二、作用域链

  作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

  • 当代码在执行环境中执行时,会创建变量对象的一个作用域链。
  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果当前执行环境是函数,则将其活动对象作为变量对象。
  • 活动对象在最开始的时候只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。
  • 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。
  • 这样一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象。
  • this对象是在运行时基于函数的执行环境绑定的。也就是说在执行环境中一旦使用this,那么就会给这个this指向一个明确的对象。

  结合上述代码以及概念描述,我们可以理解成以下:

标识符解析是沿着作用链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后组件向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)。

在上图中内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数,这些环境之间的联系是线性、有次序的。

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

  来看下面这一条栗子:

function compare(value1, value2){
  if (value1 < value2){
    return -1;
  } else if (value1 > value2){
    return 1;
  } else {
    return 0;
  }
}
var result = compare(5, 10);

以上代码先定义了 compare()函数,然后又在全局作用域中调用了它。当调用 compare()时,会 创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的作用域链中则处于第二位。通过下图可以很明显上述栗子的作用域链。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。 但是,闭包的情况又有所不同。

三、闭包

闭包是指有权访问另一个函数执行环境中的变量的函数。

  如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。

function createComparisonFunction(propertyName) {

    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
} 
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" }); 

  在上述代码中可以看出有两条作用域链。一条是全局环境到createComparisonFunction局部环境的作用域链,另一条则是全局环境到匿名函数局部环境的作用域链。然而另一条作用域链需要依赖createComparisonFunction()函数的活动对象,因此,在 createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparisonFunction()的活动对象。

  在匿名函数从 createComparisonFunction()中被返回后,它的作用域链被初始化为包含 createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在 createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。因此闭包会比其他函数占用更多的内存。

四、闭包的作用

1、封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设我们现在计算所传参数变量的乘积:

    const mult = function (...args) {
        let a = 1
        for (const item of args) {
            a = a * item
        }
        return a
    }

这里值得注意的是`...args`这种写法是 ES6 提供的一种扩展运算符写法,用在函数参数中叫做rest参数写法用来代替 arguments。args 变量直接返回的是一个有序列表数组,所以省去了下面要将 arguments 类数组转换为数组的繁琐、难以理解的步骤。

对于这样一份代码我们会觉得对于那些相同的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:

    let cache = {}
    const mult = function (...args) {if (cache[args]) {
            return cache[args]
        }
        let a = 1
        for (const item of args) {
            a = a * item
        }
        return cache[args] = a
    }

虽然性能有所改进,但是我们看到cache这个变量仅仅在mult函数中被使用,完完全全的暴露在全局作用域下。因此不如把它放在mult函数内部,这样可以减少页面中的全局变量,避免在其他地方不小心被修改而引发错误,代码如下:

    const mult = (function () {
        let cache = {}
        return function (...args) {if (cache[args]) {
                return cache[args]
            }
            let a = 1
            for (const item of args) {
                a = a * item
            }
            return cache[args] = a
        }
    })()

以上代码我们就是通过闭包的形式有效解决了我们遇到的一些问题。但是在实际项目中,我们会为了业务白那些很大一块代码块,因此如果我们在一个大的函数中有一些代码能够独立出来,通常会封装在独立的小函数里面,这样有利于代码复用。再加上一个良好的命名,那样也起到了注释的作用。因此提炼函数是代码重构中的一种常见技巧。

如果这些小函数不需要程序在程序的其他地方使用,那么最好的方式就是用闭包封闭起来。比如上面求乘积的代码我们现在可以这样来写:

    const mult = (function () {
        let cache = {}
        // 抽离计算乘积的函数calcu
        const calcu = function (...args) {
            let a = 1
            for (const item of args) {
                a = a * item
            }
            return a
        }

        return function (...args) {if (cache[args]) {
                return cache[args]
            }
            return cache[args] = calcu.apply(null, args)
        }
    })()

2、延续局部变量的生命周期

Image对象经常用于数据上报,就是只需要将数据通知上报给服务端,而客户端不需要关心服务端的状态和返回值,如下所示:

    const report = function (src) {
        const image = new Image()
        image.src = src
    }
    
    report('http://xxx.ooo.com/reportData')

但是在一些特殊情况下比如低版本浏览器下上报数据会存在丢失一部分数据,也就是说report函数并不是每一次都成功发起了HTTP请求。

丢失数据的原因是因为变量image在report函数中是一个局部变量,当report函数调用结束后,image局部变量会立即被销毁,因此就会有还没得及发出HTTP请求就丢失的情况。为了避免这种情况我们可以把image变量用闭包封闭起来,延长这个局部变量的生命的周期就可以解决请求丢失的问题。如:

    const report = (function () {
        let image = null
        return function (src) {
            image = new Image()
            image.src = src
        }
    })()
posted @ 2018-03-04 19:03  Feesir  阅读(369)  评论(0编辑  收藏  举报