全面理解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 相关文章推荐
ImageZoom 图片放大镜效果(多功能扩展篇)
Apr 14 Javascript
javascript之Partial Application学习
Jan 10 Javascript
使用JavaScript动态设置样式实现代码(2)
Jan 25 Javascript
做web开发 先学JavaScript
Dec 12 Javascript
JavaScript时间转换处理函数
Apr 14 Javascript
微信小程序 122100版本更新问题解决方案
Dec 22 Javascript
学习使用Bootstrap栅格系统
May 11 Javascript
Javascript将图片的绝对路径转换为base64编码的方法
Jan 11 Javascript
angularJs中$http获取后台数据的实例讲解
Aug 08 Javascript
vue 实现左右拖拽元素并且不超过他的父元素的宽度
Nov 30 Javascript
js实现通过开始结束控制的计时器
Feb 25 Javascript
使用PreloadJS加载图片资源的基础方法详解
Feb 03 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
一些星际专用术语解释
2020/03/04 星际争霸
php中sql注入漏洞示例 sql注入漏洞修复
2014/01/24 PHP
非常经典的PHP文件上传类分享
2016/05/15 PHP
PHP实现图的邻接矩阵表示及几种简单遍历算法分析
2017/11/24 PHP
一个对于js this关键字的问题
2007/01/09 Javascript
JQuery CSS样式控制 学习笔记
2009/07/23 Javascript
用客户端js实现带省略号的分页
2013/04/27 Javascript
以JSON形式将JS中Array对象数组传至后台的方法
2014/01/06 Javascript
js获取某元素的class里面的css属性值代码
2014/01/16 Javascript
Angularjs基础知识及示例汇总
2015/01/22 Javascript
简介AngularJS的视图功能应用
2015/06/17 Javascript
JS简单实现String转Date的方法
2016/03/02 Javascript
【经典源码收藏】jQuery实用代码片段(筛选,搜索,样式,清除默认值,多选等)
2016/06/07 Javascript
详解vue 中使用 AJAX获取数据的方法
2017/01/18 Javascript
使用vue框架 Ajax获取数据列表并用BootStrap显示出来
2017/04/24 Javascript
javascript 中关于array的常用方法详解
2017/05/05 Javascript
JQuery 封装 Ajax 常用方法(推荐)
2017/05/21 jQuery
js实现把时间戳转换为yyyy-MM-dd hh:mm 格式(es6语法)
2017/12/28 Javascript
vue-cli构建vue项目的步骤详解
2019/01/27 Javascript
详解如何在Vue项目中导出Excel
2019/04/19 Javascript
layui固定下拉框的显示条数(有滚动条)的方法
2019/09/10 Javascript
typescript配置alias的详细步骤
2020/08/12 Javascript
Python标准库之itertools库的使用方法
2017/09/07 Python
基于Python3.6+splinter实现自动抢火车票
2018/09/25 Python
关于ZeroMQ 三种模式python3实现方式
2019/12/23 Python
Python代码一键转Jar包及Java调用Python新姿势
2020/03/10 Python
python爬虫看看虎牙女主播中谁最“顶”步骤详解
2020/12/01 Python
波兰家居和花园家具专家:4Home
2019/05/26 全球购物
开发中都用到了那些设计模式?用在什么场合?
2014/08/21 面试题
枚举和一组预处理的#define有什么不同
2016/09/21 面试题
生物化工专业个人自荐信
2013/09/26 职场文书
新郎父亲婚宴答谢词
2014/01/11 职场文书
化工实习心得体会
2014/09/09 职场文书
2014年电话销售工作总结
2014/12/01 职场文书
舞出我人生观后感
2015/06/16 职场文书
2015年幼儿教育工作总结
2015/07/24 职场文书