深入理解javascript闭包

网上关于闭包的文章很多,各种专业文献上的“闭包”定义非常抽象,很难看懂。官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式,还是看不懂?
不着急,引用《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.

个人翻译为:闭包指的是一个函数和该函数能访问和引用的所有变量对象(也叫作用域对象)的集合
我觉的也可以这么理解:一个闭包就是当一个函数被其包含作用域外部引用时,一个没有释放资源的栈区,栈区包含该函数本身和函数所引用的作用域链上的变量对象。
所以,在Javascript中,从不同角度看有两种形式:
1)从理论角度看,所有的函数都是闭包,包括全局函数,因为他们都有自己的变量对象和作用域链,访问全局变量就相当于访问最外层的作用域对象
2)从实践角度看,闭包函数一般有以下特点的:创建它们的上下文(比如父函数执行环境)已经销毁,它们仍然存在并被引用,而且还引用了其作用域链里其他作用域对象的变量。
最简单的就像这样:

function F_func() {
  var num = 42;
  var S_func = functio() {
    return ++num;
  }
  return S_func;
}
var add = F_func();

当一个或多个内部函数被包含函数返回,而内部函数引用了其他作用域的变量,当内部函数在包含函数之外被调用,我们就可以称它们构成了闭包。

一、闭包的原理

1、外部函数在调用时会创建自身的执行环境、变量对象和作用域链,调用结束后这些回收,但是函数的变量对象若有变量被闭包函数调用,则基于Js的垃圾回收机制,函数调用结束后其执行环境、作用域链将销毁,变量对象却不会立即回收,而是在闭包函数的调用链里;
2、每调用一次外部函数就会创建自身的执行环境、变量对象等操作,便产生一个新的闭包,以前的闭包依旧存在且互不影响;
3、同一个闭包会保留上一次的状态,当它被再次调用时会在同一作用域链上进行;
还是用上面的例子说明

var name = 'scope';
function F_func() {
  var num = 42;
  function S_func() {
    var t = 1;
    num += t;
    return num;
  }
  return S_func;
}
var a = F_func();
a(); // 43
a(); // 44
var b = F_func();  // 重新调用F_func(),产生新的闭包
b(); // 43  
a = null;   // 通知回收
b(); // 44 

大概的执行过程(其中作用链以指针关联,vo-指代变量对象):

  • 作用域链的各个变量对象以指针关联
  • vo-指代变量对象

1)进入全局上下文,创建全局执行环境、全局变量对象和作用域链,入栈

   //对应作用域链
   global ---> global-vo {
                 name: 'scope',
                 F_func: Function,
                 a: undefined,
                 b: undefined
               }

2)第一次执行F_func函数,创建F_func执行环境、变量对象和作用域链,入栈
3)执行F_func函数,初始化上下文、变量对象和内部函数的作用域链

   //对应作用域链
   F_func ----- F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                  |                  b: undefined
                  |               }
                  |          
   S_func ----- S_func-vo { 
                   t: 1      
                } 

4)F_func函数执行完毕,变量a引用返回的内部函数S_func,F_func的执行环境和作用域链出栈并销毁

   //对应作用域链
                F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                   |                 b: undefined
                   |               }
                   |             
   a=S_func ----- S_func-vo { 
                   t: 1 
                  }

5)再次调用a函数,后续每次调用都在这个作用域链里进行,将会继承变量对象里变量的变化
6)第二次执行F_func函数,重新创建F_func执行环境...重复上述过程,b也将获得一个自己的执行环境、变量对象和作用域链,同a是一样的

   //对应作用域链
                F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                   |                 b: undefined
                   |               }
                   |             
   b=S_func ----- S_func-vo { 
                   t: 1 
                  }

7)最后设置a为null,解除了a和内部函数的引用,垃圾回收机制下一次检查时将回收存储并清除内部函数的作用域链和变量对象
8)b不受影响,依然可以调用
4、当外部函数中存在多个内部函数时,这些函数构成闭包引用的同一份父函数的变量对象,适合构造对象;
举个例子:

function F_func() {
  var num = 42;
  return {
      fun1: function() { console.log(num); },
      fun2: function() { num++; },
      fun3: function() { num--; } 
  };
}
var obj = F_func();
obj.fun2();
obj.fun2();
obj.fun3();
obj.fun1(); // 43

3个匿名内部函数引用的都是F_func执行完生产的同一份变量对象

二、闭包的用途

闭包可以用在许多地方,主要有:

  • 读取函数内部的变量
  • 让这些变量的值始终保持在内存中。
  • 模拟块级作用域变量,构造变量临时性死区,同时也能创建私有作用域,避免向全局作用域添加过多变量,减少与其他开发者产生命名冲突
    示例:
(function() {
    var num= 10;
    var add = function(n) {return n + num; }
    ...
})();
  • Js里变量是函数级作用域,函数便是变量的私有作用域,函数外不能直接访问,只能通过调用域链,可以封装私有属性和私有方法,并开发共有接口,一般用与面向对象的构造
    示例:
function Person(name) {
    var age = 18;
    getAge = function() {
        return age;
    };
    this.getName = function() {
        return name + getAge;
    };
}
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18

三、注意事项

1)由闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题和内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部设置为null
2)闭包增加的变量查找的作用域链的长度,会在一定程度上影响查找速度

四、实例分析

(1)修改前

function buildArr(arr) {
   var result = [];
   for (var i = 0; i < arr.length; i++) {
     var item = 'item' + i;
     result.push(function() {console.log(item + ' ' + arr[i])});
   }
   return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0]();  //  item2 undefined
fnlist[1]();  //  item2 undefined
fnlist[2]();  //  item2 undefined

外部函数buildArr执行完后几个fnlist匿名函数的作用域链为:

 fnlist[n] {  -----  buildArr-vo {           -----   global-vo {
 }                       result:[Function],              buildArr: Function,
                         i: 3,                           fnlist: undefined
                         item: item2                 }
                         arr: [1,2,3]
                      } 

(2)修改后

function buildArr(arr) {
   var result = [];
   for (var i = 0; i < arr.length; i++) {
      result.push((function(n) {
          var item = 'item' + n;
          return function() {
            console.log(item + ' ' + arr[n]);
         }
     })(i));
  }
  return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0]();  //  item0 1
fnlist[1]();  //  item1 2
fnlist[2]();  //  item2 3

外部函数buildArr执行完后几个fnlist匿名函数的作用域链为:

fnlist[0] {  -----  匿名函数-vo {  -----  buildArr-vo {          ----- global-vo {
}                       n: 0                result: [Function],          buildArr: Function,
                        item: item0         i: 3,                        fnlist: undefined
                     }                      arr: [1,2,3]              }
                                          } 
                                              |
fnlist[1] {  -----  匿名函数-vo { -------------- 
}                        n: 1                 |
                         item: item1          |
                     }                        |
fnlist[2] {  -----  匿名函数-vo {  ------------
}                       n: 2                
                        item: item2         
                     }                          

五、实现闭包的方法

闭包主要依靠外部和内部函数来构造,而函数可以又有多种使用方式,比如:一般函数、对象方法等,所以可以很多实现闭包的方法,只要满足闭包的特点即可。
(1)作为普通函数使用

1.1 最简单闭包:
var myFunc = (function() {
    var num = 10;
    return function() {
      return num++;
    };
})();

1.2 返回对象或数组:也可叫模块模式或增强单例对象
var myArr = (function() {
    var num = 10;
    function add() {
      return num++;
    };
    return [add];
})();
var myObj = (function() {
    var num = 10;
    function add() {
      return num++;
    };
    return {
        add1: add,
        getNum: function() {return num;} // 公共接口
    };
})();

1.3 多级作用域链
var myObj = (function() {
    var num = 10;
    var arr = [];
    for (var i = 0;i < num;i++) {
        arr.push((function(n) {
            return function() { return n + 1;};
        })(i));
    }
    return {
        funs: arr
    };
})();

1.4 闭包函数以其他方式被引用
var $dom = document.querySelector("#id")
function() {
    var num = 10;
    $dom.onclick = function() {
        console.log(num);
    };
}
function() {
    var num = 10;
    setInterval(function() {
      console.log(num);
    },2000);
}
function() {
    var num = 10;
    clickMe = function() { 
        console.log(num);
    };
}

(2)作为对象属性方法使用

var circle = {  
   "PI":3.14159,  
   "area": function(r) {  
                 return this.PI * r * r;  
            }                
};  
console.log(circle.area(2));

(3)作为构造函数和面向对象

广泛用于面向对象编程方式中

3.1 实例方法
function Person(name) {
    var age = 18;
    getAge = function() {
        return age;
    };
    this.getName = function() {
        return name + getAge;
    };
}
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18

3.2 静态方法和变量,静态共享
(function() {
    var name = 'lilei';
    var age = 18;
    Person = function(name) {
        name = name;
    };
    Person.prototype.getName = function() {
       return name + age;
    };
})();
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18
var lilei = new Person('Lilei');
lucy.getName();   // Lilei18
lilei.getName();  // Lilei18

3.3 原始对象
var Circle = new Object();  
Circle.PI = 3.14159;  
Circle.Area = function( r ) {  
       return this.PI * r * r;  
}  
alert( Circle.Area( 1.0 ) );

3.3 工厂模式生产对象
var Circle = function() {  
   var obj = new Object();  
   obj.PI = 3.14159;  

   obj.area = function( r ) {  
       return this.PI * r * r;  
   }  
   return obj;  
}          
var c = new Circle();  
alert( c.area( 1.0 ) );

3.3 原型模式生产对象
function Circle(r) {  
      this.r = r;  
}  
Circle.PI = 3.14159;  
Circle.prototype.area = function() {  
  return Circle.PI * this.r * this.r;  
}  
var c = new Circle(1.0);     
alert(c.area());

其他...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容