很久之前就想写一篇关于闭包的博客了,但是总是担心写的不够完全、不够好,不管怎样,还是要把我理解的闭包和大家分享下,比较长,希望耐心看完。

定义

说实话,给闭包下一个定义是很困难的,原因在于javascript设计的时候并没有专门设计闭包这样一个规则,闭包是随着作用域链、函数可以作为一等公民这样的规则而诞生的。

尽管不能下一个很完美的定义,但是我们还是可以给闭包下一个尽量准确的定义。

闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。

哪些是闭包?

来看下面这个例子1:

function foo() {
var a = 2;
function bar() {
console.log( a ); //
}
bar();
}
foo();

基于词法作用域的查找规则,函数bar() 可以访问外部作用域中的变量a(这个例子中的是一个RHS 引用查询)。

那么这个是闭包吗?很遗憾不是,因为bar函数执行在其定义的词法作用域处。


不过稍加修改后就是个闭包了,例子2:

function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

baz函数执行实际上只是通过不同的标识符引用调用了内部的函数bar()

bar()函数显然可以被正常执行,也就是在自己定义的词法作用域以外的地方执行

根据作用域的规则,函数bar()函数能够访问foo()的内部作用域,因此foo()执行完后,其内部作用域并不会被回收,bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。


来看例子3:

function foo() {
var a = 2;
function baz() {
console.log( a ); //
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}

是的,这也是个闭包,这里将baz传递出去了在bar()函数中执行,而不是在自己定义的词法作用域中执行,但是它却保留这对定义时词法作用域的引用


再看例子4:

var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz 分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); //

是的没错,这还是个闭包,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。


那我们看一个难一点的例子5:

function wait(message) {
  setTimeout( function timer() {
    console.log( message );
  }, 1000 );
}
wait( "Hello, closure!" );

这是闭包吗?答案是的,在这里我们向setTimeOut传入timer()函数,并且timer函数可以访问wait的内部作用域,保持着对wait内部作用域的引用,比如里面的message变量。

这时候你肯定会心生疑惑:不对呀?这在哪执行呢?不是说要在定义的词法作用域以外执行吗?

传入的timer函数当然会被执行,只是内部引擎调用执行的

深入到引擎的内部原理中,内置的工具函数setTimeout(..) 持有对一个参数的引用,这个参数也许叫作fn 或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer 函数,而词法作用域在这个过程中保持完整,time函数保持着对wait内部作用域的引用。

IIFE(立即执行函数)是闭包吗?

例子6:

var a = 2;
(function IIFE() {
console.log( a );
})();

按照我们的定义来说,这不是闭包。

但是,尽管IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。

因此IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包。

这也是为什么很难给闭包下定义的地方,因为如果从内存或者作用来看,IIFE创建了闭包(也就是在内存中创建了一块区域,这块区域保存着作用域链上作用域的引用,稍后可见例子9),或者说效果等同于创建了闭包。

而如果从闭包的定义来看,这却不是闭包。


我们来看例子7:

for (var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

大家都知道这段代码会输出五次6,为什么呢?

因为setTimeOut()是异步函数,也就是等循环结束后才去执行setTimeOut()中的回调函数,而在for循环中,并不存在着块级作用域,也就是这个i声明在全局作用域中,并且自始至终只有一个i(因为var声明会变量声明提升,也就是其实只声明了一次),而在for循环结束后,这个i的值是6。setTimeOut()中的回调函数timer()保持着对i的引用,但是5次timer()函数引用的只是同一个i,所以输出5次6。


例子8:

for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}

这样有效果么?答案是没有的,虽然通过IIFE每次都创建了一个作用域,但是这个作用域是空的(也就是创建了一个空作用域),所以还会沿着词法作用域链去上一层找i,结果找到的还是全局作用域中的i,也就是只有一个i,还是会输出五次6。


所以我们需要这样改,来看例子9:

// 它需要有自己的变量,用来在每个迭代中储存i 的值:
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
// 行了!它能正常工作了!。
// 可以对这段代码进行一些改进:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//当然你也可以这样写
for (var i=1; i<=5; i++) {
(function(i) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})( i );
}

在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

好在ES6出来了let的解决方案,let并不会变量声明提升,并且具有块级作用域的效果,也就是这里会产生5个i的内存空间,被五个timer()函数引用着。

例子10:

for (let i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

关于闭包的垃圾回收

问题1:闭包会造成内存泄漏吗?

我们常说闭包会造成内存泄漏,这是真的吗?答案是不会的。

之所以之前一直说闭包会造成垃圾泄露是由于IE9 之前的版本对JavaScript 对象(标记清除)和COM 对象(引用计数)使用不同的垃圾收集方法。因此闭包在IE 的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁


例子11:

function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}

以上代码创建了一个作为element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element 的引用数。只要匿名函数存在,element 的引用数至少也是1,因此它所占用的内存就永远不会被回收。

解决办法就是把element.id 的一个副本保存在一个变量中,从而消除闭包中该变量的循环引用同时将element变量设为null。


例子12:

function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}

问题2:闭包中没有使用的变量会被回收吗?

答案是会的。

来看例子13:

function foo() {
var x = {};
var y = "whatever"; return function bar() {
alert(y);
};
} var z = foo();

在这里x没有被使用,那么x会被回收吗?答案是的。

理论上来说,bar函数保存着foo作用域中的引用,那么x不应该会被回收。但是现代javascript引擎是非常智能的,对这里进行了优化。

javascript引擎经过逃逸分析(分析函数调用关系,以判断变量是否“逃逸”出当前作用域范围)后判断出来x没有在闭包中使用到,那么它就会把x从堆中的作用域中移除出去。

一般是如何分析呢?很简单,如果闭包中没有引用到这个变量,并且没有使用 eval 或者 new Function,那么javascript引擎可以知道闭包的内存中的作用域不需要这个变量x.

具体测试可以看之前司徒正美的一篇文章:JS闭包测试

或者可以看看stackoverflow上的一篇解答:JavaScript Closures Concerning Unreferenced Variables

问题3:闭包中函数里的变量是分配在堆中还是栈中?

在简单的解释器实现里,函数里的变量是分配在堆而不是在栈上的。现代 JS 引擎当然就比较牛逼了,通过逃逸分析是可以知道哪些可以分配在栈上,哪些需要分配在堆上的。

也就是闭包中使用到的变量会分配在堆中,没有使用到的会分配在栈中(针对简单类型而言),以方便回收。

比如例子13的x,没有被闭包使用,不过是一个复杂类型,所以它在内存中是变量x存储在栈中,同时栈中x的值是堆中的对象{}的地址,大概是下面这样

【栈x】---->(堆{})

例子13中的y,被闭包使用了,闭包的函数就基于原先的词法作用域单独在堆中分配了内存,也就是闭包保存在了堆,同时其使用的变量也随着闭包一起保存在堆,大概是下面这样。

(堆(闭包(y:“whatever”)))

好了,以上这就是我的个人理解了,如果有什么疑问或者建议欢迎讨论。

你不知道的JS(2)深入了解闭包的更多相关文章

  1. 你不知道的JS之作用域和闭包 附录

     原文:你不知道的js系列 A 动态作用域 动态作用域 是和 JavaScript中的词法作用域 对立的概念. 动态作用域和 JavaScript 中的另外一个机制 (this)很相似. 词法作用域是 ...

  2. 你不知道的JS之作用域和闭包(五)作用域闭包

    原文:你不知道的js系列 一个简单粗暴的定义 闭包就是即使一个函数在它所在的词法作用域外部被执行,这个函数依然可以访问这个作用域. 比如: function foo() { var a = 2; fu ...

  3. 你不知道的JS之作用域和闭包(三)函数 vs. 块级作用域

      原文:你不知道的js系列 在第(二)节中提到的,标识符在作用域中声明,这些作用域就像是一个容器,一个嵌套一个,这个嵌套关系是在代码编写时定义的. 那么到底是什么产生了一个新的作用域,只有函数能做到 ...

  4. 你不知道的JS之作用域和闭包(四)(声明)提升

    原文:你不知道的js系列 先有鸡还是先有蛋? 如下代码: a = 2; var a; console.log( a ); 很多开发者可能会认为结果会输出 undefined,因为 var a 在 a ...

  5. 你不知道的JS之作用域和闭包(二)词法作用域

    原文:你不知道的js系列 词法作用域(Lexical Scope) Lex time 一个标准的编译器的第一个阶段就是分词(token化) 词法作用域就是在词法分析时定义的作用域.换句话说,词法作用域 ...

  6. 你不知道的JS之作用域和闭包(一)什么是作用域?

    原文:你不知道的js系列 什么是作用域(Scope)? 作用域 是这样一组规则——它定义了如何存放变量,以及程序如何找到之前定义的变量. 编译器原理 JavaScript 通常被归类为动态语言或者解释 ...

  7. 说说循环与闭包——《你不知道的JS》读书笔记(一)

    什么是闭包 <你不知道的JS>里有对闭包的定义:"当函数可以记住并访问所在的词法作用域,即使函数是在当前作用域之外执行,这就产生了闭包." 讲闭包是啥的太多了...就一 ...

  8. 翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  9. 翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  10. 翻译连载 | 附录 B: 谦虚的 Monad-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

随机推荐

  1. 需要优化代码的leetcode

    1  关于验证字符串的问题: 2

  2. 异想-天开 python---while、for、if-else 循环学习

    for循环: for i in range(10): # i循环10次 print('------',i) for j in range(10): print(j) if j > 2 : bre ...

  3. java-方法重载、参数传递、

    1.Java的方法重载overload:同一个类内,可以有多个同名的方法,只要参数不同即可(包括参数类型和个数.多类型顺序) 2.基本类型(8种:byte\short\int\long\double\ ...

  4. [开发笔记]--把input框设置成font-size:0埋下的坑。

    vue项目上开发了一个输入pin码的组件.在某些安卓机器上.用户点击键盘后会自动失去焦点自动收起键盘.经过排查,发现是css属性font-size设置成0导致的.

  5. oracle树形结构全路径查询

    很实用的语法,父子节点通过id与patientId来关联,知道子节点的id,想查出所有的父节点: START WITH ...CONNECT BY ... SELECT T2.ORG_FULLNAME ...

  6. Asp.net Web Api开发(第四篇)Help Page配置和扩展

    https://blog.csdn.net/sqqyq/article/details/52708613

  7. 3.1.4 Spring的事务管理

    四.Spring的事务管理 事务原本是数据库中的概念, 在Dao层. 但一般情况下, 需要将事务提升到 业务层, 即Service层. 这样做是为了 能够使用事务的特性来管理具体的业务. 1. Spr ...

  8. ESP8266 RTOS SDK烧写环境构建

    简介 esptool是一个Python软件程序,适用于ESP8266等一系列芯片的烧写,灵活高效. 环境构建 在官网下载安装最新2.7版python (linux和os x一般会自带python2.7 ...

  9. E. Neko and Flashback

    传送门: 题意:假定我们已知a[]={3,4,6,5,7},  那么b[]通过min(a[i],a[i+1])得到 那么b[]={3,4,5,5}, c[]通过max(a[i],a[i+1])得到 c ...

  10. UGUI动态更换精灵图片

    //动态更换精灵图片 m_headimage.overrideSprite = Resources.Load("texture/"+info.HeadPortrait,typeof ...