万物科技 学,以致用

JavaScript 9. 闭包

2017-06-09
Geng

JavaScript的函数地位高, 功能强大, 是一等公民. 可以像其他变量一样传来传去, 还可以调用. 我们已经知道函数可以访问外部数据, 那么函数和外部数据如何交流, 有什么限制呢?

这部分内容在作用域一章中已经所有涉及, 但是没有深入, 这里来从头到尾捋一遍.

先把作用域用过的例子抄过来:

function addBy1() {
    var counter = 0;
    
    var count = function() {
        counter = counter + 1;
        return counter;
    }
    
    return count;
}

var countBy1 = addBy1();

countBy1();
countBy1();
countBy1();
3

这个方法的关键就是addBy1()内嵌了一个count()函数, 而且它作为addBy1()的返回值返回, 使得它的生存时间比addBy1()还要长. 也就是说, 在addBy1()释放掉内存后, count()还存在, 而且count()所需的生存环境也还存在. 这里count()的生存环境其中之一就是counter变量.

通过上面方法, 我们使用countBy1()获取了addBy1()方法的返回值count, 那么相当于就是创建了一个小空间, 这个空间包括了具体的执行函数count(), 也包括count()所需环境counter变量. 每次调用countBy1(), 都是这个函数执行在其环境中, 也就是可以记录counter的值了. 那么这里的count函数就是一个闭包.

这是一个JavaScript常见的设计模式: module pattern

那么, 上面说的那个变量的生存环境到底是什么呢?

Lexical Environment 词汇环境

什么是变量

首先, 我们要真的知道变量是什么.

看一个我们见过的例子:

var whoAmI;  // 🐽
whoAmI = 'wanwu.tech'; // 🐱
console.log('I am: ' + whoAmI);
console.log('I am also in (global/window):' + global.whoAmI);  // 浏览器使用window.whoAmI
I am: wanwu.tech
I am also in (global/window):wanwu.tech

我们之前在对象中提过全局变量可以直接写变量名, 也可以写成global/window的属性的形式, 其实这就隐含了一个意思: 变量是某个对象的属性.

什么是词汇环境

JavaScript中, 任何运行的函数, 代码块和脚本都有一个与之对应的对象: 词汇环境.

词汇环境包括:

  • 环境记录: 就是一个对象, 其属性就是词汇环境范围内的变量(还有其余一些信息, 比如:this).
  • 指向上层词汇环境的索引.

那么我们可以认为, 全局变量的词汇环境就是global/window, 而且因为全局变量所在词汇环境已经没有外层词汇环境了, 所以全局变量所在词汇环境指向上层索引为null.

对于上面代码, 脚本运行启动到 🐽 行运行之前, 词汇环境是空的. 然后 🐽 行运行, 声明了一个变量, 但是没有赋值. 最后 🐱 行赋值, 词汇环境的环境记录有了一个有值得变量.

再看函数

函数一章中, 我说过函数声明和函数表达式几乎等效. 那等效我们都知道了, 为什么几乎呢? 看代码:

// 函数声明方法
me();  

function me() {
    console.log('I am a wanwu.tech');
}
I am a wanwu.tech
// 函数表达式
similarMe();  // 😆

var similarMe = function() 
    console.log('I am a wanwu.tech');
}  // 🍄
evalmachine.<anonymous>:2

similarMe();  

^



TypeError: similarMe is not a function

    at evalmachine.<anonymous>:2:1

    at ContextifyScript.Script.runInThisContext (vm.js:44:33)

    at Object.runInThisContext (vm.js:116:38)

    at run ([eval]:617:19)

    at onRunRequest ([eval]:388:22)

    at onMessage ([eval]:356:17)

    at emitTwo (events.js:125:13)

    at process.emit (events.js:213:7)

    at process.nextTick (internal/child_process.js:755:12)

    at _combinedTickCallback (internal/process/next_tick.js:95:7)

看到几乎的地方了吗?

我们先看函数表达式的方法, 根据上面说的词汇环境, 我们应该可以认识到, 在 😆 行运行的时候, 词汇环境中没有一个变量叫做similarMe, 所以报错: “TypeError: similarMe is not a function”. 要一直到 🍄 行, 才能使用similarMe.

但是函数声明方法就比较特殊了. 它不是在执行到的时候才运行, 而是在词汇环境建立的时候就建立了, 也就是脚本已启动就着手建立它. 所以, 你就可以在函数定义前调用这个函数.

内外环境

像大多数你熟悉的语言一样, JavaScript也是有局部变量就使用局部的, 没有就去外面一层找, 再没有再向外找, 没有外面的话就报错. 只不过这里用了一个专业术语: 词汇环境.

内嵌函数

内嵌函数我们都学过了, 但是没说叫内嵌函数这么专业的名词而已, 我只是说”内嵌了一个函数”. 有代码为证(作用域): 本章第一段代码.

函数是JavaScript一等公民, 内嵌有什么不行的? 把它就看成一个变量就行了嘛. 不过这里我们需要深入探讨下这段话:

这个方法的关键就是addBy1()内嵌了一个count()函数, 而且它作为addBy1()的返回值返回, 使得它的生存时间比addBy1()还要长. 也就是说, 在addBy1()释放掉内存后, addBy1()还存在, 而且addBy1()所需的生存环境也还存在. 这里addBy1()的生存环境其中之一就是counter变量.

通过上面方法, 我们使用countBy1()获取了addBy1()方法的返回值count, 那么相当于就是创建了一个小空间, 这个空间包括了具体的执行函数count(), 也包括count()所需环境counter变量. 每次调用countBy1(), 都是这个函数执行在其环境中, 也就是可以记录counter的值了. 那么这里的count函数就是一个闭包.

  • count()为什么的生存空间是什么?
  • count()为什么活的时间比其父函数addBy1()还要长?

到了现在, 第一个问题已经不是问题了吧, 这个生存空间就是词汇空间.

第二个问题, 就要涉及到JavaScript垃圾回收机制了. 简单的说, 就是如果有全局变量可以引用到就保留, 否则就当垃圾扔掉. 然后我把最开始的代码抄过来, 方便看代码:

function addBy1() {
    var counter = 0;
    
    var count = function() {
        counter = counter + 1;
        return counter;
    }
    
    return count;
}

var countBy1 = addBy1();

countBy1();
countBy1();
countBy1();

首先词汇空间比较容易, 重点关注生命长短问题. 其关键就是全局变量有谁. 这里我们只有一个, 就是countBy1, 那么它指向谁, addBy1还是count. 这个问题搞清楚, 我后面说的就是浪费你的时间了.

没有搞清的话, 我们来读出这句: var countBy1 = addBy1();: 声明一个变量countBy, 使它指向addBy1()的返回值.

addBy1()的返回值就是count吧? 那你说countBy1指向谁? 当然是count!!!

那么, 通过var countBy1 = addBy1();, 我们外部索引到的是count, 那么活下来的就是count及其词汇空间.

至于addBy1()? 死了.

那么, 之后你当然可以达到你的目的了.

闭包

终于到闭包了.

闭包就是记得而且能访问他的外部变量的函数. JavaScript中几乎所有函数都是闭包.


Comments

你可以请我喝喝茶,聊聊天,鼓励我

Wechat Pay
wechat

Thanks!