1. 简介

闭包:

  • 函数执行时形成一个私有作用域,保护里面的私有变量不受外界干扰,这种保护机制称之为闭包

  • 市面上开发者认为的闭包是:函数执行形成一个不销毁的私有作用域(私有栈内存),不仅可以保护私有变量不被外界干扰,还可以保存一些内容,供后期调取使用,才是闭包

高级程序设计:
闭包是指有权访问另一个函数作用域中的变量的函数,只有函数内部的子函数才能读取函数中的私有变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

销毁和不销毁主要看它还有没有作用,内部的函数创建的作用域执行完毕后销毁,但是在外部函数的作用域不会销毁,因为存在私有变量被其内部的函数引用,所以不会销毁。

创建闭包的常见方式,就是在一个函数内部创建另一个函数。

  1. // 形式1,函数中返回函数
  2. /* 柯里化函数 */
  3. function fn() {
  4. var a = 12;
  5. return function() {
  6. console.log(a);
  7. }
  8. }
  9. var f = fn();
  10. // 形式2,高级单例模式
  11. /* 惰性函数 */
  12. var fn = (function() {
  13. var a = 12;
  14. function fn() {
  15. console.log(a);
  16. }
  17. return {
  18. fn: fn
  19. }
  20. })();
  21. // 形式3,函数中创建函数
  22. function fn() {
  23. var element = document.getElementById('box');
  24. element.onclick = function() {
  25. //=> 可以访问外部的 element
  26. console.log(element.id);
  27. }
  28. }//=> 这里的 element 永远不会销毁,除非手动销毁

在函数的内部返回的匿名函数,访问了外部函数的变量。即使这个函数被返回,而且在其他地方调用,它仍然可以访问该变量。这是因为内部函数的作用域链中包含外部函数的变量对象

解析题:

  1. function fn() {
  2. var i = 5;
  3. return function (n) {
  4. console.log(n*(--i));
  5. }
  6. }
  7. var f = fn();
  8. f(10); //=> 10*4 = 40
  9. fn()(10); //=> 10*4 = 40
  10. fn()(20); //=> 20*4 = 80
  11. f(20); //=> 20 * 3 = 60
  12. 调用 f 时,使用的是同一个作用域的 i,每次调用后,i 都变化
  13. 调用 fn()() 时,每次都是不同的 i,初始值都是 5
  1. var i = 2;
  2. function fn() {
  3. i += 2;
  4. return function (n) {
  5. console.log(n + (--i));
  6. }
  7. }
  8. var f = fn(); //=> 只执行 fn,全局 i+=2,执行后 i = 4
  9. f(2); //=> 此时执行的是 fn 内部的函数,只执行--i,此时 i = 3,结果为 2 + 3 = 5
  10. f(3); //=> 此时执行的是 fn 内部的函数,只执行--i,此时 i = 2,结果为 3 + 2 = 5
  11. fn()(2); //=> 此时执行的是 fn 以及内部函数,执行了 i+=2 和 --i,此时 i = 3,结果为 2 + 3 = 5
  12. fn()(3); //=> 此时执行的是 fn 以及内部函数,执行了 i+=2 和 --i,此时 i = 4,结果为 3 + 4 = 7
  13. f(4); //=> 此时执行的是 fn 内部函数,只执行 --i,执行之后 i = 3,结果为 4 + 3 = 7

2. 函数作用域链

函数的作用域链是指函数执行时,查找变量的机制:
当前作用域的变量对象 -> 上一级作用域的变量对象 -> … -> 全局作用域的变量对象
这就是作用域链。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象

在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。

函数的作用域链的形成机制:

  • 定义函数时,创建一个包含上一级作用域变量对象至全局变量对象的作用域链,保存在内部的 [[Scope]] 属性中

  • 调用函数时,为函数创建一个执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建起执行环境的作用域链

  • 再创建一个函数本身的变量对象并推入到执行环境作用域链的前端

也就是说,函数的作用域链的创建,是跟函数定义的位置有关,而跟函数执行的位置无关

  1. var a = 12;
  2. function fn() {
  3. //=> arguments:实参集合
  4. //=> arguments.callee:函数本身
  5. //=> arguments.callee.caller:当前函数在哪执行,caller就是谁(记录的是它执行的宿主环境),在全局下执行 caller 的结果是 null
  6. console.log(a);
  7. }
  8. function sum() {
  9. var a = 120;
  10. fn(); //=> 12,其作用域链只与其定义的位置有关,其上一级作用域是全局作用域
  11. }
  12. sum(); //=> 12

3. this 对象

在闭包中使用 this 对象也可能导致问题。this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window

  1. var name = "The Window";
  2. var object = {
  3. name: "My Object",
  4. getNameFunc: function() {
  5. return function() {
  6. return this.name;
  7. }
  8. }
  9. }
  10. // 由于 getNameFunc() 返回一个函数,所以调用 getNameFunc()() 会立即调用它返回的函数。
  11. alert(object.getNameFunc()()); //"The Window"

每个函数在被调用时都会自动取得两个特殊变量:thisarguments内部函数在搜索这两个变量时,只会搜索到其变量对象为止。所以永远不可能直接访问外部函数中的这两个变量。

可以把外部作用域中的 this 对象保存在一个闭包能够访问到的变量中,就可以让闭包访问该对象了。

  1. var name = "The Window";
  2. var object = {
  3. name: "My Object",
  4. getNameFunc: function() {
  5. var that = this;
  6. return function() {
  7. return that.name;
  8. }
  9. }
  10. }
  11. alert(object.getNameFunc()()); //"My Object"

在几种特殊情况下,this 的值可能意外发生改变。

  1. var name = "The Window";
  2. var object = {
  3. name: "My Object",
  4. getName: function() {
  5. return this.name;
  6. }
  7. }
  8. object.getName(); //"My Object"
  9. (object.getName)(); //"My Object"
  10. (object.getName = object.getName)(); //"The Window"

第一种调用是正常调用,一般不会使用下面两种方式,这里只是用于说明;
第二种调用,就好像只是在引用一个函数,但 this 的值得到了维持;
第三种调用,先执行了一条赋值语句,然后再调用赋值后的结果,因为这个赋值表达式的值是函数本身,所以 this 的值不能维持。

4. 闭包的内存泄漏

全局作用域在页面关闭的时候销毁,函数作用域一般在代码执行完毕之后销毁。

但是闭包的情况又有所不同。外部函数的私有作用域(栈内存)被内部的函数占用着,所以外部函数在执行完毕后也不会销毁其私有作用域

  1. function createComparisonFunction(propertyName) {
  2. return function(object1, object2) {
  3. var value1 = object1[propertyName];
  4. var value2 = object2[propertyName];
  5. if (value1 < value2) {
  6. return -1;
  7. }
  8. else if (value1 > value2) {
  9. return 1;
  10. }
  11. else {
  12. return 0;
  13. }
  14. }
  15. }
  16. var compare = creatComparisonFunction("name");
  17. var result = compare({ name: "Nichoals" }, { name: "Greg" });

手动解除引用

  1. //=> 手动解除引用,以便释放内存
  2. //=> 释放内部函数的引用,那么内部函数会被销毁,那么外部函数执行时的私有作用域就没有被谁占用着,那么也会被销毁
  3. compare = null;

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用可能会导致内存占用过多。使用闭包时,需要注意手动接触引用

5. 闭包的作用

(1)闭包具有保护作用:保护私有变量不受外界干扰
在真实项目中,尤其是团队协作开发的时候,应当尽可能的减少全局变量的使用,以防止相互之间的冲突(全局变量污染),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量

  1. //=> 形成块级作用域
  2. (function(){
  3. //=> 把你的代码全部复制进来
  4. })();

不仅如此,我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用。

  1. jQuery 方式:把需要暴露的方法通过 window 抛到全局
  1. (function(){
  2. function jQuery() {
  3. //...
  4. }
  5. //...
  6. //=> 把需要供外面使用的方法,通过给 window 设置属性的方式暴露出去
  7. window.jQuery = window.$ = jQuery
  8. })();
  9. jQuery();
  10. $();
  1. Zepto 方式:基于 return 一个对象把需要供外面使用的方法暴露出去
  1. //=> 高级单例模式
  2. var Zepto = (function() {
  3. //...
  4. return {
  5. xxx: function () {
  6. }
  7. };
  8. })();

(2)闭包具有保存作用:形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用

  1. 在函数外部永远不可能直接访问到函数内的 this 对象
  1. function () {
  2. var that = this;
  3. return function() {
  4. that //=> 获得上一级函数的 this 对象
  5. }
  6. }
  1. for 循环中
  1. for (var i = 0; i < tabList.length; i++) {
  2. tabList[i].onclick = function () {
  3. changTab(i);
  4. //=> 执行方法,形成一个私有栈内存,遇到变量,不是私有变量,向上一级作用域查找(上级作用域 window)
  5. //=> 当我们点击的时候,外层循环已经结束(能点击的时候页面已经加载完成,页面加载完成预示着 JS 代码都已经执行完成,也就是循环也都执行完成),外层循环结束已经让全局的 i = li 的总长度
  6. //=> 所有时间绑定都是异步编程(同步编程:一件事一件事的做,当前这件事没完成,下一个任务不能处理。异步编程:当前这件事没有彻底完成,不再等待,继续执行下面的任务),绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束(让全局的 i 等于循环最后的结果)
  7. }
  8. }
  9. //=> 解决方案
  10. //=> 1. 提前把 i 保存在自定义属性,点击时,使用这个自定义属性,而不使用 i
  11. //=> 2. 闭包,把它的上一级作用域改变
  12. tabList[i].onclick = (function(i) {
  13. return function () {
  14. changeTab(i) //=> 上级作用域:自执行函数形成的作用域
  15. }
  16. })(i);
  17. //=> 循环几次就形成几个不销毁的作用域,而每一个不销毁的栈内存中,都存储了一个私有变量 i,而这个值分别是每一次执行传递进来的全局 i 的值。当点击的时候,执行返回的小函数,遇到变量,向它自己的上级作用域查找,找到前面不销毁作用域的 i 的值,达到我们想要的效果。这就是闭包的保存效果。但是真实项目中不要使用,因为会消耗性能。
  18. //=> 3. 基于 ES6 的 let 创建变量,是存在块级作用域的(类似于私有作用域)
  19. for (let i = 0; i < tabList.length; i++) {
  20. tabList[i].onclick = function () {
  21. changTab(i);
  22. }
  23. }

6. 模仿块级作用域

JavaScript 没有块级作用域的概念(ES6 已经有了),这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。即使在后面重复声明(没有赋值),一样会是以前的值。JavaScript 从来不会告诉你是否多次声明了同一个变量。它会忽略后续的声明,但是会执行声明中变量的初始化。

  1. for (var i = 1; i < 9; i++) {
  2. //...
  3. }
  4. var i;
  5. alert(i); //9

立即执行匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(私有作用域)的匿名函数如下:

  1. (function() {
  2. //这里是块级作用域
  3. })();
  4. // 其他写法
  5. ~function(){
  6. }();
  7. !function(){
  8. }();
  9. void function() {
  10. }();

将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用函数。

这种技术经常用在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,应该尽量减少向全局作用域中添加变量和函数,过多的变量和函数很容易导致命名冲突。
这种做法也可以减少闭包占用内存的问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链。

7. 私有变量

在私有作用域中,只有以下两种情况是私有变量:

  • 声明过的变量(带 var / function)

  • 形参也是私有变量

剩下的都不是私有变量,需要基于作用域链向上查找

  1. /*
  2. * 变量提升:
  3. * var a,var b,var c,function fn = xxx
  4. */
  5. var a = 12,
  6. b = 13,
  7. c = 14;
  8. function fn(a) {
  9. /*
  10. * fn 执行:形成私有作用域
  11. * 形参赋值 a = 12,变量提升 var b
  12. */
  13. console.log(a, b, c); // 12, undefined, 14,c 是全局变量
  14. var b = c = a = 20; //=> 把全局变量 c 改为 20,其他都是私有变量赋值
  15. console.log(a, b, c); //=> 20, 20, 20
  16. }
  17. fn(a);
  18. console.log(a, b, c) //=> 12, 13, 20

360 面试题

  1. var ary = [12,23];
  2. function fn(ary) {
  3. console.log(ary); //=> [12,23],地址与全局一样
  4. ary[0] = 100; //=> 改变私有变量,地址一样,全局也改变
  5. ary = [100]; //=> 改变了私有变量的地址
  6. ary[0] = 0; //=> 改变私有变量,地址不一样与全局无关
  7. console.log(ary); //=> [0]
  8. }
  9. fn(ary);
  10. console.log(ary); //=> [100, 23]

7.1 特权方法

利用闭包可以通过作用域链访问私有变量的特性,可以创建用于访问私有变量的公有方法,我们称有权访问私有变量和私有函数的公有方法为特权方法

在构造函数中定义特权方法

  1. function MyObject() {
  2. //私有变量和私有函数
  3. var privateVarible = 10;
  4. function privateFunction() {
  5. return false;
  6. }
  7. //特权方法
  8. this.publicMethod = function() {
  9. privateVarible++;
  10. return privateFunction();
  11. }
  12. }

在创建 MyObject 的实例后,除了 publicMethod() 方法外,没有任何办法访问内部的私有变量。但是这里每创建一个实例,都重复创建了相同的私有变量和私有函数。

利用私有和特权成员,可以隐藏那些不应该被直接修改的数据

  1. function Person(name) {
  2. this.getName = function() {
  3. return name;
  4. };
  5. this.setName = function(value) {
  6. name = value;
  7. }
  8. }
  9. var person = new Person("Nicholas");
  10. alert(person.getName()); //"Nicholas"
  11. person.setName("Greg");
  12. alert(person.getName()); //"Greg"

每次的私有变量 name 在不同的实例中是不同的,跟前面的方法有所区别。

在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每个实例都会创建同样一组新方法。

7.2 静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。

  1. (function() {
  2. var privateVariable = 10;
  3. function privateFunction() {
  4. return false;
  5. }
  6. //构造函数 没有 var 暴露给全局
  7. MyObject = function() {
  8. };
  9. 另一种方式:暴露给全局
  10. window.MyObject = MyObject;
  11. MyObject.prototype.publicMethod = function() {
  12. privateVariable++;
  13. privateFunction();
  14. };
  15. })();

这里的构造函数使用函数表达式,同样没有用 var 关键字,是为了使其创建为全局函数。注意,这在严格模式下会导致错误。

这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,有保存着对包含作用域的引用。

但是这种方法创建会因为使用原型而增进代码复用,但每个实例都没有属于自己的私有变量,都会共享。

注意:多查找作用域链中的一个层次,就会在一定程度上影响查找速度,这是闭包和私有变量的一个明显的不足之处。

7.3 模块模式

前面的模式是用于自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式则是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象

按照惯例,JavaScript 是以对象字面量来创建单例对象的。

  1. var singleton = {
  2. name: value,
  3. method: function() {
  4. //方法代码
  5. }
  6. };

模块模式通过为单例添加私有变量和特权方法能够使其增强。

  1. var singleton = function() {
  2. var privateVariable = 10;
  3. function privateFunction() {
  4. return false;
  5. }
  6. return {
  7. publicProperty: true,
  8. publicMethod: function() {
  9. privateVariable++;
  10. privateFunction();
  11. }
  12. }
  13. }();

模块模式使用了一个返回对象的立即执行匿名函数。从而本质上讲,对象字面量定义的是单例的公共接口,这种模式在需要对单例进行某些初始化,同时又要维护其私有变量时非常有用。

  1. var application = function() {
  2. //私有变量和函数
  3. var components = new Array();
  4. //初始化
  5. components.push(new BaseComponent());
  6. //公共
  7. return {
  8. getComponentCount: function() {
  9. return components.length;
  10. },
  11. registerComponent: function(component) {
  12. if (typeof component == "object") {
  13. components.push(component);
  14. }
  15. }
  16. };
  17. }();

在 Web 应用程序中,经常需要使用一个单例来管理应用程序级的信息。简而言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

7.4 增强的模块模式

这种增强的模块模式适合那些单例必须是某种类型的实例,同时必须添加某些属性和方法对其加以增强的情况。

这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况。

  1. var singleton = function() {
  2. //私有变量和私有函数
  3. var privateVariable = 10;
  4. function privateFunction() {
  5. return false;
  6. }
  7. //创建对象,这里是 CustomType 的一个实例
  8. var object = new CustomType();
  9. //添加特权属性和方法
  10. object.publicProperty = true;
  11. object.publicMethod = function() {
  12. privateVariable++;
  13. privateFunction();
  14. };
  15. //返回这个对象
  16. return object;
  17. }();