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中几乎所有函数都是闭包.
完