闭包和执行上下文是什么样的关系?

913 阅读4分钟

今天来说一下这些东西:

  • 闭包;
  • 作用域链;
  • 执行上下文;
  • this值;

实际上,它们有表示不同的意思的术语,所指向的几乎是同一部分的知识,我们来看一下图片。

我们来说一下它们之间的关系是怎样的。

闭包

闭包的英文closure,在计算机领域它有三个不同的含义:编译原理中,它是处理语法产生式的一个步骤;在计算几何中,它表示凸包;而在编程语言中,它表示一种函数。

简单的来说,闭包其实就是一个绑定了执行环境的函数,这个函数并不是印在书本的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就想人在外太空需要自带吸氧设备一样。

在闭包定义中,闭包包含了两个部分。

  • 环境部分

    环境

    标识符列表

  • 表达式部分

当我们把视角放在javascrip的标准中,我们发现,标准中并没有出现过closure这个术语,但是,在javascri中找到对应的闭包组成的部分。

  • 环境部分

    环境:函数的词法环境

    标识符列表:函数中用到的未声明的变量

  • 表达式部分:函数体

我们可以认为,javascript中的函数完全符合闭包的定义,它的环境部分 是函数词法环境部分的组成,它的标识符列表是函数中用到的未声明变量,它的表达式部分就是函数体。

实际上javascript中的闭包对应的感念就是"函数",可能是这个概念太过于普通,跟闭包看起来又没什么联系。

执行上下文:执行的基础设施

相比普通函数,javascript函数的主要复杂性来自于它携带的"环境部分"。当然,发展到今天的javascript设计中,词法环境只是javascript执行上下文的一部分。

在ES5中,

我们改进了命名方式,把执行上下文最初的三个部分改为下面的样子。

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this值。

在ES2018中,

执行上下文又变成了这个样子,this值被归入lexical environment,但是增加了不少内容。

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • code evaluation state:恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示当前生成器。

从实际的代码示例出发:

 var b = {}
 let c = 1
 this.a = 2;

想要正确执行它,我们需要知道下面的信息:

1.var 把b声明到哪里;

2.b表示那个变量;

3.b的原型是哪个对象;

4.let 把c声明到哪里;

5.this指向那个对象。

var声明与赋值

我们分析一下代码:

var b = 1

通常我们认为声明了b,并且赋值为1,var声明作用域函数执行的作用域,也就是说,var会穿透for、if等语句。

由于语法规定了function关键字开头是函数声明,所以想让函数变成函数表达式,我们必须得加点东西,最常见的做法是加括号。

(function(){
var a;
//code
}());


(function(){
var a;
//code
})();

但是,括号有个去缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末端的函数调用,产生不符合预期,而且难以调试。所以一些推荐不加分号的代码风格规范。

;(function(){
var a;
//code
}())


;(function(){
var a;
//code
})()

使用void关键字。下面这种形式。

void function(){
var a;
//code
}();

需要注意的是,有时候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);

可以看到,在 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

var b = {}

在ES2016之前的本本中,标准中甚少提及{}原型问题,Realm中包含一组完整的内置对象,而且是复制关系。

一下代码示例:

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 中执行,所以表现出了不同的行为。