全面理解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 String 对象
Apr 25 Javascript
javascript 遍历验证所有文本框的值
Aug 27 Javascript
js 自定义个性下拉选择框示例
Aug 20 Javascript
JS去除字符串两端空格的简单实例
Dec 27 Javascript
js表单中选择框值的获取及表单的序列化
Dec 17 Javascript
原生JS实现移动端web轮播图详解(结合Tween算法造轮子)
Sep 10 Javascript
import与export在node.js中的使用详解
Sep 28 Javascript
微信小程序中setInterval的使用方法
Sep 29 Javascript
JS设计模式之数据访问对象模式的实例讲解
Sep 30 Javascript
如何用input标签和jquery实现多图片的上传和回显功能
May 16 jQuery
layui实现图片虚拟路径上传,预览和删除的例子
Sep 25 Javascript
JavaScript中的this/call/apply/bind的使用及区别
Mar 06 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
3
2006/10/09 PHP
PHP print类函数使用总结
2010/06/25 PHP
深入解析php中的foreach问题
2013/06/30 PHP
Yii把CGridView文本框换成下拉框的方法
2014/12/03 PHP
Yii 实现数据加密和解密
2021/03/09 PHP
javascript 获取url参数和script标签中获取url参数函数代码
2010/01/22 Javascript
jQuery Pagination Ajax分页插件(分页切换时无刷新与延迟)中文翻译版
2013/01/11 Javascript
开发中可能会用到的jQuery小技巧
2014/03/07 Javascript
JQuery教学之性能优化
2014/05/14 Javascript
javascript判断数组内是否重复的方法
2015/04/21 Javascript
jQuery实现自动滚动到页面顶端的方法
2015/05/22 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
2016/05/12 Javascript
jquery简单插件制作(fn.extend)完整实例
2016/05/24 Javascript
javascript设计模式Constructor(构造器)模式
2016/08/19 Javascript
node.js中的事件处理机制详解
2016/11/26 Javascript
python学习手册中的python多态示例代码
2014/01/21 Python
Python模块包中__init__.py文件功能分析
2016/06/14 Python
python3中dict(字典)的使用方法示例
2017/03/22 Python
Tensorflow 合并通道及加载子模型的方法
2018/07/26 Python
详解Python:面向对象编程
2019/04/10 Python
python,Django实现的淘宝客登录功能示例
2019/06/12 Python
python字符串,元组,列表,字典互转代码实例详解
2020/02/14 Python
ubuntu 安装pyqt5和卸载pyQt5的方法
2020/03/24 Python
django模板获取list中指定索引的值方式
2020/05/14 Python
弄清Pytorch显存的分配机制
2020/12/10 Python
Python 带星号(* 或 **)的函数参数详解
2021/02/23 Python
使用CSS3的font-face字体嵌入样式的方法讲解
2016/05/13 HTML / CSS
浅谈CSS3 动画卡顿解决方案
2019/01/02 HTML / CSS
HTML5开发动态音频图的实现
2020/07/02 HTML / CSS
介绍一下EJB的体系结构
2012/08/01 面试题
党员思想汇报范文
2013/12/30 职场文书
诉前财产保全担保书
2014/05/20 职场文书
环境建议书
2015/02/04 职场文书
外出听课学习心得体会
2016/01/15 职场文书
Python实战之疫苗研发情况可视化
2021/05/18 Python
PostgreSQL之连接失败的问题及解决
2023/05/08 PostgreSQL