JavaScript中的闭包

文章同步到githubhtml

js的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成如下文章。java

什么是闭包

我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然表达的不同,可是都在对闭包作了最正确的定义和翻译,也帮助你们更好的理解闭包,这阅读起来可能比较模糊,你们日后看,本文经过对多个经典书籍中的例子讲解,相信会让你们能很好的理解js中的闭包。文章开始,我会先铺垫一下闭包的概念和为何要引入闭包的概念,而后结合例子来讲明讲解,并讲解如何使用闭包。git

百度百科中的定义:

闭包包含自由(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于如下二者的结合:要执行的代码块(因为自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(做用域) -- 百度百科github

《javaScript权威指南》中的概念:

函数对象能够经过做用域链互相关联起来,函数体内部的变量均可以保存在函数做用域内,这种特性在计算机科学中成为闭包面试

《javaScript高级教程》中概念:

闭包是指有权访问另外一个函数做用域中的变量的函数。segmentfault

MDN中的概念

闭包-MDN概念

我的总结的闭包概念:

  1. 闭包就是子函数能够有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。归根结底,就是利用js得词法(静态)做用域,即做用域链在函数建立的时候就肯定了。
  2. 子函数若是不被销毁,整条做用域链上的变量仍然保存在内存中。

为何引入闭包的概念

我引入《深刻理解JavaScript系列:闭包(Closures)》文章中的例子来讲明,也能够直接去看那篇文章,我结合其余书籍反复读了不少遍此文章才理解清楚。以下:浏览器

function testFn() {

  var localVar = 10;  // 自由变量

  function innerFn(innerParam) {
    alert(innerParam + localVar);
  }

  return innerFn;
}

var someFn = testFn();
someFn(20); // 30

通常来讲,在函数执行完毕以后,局部变量对象即被销毁,因此innerFn是不可能以返回值形式返回的,innerFn函数做为局部变量应该被销毁才对。闭包

这是当函数以返回值时的问题,另外再看一个当函数以参数形式使用时的问题,仍是直接引用《深刻理解JavaScript系列》中的例子,也方便你们有兴趣能够直接去阅读那篇文章函数

var z = 10;

function foo() {
  alert(z);
}

foo(); // 10 – 使用静态和动态做用域的时候

(function () {

  var z = 20;
  foo(); // 10 – 使用静态做用域, 20 – 使用动态做用域

})();

// 将foo做为参数的时候是同样的
(function (funArg) {

  var z = 30;
  funArg(); // 10 – 静态做用域, 30 – 动态做用域

})(foo);

当函数foo在不一样的函数中调用,z该取哪一个上下文中的值呢,这就又是一个问题,因此就引入了闭包的概念,也能够理解为定义了一种规则。学习

理解闭包

函数以返回值返回

看一个《javsScript权威指南》中的一个例子,我稍微作一下修改以下:

var scope = 'global scope';
function checkScope() {
    var scope = 'local scope';
    return function() {
        console.log(scope);
    }
}

var result = checkScope(); 
result();   // local scope checkScope变量对象中的scope,非全局变量scope

分析:

即便匿名函数是在checkScope函数外调用,也没有使用全局变量scope,便是利用了js的静态做用域,当匿名函数初始化时,就建立了本身的做用域链(做用域链的概念这里不作解释,能够参考个人另外一篇文章js中的执行栈、执行环境(上下文)、做用域、做用域链、活动对象、变量对象的概念总结,其实当把做用域链理解好了以后,闭包也就理解了), 此匿名函数的做用域链包括checkScope的活动对象和全局变量对象, 当checkScope函数执行完毕后,checkScope的活动对象并不会被销毁,由于匿名函数的做用域链还在引用checkScope的活动对象,也就是checkScope的执行环境被销毁,可是其活动对象没有被销毁,留存在堆内存中,直到匿名函数销毁后,checkScope的活动对象才会销毁,解除对匿名函数的引用将其设置为null便可,垃圾回收将会将其清除,另外当外部对checkScope的自由变量存在引用的时候,其活动对象也不会被销毁

result = null; //解除对匿名函数的引用

注释:

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

补充:
引用一下《javsScript权威指南》中的补充,帮助你们进一步理解
闭包JavaScript权威指南概念

函数以参数形式使用

当函数以参数形式使用时通常用于利用闭包特性解决实际问题,好比浏览器中内置的方法等,下面我直接引用《深刻理解JavaScript系列:闭包(Closures)》中关于闭包实战部分的例子以下:

sort

在sort的内置方法中,函数以参数形式传入回调函数,在sort的实现中调用:

[1, 2, 3].sort(function (a, b) {
  ... // 排序条件
});

map

和sort的实现同样

[1, 2, 3].map(function (element) {
  return element * 2;
}); // [2, 4, 6]

另外利用自执行匿名函数建立的闭包

var foo = {};

// 初始化
(function (object) {

  var x = 10;

  object.getX = function() {
    return x;
  };

})(foo);

alert(foo.getX()); // 得到闭包 "x" – 10

利用闭包实现私有属性的存取

先来看一个例子

var fnBox = [];
function foo() {
    for(var i = 0; i < 3; i++) {
        fnBox[i] = function() {
            return i;
        }
    }
}

foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); //  3
console.log(fn1()); //  3
console.log(fn2()); //  3

用伪代码来讲明以下:

fn0.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3]
}

fn1.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3]
}

fn2.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3],
}

分析:

这是由于fn0、fn一、fn2的做用域链共享foo的活动对象, 并且js没有块级做用域,当函数foo执行完毕的时候foo的活动对象中i的值已经变为3,当fn0、fn一、fn2执行的时候,其最顶层的做用域没有i变量,就沿着做用域链查找foo的活动对象中的i,因此i都为3。

可是这种结果每每不是咱们想要的,这时就能够利用认为建立一个闭包来解决这个问题,以下:

var fnBox = [];
function foo() {
    for(var i = 0; i < 3; i++) {
        fnBox[i] = (function(num) {
            return function() {
                return num;
            }
        })(i);
    }
}
foo();
var fn0 = fnBox[0];
var fn1 = fnBox[1];
var fn2 = fnBox[2];
console.log(fn0()); //  0
console.log(fn1()); //  1
console.log(fn2()); //  2

用伪代码来讲明以下:

fn0.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3],
    fn0自己的活动对象AO: {num: 0} 
}

fn1.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3],
    fn1自己的活动对象AO: {num: 1} 
}

fn2.[[scope]]= {
    // 其余变量对象,一直到全局变量对象
    父级上下文中的活动对象AO: [data: [...], i: 3],
    fn2自己的活动对象AO: {num: 2} 
}

分析:

当使用自执行匿名函数建立闭包, 传入i的值赋值给num,因为做用域链是在函数初始化时建立的,因此当每次循环时,函数fn十、fn一、fn2的做用域链中保存了当次循环是num的值, 当fn十、fn一、fn2调用时,是按照自己的做用域链进行查找。

闭包引发的内存泄漏

闭包-内存泄漏

总结

从理论的角度将,因为js做用域链的特性,js中全部函数都是闭包,可是从应用的角度来讲,只有当函数以返回值返回、或者当函数以参数形式使用、或者当函数中自由变量在函数外被引用时,才能成为明确意义上的闭包。

最后,我想表达的式,本篇大量引用和罗列了经典的犀牛书《javaScript权威指南》、红宝书《javaScript高级教程》、以及《深刻理解JavaScript系列:闭包(Closures)》系列文章中的概念和例子,不为能造成本身的独特看法,只为了能把闭包清晰的讲解出来。笔者是个小菜鸟,能力实在有限,也在学习中,但愿你们多多指点,如发现错误,请多多指正。也但愿看过此文的朋友能对闭包多一些理解,那我写这篇文章也就值得了。下次面试时就能够告诉面试官什么是闭包了。谢谢。

相关文章
相关标签/搜索