全面理解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实现无刷新更新数据的详细步骤 asp
Dec 26 Javascript
JavaScript 字符串处理函数使用小结
Dec 02 Javascript
关于JavaScript中原型继承中的一点思考
Jul 25 Javascript
JavaScript验证图片类型(扩展名)的函数分享
May 05 Javascript
浅谈js和css内联外联注意事项
Jun 30 Javascript
jQuery组件easyui对话框实现代码
Aug 25 Javascript
jQuery基于事件控制实现点击显示内容下拉效果
Mar 07 Javascript
Three.js利用Detector.js插件如何实现兼容性检测详解
Sep 26 Javascript
JS动态添加元素及绑定事件造成程序重复执行解决
Dec 07 Javascript
重学 JS:为啥 await 不能用在 forEach 中详解
Apr 15 Javascript
解决layUI的页面显示不全的问题
Sep 20 Javascript
vue自定义switch开关组件,实现样式可自行更改
Nov 01 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+FLASH实现上传文件进度条相关文件 下载
2007/07/21 PHP
PHP跳转页面的几种实现方法详解
2013/06/08 PHP
PHP实现图片压缩的两则实例
2014/07/19 PHP
php跨服务器访问方法小结
2015/05/12 PHP
PHP getNamespaces()函数讲解
2019/02/03 PHP
脚本吧 - 幻宇工作室用到js,超强推荐expand.js
2006/12/23 Javascript
jQuery $.data()方法使用注意细节
2012/12/31 Javascript
使用变量动态设置js的属性名
2014/10/19 Javascript
Javascript模拟加速运动与减速运动代码分享
2014/12/11 Javascript
jQuery过滤选择器详解
2015/01/13 Javascript
js行号显示的文本框实现效果(兼容多种浏览器 )
2015/10/23 Javascript
JS函数的定义与调用方法推荐
2016/05/12 Javascript
React.js中常用的ES6写法总结(推荐)
2017/05/09 Javascript
Angular2 父子组件数据通信实例
2017/06/22 Javascript
简单谈谈React中的路由系统
2017/07/25 Javascript
JScript实现表格的简单操作
2017/08/15 Javascript
JS实现的简单下拉框联动功能示例
2018/05/11 Javascript
JS实现的贪吃蛇游戏完整实例
2019/01/18 Javascript
angular6根据environments配置文件更改开发所需要的环境的方法
2019/03/06 Javascript
webpack3升级到webpack4遇到问题总结
2019/09/30 Javascript
JavaScript享元模式原理与用法实例详解
2020/03/09 Javascript
初学python数组的处理代码
2011/01/04 Python
Python实现Const详解
2015/01/27 Python
Win7下Python与Tensorflow-CPU版开发环境的安装与配置过程
2018/01/04 Python
使用pandas批量处理矢量化字符串的实例讲解
2018/07/10 Python
python梯度下降法的简单示例
2018/08/31 Python
python构建基础的爬虫教学
2018/12/23 Python
Python玩转Excel的读写改实例
2019/02/22 Python
Python 实现训练集、测试集随机划分
2020/01/08 Python
python数据爬下来保存的位置
2020/02/17 Python
带你学习Python如何实现回归树模型
2020/07/16 Python
英国婴儿和儿童服装网站:Vertbaudet
2018/04/02 全球购物
美国在线面料商店:Fashion Fabrics Club
2020/01/31 全球购物
办公室综合文员岗位职责范本
2014/02/13 职场文书
2014最新党员批评与自我批评材料
2014/09/24 职场文书
如何使用pdb进行Python调试
2021/06/30 Python