执行结果/闭包
https://mp.weixin.qq.com/s/HLFZkD8w1PLeFFr-Xkoe8Q
闭包和执行上下文到底是怎么回事?
相关术语:闭包;作用域链;执行上下文;this 值
闭包
产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。
闭包其实只是一个绑定了执行环境的函数。闭包与普通函数的区别是,它携带了执行的环境。 *我们容易产生一个常见的概念误区,有些人会把 JavaScript 执行上下文,或者作用域(Scope,ES3 中规定的执行上下文的一部分)这个概念当作闭包。
执行上下文:执行的基础设施
JavaScript 中与闭包“环境部分”相对应的术语是“词法环境”,但是 JavaScript 函数比λ函数要复杂得多,我们还要处理 this、变量声明、with 等等一系列的复杂语法,λ函数中可没有这些东西,所以,在 JavaScript 的设计中,词法环境只是 JavaScript 执行上下文的一部分。
JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”。
执行上下文在 ES3 中,包含三个部分。
- scope:作用域,也常常被叫做作用域链。
- variable object:变量对象,用于存储变量的对象。
- this value:this 值。
在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。
- lexical environment:词法环境,当获取变量时使用。
- variable environment:变量环境,当声明变量时使用。
- this value:this 值。
在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容。(推荐)
- lexical environment:词法环境,当获取变量或者 this 值时使用。
- variable environment:变量环境,当声明变量时使用。
- code evaluation state:用于恢复代码执行位置。
- Function:执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm:使用的基础库和内置对象实例。
- Generator:仅生成器上下文有这个属性,表示当前生成器。
本篇我们先讲 var 声明与赋值,let,realm 三个特性来分析上下文提供的信息,分析执行上下文中提供的信息。var b = {} // var 把 b 声明到哪里;b 表示哪个变量;b 的原型是哪个对象;
let c = 1 // let 把 c 声明到哪里;
this.a = 2; // this 指向哪个对象。
var 声明与赋值
var b = 1
通常我们认为它声明了 b,并且为它赋值为 1,var 声明作用域函数执行的作用域。也就是说,var 会穿透 for 、if 等语句。
在只有 var,没有 let 的旧 JavaScript 时代,诞生了一个技巧,叫做:立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制 var 的范围。
由于语法规定了 function 关键字开头是函数声明,所以要想让函数变成函数表达式,我们必须得加点东西,最常见的做法是加括号。
(function(){ var a; }());
(function(){ var a; })();
但是,括号有个缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末的函数调用,产生完全不符合预期,并且难以调试的行为,加号等运算符也有类似的问题。所以一些推荐不加分号的代码风格规范,会要求在括号前面加上分号。
;(function(){ var a; }())
;(function(){ var a; })()
比较推荐的写法是使用 void 关键字。也就是下面的这种形式。
void function(){ var a; }();
这有效避免了语法问题,同时,语义上 void 运算表示忽略后面表达式的值,变成 undefined,我们确实不关心 IIFE 的返回值,所以语义也更为合理。
值得特别注意的是,有时候 var 的特性会导致声明的变量和被赋值的变量是两个 b,JavaScript 中有特例,那就是使用 with 的时候:
var b;
void function(){
var env = {b:1};
b = 2;
console.log("In function b:", b);
with(env) {
var b = 3;
console.log("In with b:", b);
}
}();
console.log("Global b:", b);
// In function b: 2; In with b: 3; Global b: undefined
在这个例子中,我们利用立即执行的函数表达式(IIFE)构造了一个函数的执行环境,并且在里面使用了我们一开头的代码。
可以看到,在 Global function with 三个环境中,b 的值都不一样,而在 function 环境中,并没有出现 var b,这说明 with 内的 var b 作用到了 function 这个环境当中。
var b = {} 这样一句对两个域产生了作用,从语言的角度是个非常糟糕的设计,这也是一些人坚定地反对在任何场景下使用 with 的原因之一。
let
let 是 ES6 开始引入的新的变量声明模式,比起 var 的诸多弊病,let 做了非常明确的梳理和规定。
为了实现 let,JavaScript 在运行时引入了块级作用域。也就是说,在 let 出现之前,JavaScript 的 if for 等语句皆不产生作用域。
我简单统计了下,以下语句会产生 let 使用的作用域:
for; if; switch; try/catch/finally。
Realm
在最新的标准(9.0)中,JavaScript 引入了一个新概念 Realm,它的中文意思是“国度”“领域”“范围”。在 ES2016 之前的版本中,标准中甚少提及{}的原型问题。但在实际的前端开发中,通过 iframe 等方式创建多 window 环境并非罕见的操作,所以,这才促成了新概念 Realm 的引入。
Realm 中包含一组完整的内置对象,而且是复制关系。
对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 instanceOf 几乎是失效的。
以下代码展示了在浏览器环境中获取来自两个 Realm 的对象,它们跟本土的 Object 做 instanceOf 时会产生差异:
var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"
var b1 = iframe.contentWindow.b;
var b2 = {};
console.log(typeof b1, typeof b2); //object object
console.log(b1 instanceof Object, b2 instanceof Object); //false true
可以看到,由于 b1、 b2 由同样的代码“ {} ”在不同的 Realm 中执行,所以表现出了不同的行为。
🔚