JS的词法环境以及闭包
小引:
村里开了一家书店,开店时,店主就会查看自己店里的书或者供应商会给的书。
如果找不到这本书,会向外面的镇上书店问问有没有,要是还没有就问问城里
如果镇上的书店想找一本书,镇上的店主是瞧不起村里的,如果自己店里没有,他只会问问城里书店
词法环境
在 JS 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
-
环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this的值)的对象。 -
对 外部词法环境 的引用,与外部代码相关联。
一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
这里可以把词法环境当作一个书店,这个书店:
存放着一些书(环境记录)
未防找不到书,店主知道怎么去外部大书店(对 外部词法环境 的引用)
变量
就像书店里的一本书,局部变量和全局变量的区别就是一个是村里书店,只有村里人才知道,一个是全球书店,全世界都知道。
函数
-
当函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
-
在函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局)。
-
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
函数就像是书店,开书店时,就需要有一家店来存放一些书和一些供应商的书。
开业时,店主找书有两个方法:自家店和外面的书店。
当有人想买《小王子》,如果自家有(函数内部定义)或者供应商会给你(提供参数),自然不需要跑镇上,否则就要去镇上找,镇上也找不到,就去城里找。
闭包
定义:是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。
在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
为啥JS函数都是天生闭包?
因为JS函数在被创建的时候,函数的词法环境就包含了两个:内部一个(用于函数调用)和外部一个(全局)。这就让JS的函数的手老是可以”伸的很长“
函数的记性好:[[Environment]]
所有的函数在“诞生”时都会记住创建它们的词法环境。
所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。举个栗子:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert(counter()); // 0;
alert(counter()); // 1;
alert(counter()); // 2;
-
counter.[[Environment]] 有对 {count: 0} 词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]] 引用在函数创建时被设置并永久保存。
-
稍后,当调用 counter() 时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]。(此处书店比喻略有不通)
-
如果词法环境中的变量被更改(counter++),则后续的调用就会随之更改。(0 >> 1 >> 2)。
这就像村里的店主(counter),有人进店(代码调用counter),要买《小王子》(寻找变量count),而店主刚开店时就记住(counter刚被创建时),镇上书店可以买到(词法环境的引用)。
以后不管哪里的人来买(不管在哪里被调用),他就往镇上找就好了。但是如果有人把《小王子》内容改了,那没办法,顾客要的就是小王子,只能把更改后的《小王子》给他。
B站上的 "技术蛋老师" 关于JS闭包的讲解
这里大概阐述一下:
-
执行上下文(执行环境)
与上面的词法环境大致一样,JS包含三个环境,但是第三个不太安全,所以现在只考虑全局和函数
🌰举个栗子:
function a(){
b();
function b(){
c();
function c() {
console.log("我在里面");
}
}
}
a();

-
当浏览器初次运行代码的时候,会进入全局执行上下文
-
在全局中,定义了一个函数a( );,以及调用了函数a,调用时,便创建了函数a的执行上下文
-
接着,运行a时,遇到a里的嵌套函数b并调用b,这样便创建了函数b的执行上下文
-
接着,同理,创建了函数c的执行上下文
-
当想输出"我在里面时",会优先考虑处在”执行上下文栈“最上面的函数c的执行上下文
想要理解闭包的概念,就需要理解作用域链的概念
-
作用域链:相当于一个集合,当函数被创建的时候,这个集合就生成,并且包含了当前变量对象以及所有的父级变量对象(这也是为什么绝大多数的JS函数天生就是闭包函数)
-
变量对象:就是常见的函数参数,变量以及函数声明
🌰举个栗子:

-
当代码刚创建的时候,全局执行上下文包含了两个属性:一个是作用域链,一个是变量对象
-
当调用函数bag()时,函数books运行,此时同理,函数books的执行上下文就包含了两个属性
-
当函数books里的嵌套函数运行时,它的执行上下文也是如此,这时它想寻找变量book,他可以在作用域链:{“匿名函数变量对象”+“books变量对象”+“全局变量对象”}里寻找,优先级这里也是从大到小,匿名函数在books变量对象处找到了,所以输出“书包里的书本”。
经典面试题
for (var i = 0; i < 5; i++){
setTimeout (function (){
console.log(i++);
}, 4000);
}
console.log(i);
setTimeout(代码,毫秒数):在指定的毫秒数之后,调用函数或计算表达式。
这里会涉及到 任务队列 的机制,JS为了代码的运行速度,对于setTimeout这类延迟代码(即使毫秒数为0),浏览器不会将其放到执行栈里面,而是先放到任务队列里面,优先执行后面的代码。

💯答案:5, 5, 6, 7, 8, 9
解析:
-
for循环里,i 循环五次,当 i = 5 时跳出循环,由于任务队列的机制,setTimeout()的五次循环都是先放在任务队列里,等待执行栈完毕后才计算结果
-
当 i = 5 时跳出循环后,来到console.log(i),这时输出第一个值:5,执行栈完毕,任务队列开始执行
-
这时,由作用域链可知,该匿名函数可以访问到外部函数的变量,五个i++就会由 i = 5 为起点,得到 5, 6, 7, 8, 9
提问:如果想输出5,0,1,2,3,4,应该怎么改?
for (var i = 0; i < 5; i++){
//利用立即函数来帮忙
(function(x){
setTimeout (function (){
console.log(x++);
}, 4000);
})(i);
}
console.log(i);
什么是立即调用函数表达式?
在之前,JavaScript 中只有 var 这一种声明变量的方式,并且这种方式声明的变量没有块级作用域,程序员们就发明了一种模仿块级作用域的方法。这种方法被称为“立即调用函数表达式”(immediately-invoked function expressions,IIFE)。
如今,我们不应该再使用 IIFE 了,因为有let但是你可以在旧脚本中找到它们。
请再注意一下:如今我们没有理由来编写这样的代码。
块级作用域
这里如果我们把var i = 0;改成 let i = 0;这样,就会输出:undefined,0,1,2,3,4
这就涉及到块级作用域了
所谓的块级就是指if,for 和其它代码块 等
function sayHi() {
if (true) {
var phrase = "Hello";
}
alert(phrase); // 能正常工作,块级外面可以访问得到由var创建的块内变量
}
sayHi();
alert(phrase); // Error: phrase is not defined, 这里遵循着函数作用域规则:镇上的不去村里的
旧时var在块级里所创建的变量,块级外面可以访问得到,现在的let,会让这个块级的城墙加固,让外面访问不到。
if (true) {
let phrase = "Hello";
}
alert(phrase); // 不能正常工作
以上。
浙公网安备 33010602011771号