全面理解JavaScript中的闭包


Posted in Javascript onMay 12, 2016

引子

闭包是有权访问另一个函数作用域中的变量的函数。
闭包是javascript中很难理解的部分,很多高级的应用都依靠闭包来实现的,我们先来看下面的一个例子:

function outer() {
  var i = 100;
  function inner() {
    console.log(i);
  }
}

上面代码,根据变量的作用域,函数outer中所有的局部变量,对函数inner都是可见的;函数inner中的局部变量,在函数inner外是不可见的,所以在函数inner外是无法读取函数inner的局部变量的。

既然函数inner可以读取函数outer的局部变量,那么只要将inner作为返会值,就可以直接在ouer外部读取inner的局部变量。

function outer() {
  var i = 100;
  function inner() {
     console.log(i);
  }
  return inner;
}
var rs = outer();
rs();

这个函数有两个特点:

  • 函数inner嵌套在函数ouer内部;
  • 函数outer返回函数inner。

这样执行完var rs = outer()后,实际rs指向了函数inner。这段代码其实就是一个闭包。也就是说当函数outer内的函数inner被函数outer外的一个变量引用的时候,就创建了一个闭包。

作用域
简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。

全局作用域

var num1 = 1;
function fun1 (){
  num2 = 2;
}

以上三个对象num1,num2和fun1均是全局作用域,这里要注意的是末定义直接赋值的变量自动声明为拥有全局作用域;

局部作用域

function wrap(){
  var obj = "我被wrap包裹起来了,wrap外部无法直接访问到我";
  function innerFun(){
    //外部无法访问我
  }
}

作用域链
Javascript中一切皆对象,这些对象有一个[[Scope]]属性,该属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链(Scope Chain),它决定了哪些数据能被函数访问。

function add(a,b){
  return a+b;
}

当函数创建的时候,它的[[scope]]属性自动添加好全局作用域
全面理解JavaScript中的闭包

var sum = add(3,4);

当函数调用的时候,会创建一个称为运行期上下文(execution context)的内部对象,z这个对象定义了函数执行时的环境。它也有自己的作用域链,用于标识符解析,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

全面理解JavaScript中的闭包

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象(最后一个为全局对象)都未找到,则认为该标识符未定义。

闭包
闭包简单来说就是一个函数访问了它的外部变量。

var quo = function(status){
  return {
    getStatus: function(){
      return status;
    }
  }
}

status保存在quo中,它返回了一个对象,这个对象里的方法getStatus引用了这个status变量,即getStatus函数访问它的外部变量status;

var newValue = quo('string');//返回了一个匿名对象,被newValue引用着
newValue.getStatus();//访问到了quo的内部变量status

假如并没有getStatus这个方法,那么quo('sting')结束后,status自动被回收,正是因为返回的匿名对象被一个全局对象引用,那么这个匿名对象又依赖于status,所以会阻止status的释放。

例子:

//错误方案
var test = function(nodes){
  var i ;
  for(i = 0;i<nodes.length;i++){
    nodes[i].onclick = function(e){
      alert(i);
    }
  }
}

匿名函数创建了一个闭包,那么其访问的i是外部test函数中的i,所以每一个节点实际上引用的是同一个i。

全面理解JavaScript中的闭包

//改进方案
var test = function(nodes){
  var i ;
  for(i = 0;i<nodes.length;i++){
    nodes[i].onclick = function(i){
      return function(){
        alert(i);
      };
    }(i);
  }
}

每一个节点绑定了一个事件,这个事件接收一个参数,并且立即运行,传入i,因为是按值传递的,所以每一次循环都会为当前i产生一个新的备份。

全面理解JavaScript中的闭包

闭包的作用

function outer() {
  var i = 100;
  function inner() {
     console.log(i++);
  }
  return inner;
}
var rs = outer();
rs();  //100
rs();  //101
rs();  //102

上面的代码中,rs是闭包inner函数。rs共运行了三次,第一次100,第二次101,第三次102,这说明在函数outer中的局部变量i一直保存在内存中,并没有在调用自动清除。

闭包的作用就是在outer执行完毕并返回后,闭包使javascript的垃圾回收机制(grabage collection)不会回收outer所占的内存,因为outer的内部函数inner的执行要依赖outer中的变量。(另一种解释:outer是inner的父函数,inner被赋给了一个全局变量,导致inner会一直在内存中,而inner的存在依赖于outer,因些outer也始终于在内存中,不会在调用结束后被垃圾收集回收)。

闭包有权访问函数内部的所有变量。
当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

闭包与变量

由于作用域链的机制,闭包只能取得包含函数中任何变量的最后一个值。看下面例子:

function f() {
  var rs = [];

  for (var i=0; i <10; i++) {
    rs[i] = function() {
      return i;
    };
  }

  return rs;
}

var fn = f();

for (var i = 0; i < fn.length; i++) {
  console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}

函数会返回一个数组,表面上看,似乎每个函数都应该返回自己的索引值,实际上,每个函数都返回10,这是因为第个函数的作用域链上都保存着函数f的活动对象,它们引用的都是同一变量i。当函数f返回后,变量i的值为10,此时每个函数都保存着变量i的同一个变量对象。我们可以通过创建另一个匿名函数来强制让闭包的行为符合预期。

function f() {
  var rs = [];

  for (var i=0; i <10; i++) {
    rs[i] = function(num) {
      return function() {
        return num;
      };
    }(i);
  }

  return rs;
}

var fn = f();

for (var i = 0; i < fn.length; i++) {
  console.log('函数fn[' + i + ']()返回值:' + fn[i]());
}

这个版本中,我们没有直接将闭包赋值给数组,而是定义了一个匿名函数,并将立即执行匿名函数的结果赋值给数组。这里匿名函数有一个参数num,在调用每个函数时,我们传入变量i,由于参数是按值传递的,所以就会将变量i复制给参数num。而在这个匿名函数内部,又创建了并返回了一个访问num的闭包,这样,rs数组中每个函数都有自己num变量的一个副本,因此就可以返回不同的数值了。

闭包中的this对象

var name = 'Jack';

var o = {
  name : 'bingdian',

  getName : function() {
    return function() {
      return this.name;
    };
  }
}

console.log(o.getName()());   //Jack
var name = 'Jack';

var o = {
  name : 'bingdian',

  getName : function() {
    var self = this;
    return function() {
      return self.name;
    };
  }
}

console.log(o.getName()());   //bingdian

内存泄露

function assignHandler() {
  var el = document.getElementById('demo');
  el.onclick = function() {
    console.log(el.id);
  }
}
assignHandler();

以上代码创建了作为el元素事件处理程序的闭包,而这个闭包又创建了一个循环引用,只要匿名函数存在,el的引用数至少为1,因些它所占用的内存就永完不会被回收。

function assignHandler() {
  var el = document.getElementById('demo');
  var id = el.id;

  el.onclick = function() {
    console.log(id);
  }

  el = null;
}
assignHandler();

把变量el设置null能够解除DOM对象的引用,确保正常回收其占用内存。

模仿块级作用域

任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。

(function(){
  //块级作用域
})();

闭包的应用

保护函数内的变量安全。如前面的例子,函数outer中i只有函数inner才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
在内存中维持一个变量。如前面的例子,由于闭包,函数outer中i的一直存在于内存中,因此每次执行rs(),都会给i加1。

Javascript 相关文章推荐
JavaScript 设计模式 富有表现力的Javascript(一)
May 26 Javascript
extjs表格文本启用选择复制功能具体实现
Oct 11 Javascript
JavaScript代码简单实现求杨辉三角给定行的最大值
Oct 29 Javascript
node.js中的fs.unlinkSync方法使用说明
Dec 15 Javascript
JavaScript动态创建link标签到head里的方法
Dec 22 Javascript
javascript批量修改文件编码格式的方法
Jan 27 Javascript
js生成随机数(指定范围)的实例代码
Jul 10 Javascript
JS获取当前使用的浏览器名字以及版本号实现方法
Aug 19 Javascript
jQuery实现点击后高亮背景固定显示的菜单效果【附demo源码下载】
Sep 21 Javascript
js实现可以点击收缩或张开的悬浮窗
Sep 18 Javascript
浅谈es6 javascript的map数据结构
Dec 14 Javascript
通过fastclick源码分析彻底解决tap“点透”
Dec 24 Javascript
Bootstrap框架动态生成Web页面文章内目录的方法
May 12 #Javascript
Node.js的项目构建工具Grunt的安装与配置教程
May 12 #Javascript
js自定义select下拉框美化特效
May 12 #Javascript
使用jQuery制作遮罩层弹出效果的极简实例分享
May 12 #Javascript
JS函数的定义与调用方法推荐
May 12 #Javascript
使用jQuery实现Web页面换肤功能的要点解析
May 12 #Javascript
JS定义类的六种方式详解
May 12 #Javascript
You might like
一个php导出oracle库的php代码
2009/04/20 PHP
Zend Framework中的简单工厂模式 图文
2012/07/10 PHP
php微信公众号开发之欢迎老朋友
2018/10/20 PHP
Yii框架使用PHPExcel导出Excel文件的方法分析【改进版】
2019/07/24 PHP
Js+Dhtml:WEB程序员简易开发工具包(预先体验版)
2006/11/07 Javascript
使用onbeforeunload属性后的副作用
2007/03/08 Javascript
(function(){})()的用法与优点
2007/03/11 Javascript
jquery控制display属性为none或block
2014/03/31 Javascript
Node.js开发之访问Redis数据库教程
2015/01/14 Javascript
JQuery中$.each 和$(selector).each()的区别详解
2015/03/13 Javascript
JS采用绝对定位实现回到顶部效果完整实例
2016/06/20 Javascript
jQuery实现导航回弹效果
2017/02/27 Javascript
AngularJs定时器$interval 和 $timeout详解
2017/05/25 Javascript
ES6扩展运算符的用途实例详解
2017/08/20 Javascript
p5.js 毕达哥拉斯树的实现代码
2018/03/23 Javascript
微信小程序中使用ECharts 异步加载数据实现图表功能
2018/07/13 Javascript
详解关于微信setData回调函数中的坑
2019/02/18 Javascript
在VUE中实现文件下载并判断状态的方法
2019/11/08 Javascript
vue 根据选择条件显示指定参数的例子
2019/11/09 Javascript
用js限制网页只在微信浏览器中打开(或者只能手机端访问)
2020/12/24 Javascript
python getopt 参数处理小示例
2009/06/09 Python
400多行Python代码实现了一个FTP服务器
2012/05/10 Python
Python中urllib2模块的8个使用细节分享
2015/01/01 Python
Python编写一个优美的下载器
2018/04/15 Python
pandas表连接 索引上的合并方法
2018/06/08 Python
对numpy中二进制格式的数据存储与读取方法详解
2018/11/01 Python
Python递归求出列表(包括列表中的子列表)的最大值实例
2020/02/27 Python
python性能测试工具locust的使用
2020/12/28 Python
日本一家专门经营各种箱包的大型网站:Traveler Store
2016/08/03 全球购物
阿玛尼化妆品美国官网:Giorgio Armani Beauty
2017/02/02 全球购物
优秀党支部书记事迹材料
2014/05/29 职场文书
乡镇镇长个人整改措施
2014/10/01 职场文书
2014年基层党建工作总结
2014/11/11 职场文书
个人委托书范文
2015/01/28 职场文书
工程质量保证书
2015/05/09 职场文书
Ajax是什么?Ajax高级用法之Axios技术
2021/04/21 Javascript