执行上下文(Execution Context)是执行 Javascript 代码的环境。能够绝不夸张地说,执行上下文是 Javascript 中最重要的概念。它是其余不少重要概念的基础。一旦搞清楚了执行上下文是什么,咱们就能很轻松地掌握下面这些概念:javascript
this
以及arguments
是如何赋值的在深刻理解 Javascript 之 CallStack&EventLoop一文中,咱们已经简单了解了 Javascript 程序是如何执行以及函数调用的过程。咱们知道每次调用一个函数时,都会建立一个“调用信息”结构压入调用栈。其实这个调用结构就是执行上下文。所以调用栈(Call Stack)也被称为执行栈(Execution Stack)。java
执行上下文有两种类型:git
一种为全局执行上下文(Global Execution Context),程序开始时建立,有且只有一个。github
另外一种为局部执行上下文(Local Execution Context),调用函数时建立。局部执行上下文又称为函数执行上下文(Function Execution Context)。浏览器
看下面的代码:bash
var name = "darjun";
var email = "leedarjun@gmail.com";
function greeting() {
console.log(`Hi, I'm ${name}, my email is ${email}!`);
}
greeting();
复制代码
Javascript 引擎在执行代码时会建立一个全局对象(global object)。在浏览器中全局对象为window
对象,在 Node 环境中为global
对象。闭包
1) 在全部函数外层定义的变量都会保存在全局对象中。ide
2) 在函数内,未使用var
,let
或const
修饰的变量定义也会将变量存储在全局对象中。函数
接下来引擎开始解析代码,建立<main>
函数包裹代码。 而后,<main>
函数执行。此时,Javascript 引擎首先会建立一个全局执行上下文。工具
执行上下文的建立分为两个阶段:
1)建立阶段(Creation Phase)
2)执行阶段(Execution Phase)
在全局执行上下文的建立阶段,引擎将进行以下处理:
1)绑定this
到全局对象。
2)建立一个全局环境对象(Global Environment)。为<main>
中定义的变量和函数分配内存。var
定义的变量初始值为undefined
。
此时,全局执行上下文以下所示:
GlobalExecutionContext = {
Phase: Creation, // 建立阶段
this: GlobalObject,
GlobalEnvironment: {
name: undefined,
email: undefined,
greeting: fn,
}
}
复制代码
注意:此时代码还未执行。
接下来,引擎开始从上到下,一行一行地执行<main>
函数。
首先,引擎将全局执行上下文压入调用栈。这时全局执行上下文切换为执行阶段(Phase: Creation -> Execution)。而后,跳过函数定义。由于greeting
函数在建立阶段就已经被解析完成而且放入全局环境对象中了。而后执行到代码greeting();
调用greeting
函数。
引擎首先为函数greeting
建立一个局部执行上下文。局部执行上下文的建立也将经历建立和执行两个阶段。建立阶段时,引擎执行以下处理:
1)根据调用方式绑定this
变量。在这个例子中,函数greeting
是全局函数,没有对象限定。this
被绑定到全局对象。
2)建立一个局部环境对象(Local Environment)。该对象与全局环境对象做用相似,只不过是为函数中定义的变量和函数分配内存。该对象中有一个指向外层环境对象的指针outer
这时的局部执行上下文以下所示:
Greeting ExecutionContext = {
Phase: Creation, // 建立阶段
this: GlobalObject,
LocalEnvironment: {
// 没有变量或函数定义
outer: <GlobalEnvironment>
},
}
复制代码
引擎将该局部执行上下文压入调用栈开始执行。greeting
执行完成以后,从调用栈上弹出其局部执行上下文。此时栈顶只有一个全局执行上下文,继续执行<main>
。
<main>
执行完成,将全局执行上下文从调用栈中弹出,程序结束。
上面咱们了解了什么是执行上下文,而且深刻到程序执行内部观察到引擎是怎么处理函数调用的。接下来,咱们将运用执行上下文来了解 Javascript 的几个核心概念。
顶置实际上是因为 Javascript 特殊的执行逻辑而出现的。咱们先修改一下前面的示例代码:
console.log(name);
console.log(email);
var name = "darjun";
var email = "leedarjun@gmail.com";
function greeting() {
console.log(`Hi, I'm ${name}, my email is ${email}!`);
}
greeting();
复制代码
代码前两行的输出是什么?
咱们知道一个执行上下文会经历建立和执行两个阶段。在建立阶段时,引擎首先为函数中定义的变量和函数分配内存空间并存入环境对象中。var
定义的变量初始化为undefined
,函数直接解析完成。 而后,引擎压入该执行上下文,一行一行执行代码。
那么很清楚了,前两行的输出都是undefined
。由于在执行上下文的建立阶段,name
和email
会被初始化为undefined
。这就形成变量或函数还未定义就能直接使用的假象,看起来好像var
变量和函数定义被“提高”或“顶置”到代码的最前面同样。一样的道理,在代码最上面也能够打印函数greeting
,将打印出具体的函数对象。由于顶层函数在建立阶段就已经存在环境对象中了。快试试🤩。
var
的这种特性常常会形成意想不到的结果,因此 ES6 引入了另外一种变量定义方式let
。let
定义的变量在定义以前引用会抛出异常。这是怎么作到的呢?
其实很简单。在执行上下文的建立阶段,let
定义的变量也会存入环境对象中。不过,它的初始值为UnInitialized
(未初始化)。在执行时,若是引用一个值为UnInitialized
的变量,引擎直接抛出一个错误🥴。
是指函数中能访问在函数外层定义的变量,这个函数加上外层的环境就构成了一个闭包。咱们仍是经过案例来分析:
function makeAdder(num) {
return function (x) {
return x + num;
}
}
var adder2 = makeAdder(2);
console.log(adder2(10)); // 12
var adder5 = makeAdder(5);
console.log(adder5(10)); // 15
复制代码
第一次调用函数makeAdder
时,传入参数2
,返回一个匿名函数赋值给变量adder2
。这时,makeAdder
函数已返回。可是adder2
调用时能正确返回12
。说明adder2
能访问到以前传入的参数num
。
第二次调用函数makeAdder
时,传入参数5
,返回一个匿名函数赋值给变量adder5
。此时,makeAdder
函数已返回。可是adder5
调用时能正确返回15
。说明adder5
能访问到以前传入的参数num
。而且,adder2
与adder5
访问到的num
变量相互独立(一个为2,一个为5)。
运用执行上下文模拟一次程序执行过程,能很清楚的看到闭包的工做原理。
参数num
至关因而在函数内定义的变量。 首先,第一次调用makeAdder
时。引擎为这次调用建立一个新的局部环境对象,num
被保存在此对象中:
makeAdder LocalEnvironment2 = {
num: 2,
}
复制代码
adder2
被调用时,引擎会建立一个新的局部环境对象。该对象中保存着x = 10
,而且其outer
指针指向上面的LocalEnvironment2
:
adder2 LocalEnvironment = {
x: 10,
outer: <makeAdder LocalEnvironment2>
}
复制代码
adder2
执行过程当中,访问变量num
。引擎首先在adder2
的局部环境对象中查找num
,没有找到。而后引擎会到其外层的环境对象中继续查找,直到找到该变量。或者直到全局环境对象中也未能找到,抛出引用错误。 在该示例中,外层环境对象中查找到num
为2
。adder2(10)
执行完成,输出12
。
第二次调用makeAdder
时。引擎为这次调用建立一个新的局部环境对象,num
被保存在此对象中:
makeAdder LocalEnvironment5 = {
num: 5,
}
复制代码
adder5
被调用时,引擎会建立一个新的局部环境对象。该对象中保存x = 10
,而且其outer
指针指向上面的LocalEnvironment5
:
adder5 LocalEnvironment = {
x: 10,
outer: <makeAdder LocalEnvironment5>
}
复制代码
执行代码return x + num
时,按照上面的变量查找流程,在外层环境对象LocalEnvironment5
中找到的num
值为5
。adder5(10)
执行完成,输出15
。
arguments
咱们知道,在函数调用中,arguments
对象中包含传入的全部参数、参数的长度以及其余一些信息。例如:
function f(a, b, c) {
console.log(arguments);
}
f(1, 2); // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
复制代码
参数列表在调用时会依次被赋予传入的实参。调用时的局部对象会包含全部参数变量,arguments
等:
LocalEnvironment = {
a: 1,
b: 2,
arguments: [1, 2] // ...
}
复制代码
this
绑定先看一段代码:
var person = {
name: "darjun",
age: 29,
greeting: function () {
console.log(`Hi, I'm ${this.name}, ${this.age} years old`);
}
}
person.greeting(); // 输出 Hi, I'm darjun, 29 years old
var g = person.greeting;
g(); // 输出 Hi, I'm undefined, undefined years old
复制代码
前面咱们知道 Javascript 引擎在执行一个函数前会进行this
绑定。具体为this
绑定什么值,视调用形式而定。
在上面的代码中,第一次调用greeting
函数时,经过对象person
限定,引擎会将person
绑定为this
。 第二次调用前,将person.greeting
赋值给变量g
。而后直接调用函数g
,引擎看到这次调用没有.
限定符,故而将this
绑定为全局对象。 因此输出为"Hi, I'm undefined, undefined years old"(注意:输出视全局对象中是否有name
和age
属性而有所不一样)。
这里我给你们推荐一个可视化查看程序执行的工具:javascript-visualizer。
顶置:
闭包:
工具并不完善,可是很是有助于咱们理解执行上下文。很是值得一试🤩。
我认为执行上下文是 Javascript 中最最重要的概念。掌握了执行上下文,咱们能很深入地洞悉 Javascript 程序的运行机理,能很轻松地理解其余的一些重要概念:顶置(Hoisting)、闭包(Closure)、this
和arguments
等。
掌握执行上下文,真的能称霸 Javascript 世界哦🤩。