参考 :javascript
每当js引擎执行一段新的js代码时,它都会建立一个全新的执行上下文,由执行上下文来跟踪整个代码的执行状况、当前执行函数、做用域、this指向和变量映射。js引擎经过读取执行上下文就能够管理追踪该段js代码的执行状况,那么多个上下文是经过什么来管理的呢?答案是栈,多个上下文的管理是经过上下文栈来管理的。若是解析到一段跟当前上下文无关的新代码,那么js引擎就会在上下文栈里建立一个新的上下文对象,并插入栈顶的位置。当执行完上下文的时候就会在上下文栈里把这个对象踢出栈,恢复下一个栈。html
咱们来看下面这段代码,在全局状况下会有个全局上下文,放在栈底。在执行fn的时候,会在栈顶插入一个新的上下文,而且将当前执行上下文指向这里,当解析到whileFn函数时,又重复刚刚的步骤。这个时候fn上下文会 暂停 并记录执行节点,去执行whileFn上下文,当执行结束后又会 恢复 fn上下文,而且在上次暂停的地方继续执行。因而可知执行上下文跟踪代码时,会有代码执行状态(code evaluation state),用来表示执行状态是暂停、恢复等,以及指示暂停节点。除此以外执行上下文还会记录当前执行的函数(function)以及做用域(realm)java
js执行代码
var global = 'global'
console.log(global)
function fn() {
let inner = 'inner'
let time = 0
function whileFn() {
while(time < 10) {
++time
}
}
console.log(inner)
whileFn()
console.log(time)
}
fn()
复制代码
另外能够看下这段代码及其对应的执行栈示意图es6
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
复制代码
除此以外,执行上下文还会建立一个词法做用域,别看这个名词感受很流弊的样子,其实它只是用于存储标识符和实际引用之间的映射,相似于字典。当js引擎执行到一段代码时,遇到变量名或者函数名时,就会在这个字典里进行查找并调用出里边的值。词法做用域还分为词法环境LexicalEnvironment 和变量环境VariableEnvironment ,在ES5的时候这二者还有着复杂的区别和定义,可是在ES6时,这二者的区别基本就是存储变量的声明关键字不一样,前者是用来存放 let / const / function / class 等声明的标识符映射,然后者是用来存储 var 声明的标识符映射。缓存
词法做用域可分为全局做用域、模块做用域和函数做用域。闭包
// global
import module from './module'
console.log('global environment', this)
function() {
console.log('function environment', this)
}
// module
console.log('module environment', this)
复制代码
刚刚有稍微讲到这几个的区别,接下来咱们结合词法做用域的概念来具体说说。这几个声明的主要区别是初始化变量的机制和存储标识符映射的环境不一样:app
js引擎在建立当前执行上下文时,会初始化词法做用域,在 变量环境 里建立 var 变量的标识符映射并初始化为undefined。var比较特殊,能够屡次执行 var 声明语句赋值相同的变量,js引擎会作相应的建立 / 修改变量的操做ide
js引擎在建立当前执行上下文时,会初始化词法做用域,在 词法环境 里建立 let / const / function / class 标识符映射,可是只会建立 let / const / class 变量,不作初始化操做,而且禁止访问。只有在当前执行上下文执行阶段,执行 词法绑定 时,才会初始化为对应的 value 或者undefined,此时才会容许访问变量。 let / const / class 关键字不容许在同一做用域下重复声明相同变量。函数
var、let、const、class和function的建立机制是,在执行上下文的建立阶段时,js引擎就会在内部建立一个词法做用域,它就像是一个标识符查找的字典。这个字典会在建立阶段就将声明的变量、函数和类都建立,可是var 声明的变量会被初始化为undefined,而 let / const / class 声明的变量不会被初始化,而且禁止访问,function 关键字的声明会直接将函数体赋值给对应的函数名。并且除了var以外,后面几个关键字声明的标识符映射是存放在词法环境里,而var声明的标识符映射是存放在变量环境里。ui
结合上面内容,咱们来看看闭包是什么,咱们常见的闭包写法,就是新建一个闭包函数工厂,在函数里定义变量,而后再返回一个引用这些变量的函数,以下所示:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
复制代码
在makeAdder函数内会建立一个内部的词法做用域,存储传入的x变量,那么每次调用makeAdder函数就会建立一个拥有独立内部词法做用域的函数,add5函数会绑定x=5的做用域,add10绑定的是x=10的做用域。所以add5(2)和add10(2)返回的结果是不同的。 咱们能够看到,闭包是一个绑定了词法做用域的函数,经过闭包能够访问内部词法做用域定义的局部变量。这样子经过闭包咱们能够实现私有方法,避免污染全局环境或者保证方法只能在内部调用,封装细节,以及避免昂贵的计算过程,缓存计算结果。参考下面代码:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var counter1 = makeCounter();
var counter2 = makeCounter();
复制代码
每次调用makeCounter函数会建立独立的闭包,在闭包里会有私有变量privateCounter和私有方法changeBy,外部调用时不须要知道这两个东西,只须要接收increment和decrement方法,不须要知道细节实现
绑定事件监听回调,其实也是一种闭包,只是没有那么明显而已。看看下面这段代码
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
复制代码
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
复制代码
这里的运行结果你们都知道了,可是运行机制就有点意思了。这是由于整个onfocus方法绑定的事件回调是一个闭包,在每一个input控件里触发的onfocus回调函数里,它会在setupHelp这个函数上下文里查找item变量,因为遍历以后i都是2,因此此时item都是{'id': 'age', 'help': 'Your age (you must be over 16)'}这条记录。这就致使了每次showHelp的时候,对应的item都会绑定到同一个对象上去。这个时候的解决方法有两个,要么给onfocus绑定另外一个闭包,要么就是用let关键字声明一个item,保证这个item只会存在for循环的块做用域里。
// 新的闭包
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
...
function setupHelp() {
...
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
// let关键字
...
function setupHelp() {
...
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
复制代码
最后,咱们把执行上下文、词法做用域,还有var / let / const结合起来看的话,js引擎解析代码的总体过程以下: js引擎执行一段和当前上下文无关的代码时,会建立一个对应的执行上下文用于追踪管理代码,并将当前执行上下文指向该上下文。执行上下文的建立过程当中会建立对应的词法做用域,词法做用域有两种:词法环境和变量环境,分别用于存储 let / const / function / class 和 var 声明的变量。词法做用域会经过内部的环境记录来存储标识符与实际变量的映射关系,一个外部引用用于查找非当前做用域的变量时进行逐级溯源查找,以及绑定当前做用域的this指针。当建立完以后,会进入执行上下文的执行阶段,最终执行代码,获取执行结果。
举个例子,咱们看看下面的代码:
const a = 20;
let b = 30;
var c = '';
function foo(a, b) {
var d = a + b;
return d
}
foo(a, b);
复制代码
那么当js引擎解析到上述代码时,首先,会建立一个全局执行上下文用于执行代码,接着在全局上下文的建立阶段,会建立一个全局的词法做用域用于存储标识符映射,此时,var 变量会被初始化为 undefined,而 let / const 变量则是 uninitialized,而且禁止访问。以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: uninitialized,
b: uninitialized,
foo: <ref. to foo function>,
}
outer: <null>,
this: <global object>
}
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined
}
outer: <null>,
ThisBinding: <global object>
}
}
复制代码
接着,全局执行上下文进入执行阶段,进行词法绑定,将标识符和实际变量进行绑定。以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 30,
foo: <ref. to foo function>,
}
outer: <null>,
this: <global object>
}
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: ''
}
outer: <null>,
ThisBinding: <global object>
}
}
复制代码
在执行阶段,当执行到 foo(2, 3) 时,进入 foo 函数,会建立一个函数执行上下文,对应地建立函数词法做用域。函数词法做用域,除了存储内部声明的变量外,还会存储整个函数的实参和实参数量。函数上下文建立阶段的函数词法做用域以下:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
复制代码
当 foo 函数开始执行时,会执行变量的词法绑定,此时,函数上下文执行阶段的词法做用域以下:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
d: 60
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
复制代码