JavaScript执行上下文
前言:
在看js垃圾回收时遇到相关问题,感觉自己对上下文对象理解的还是不够透彻(实在是太菜了)这就回来恶补。
执行上下文(Execution Context)
执行上下文可以理解为当前代码的运行环境。在 JavaScript 中,运行环境主要包含了全局环境和函数环境。
在 JavaScript 代码运行过程中,最先进入的是全局环境,而在函数被调用时则进入相应的函数环境。全局环境和函数环境所对应的执行上下文我们分别称为全局上下文和函数上下文。
在一个 JavaScript 文件中,经常会有多个函数被调用,也就是说在 JavaScript 代码运行过程中很可能会产生多个执行上下文,那么如何去管理这多个执行上下文呢?
执行上下文是以栈(一种 LIFO 的数据结构)的方式被存放起来的,我们称之为执行上下文栈(Execution Context Stack)。
在 JavaScript 代码开始执行时,首先进入全局环境,此时全局上下文被创建并入栈,之后当调用函数时则进入相应的函数环境,此时相应函数上下文被创建并入栈,当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。
所以在执行上下文栈中,栈底永远是全局上下文,而栈顶则是当前正在执行的函数上下文。
1 | function fn2() { |
对于如上代码
1 | /* 伪代码 以数组来表示执行上下文栈 ECStack=[] */ |
在一个执行上下文中,最重要的三个属性分别是变量对象(Variable Object)、作用域链(Scope Chain)和 this 指向。
1 | EC = { |
一个执行上下文的生命周期分为创建和执行阶段。创建阶段主要工作是生成变量对象、建立作用域链和确定 this 指向。而执行阶段主要工作是变量赋值以及执行其它代码等。
变量对象(Variable Object)
生成变量有三个过程:
1. 检索当前上下文中的函数参数。该过程生成 Arguments 对象(函数参数),并建立以形参变量名为属性名,形参变量值为属性值的属性;
2. 检索当前上下文中的函数声明。该过程建立以函数名为属性名,函数所在内存地址引用为属性值的属性;
3. 检索当前上下文中的变量声明。该过程建立以变量名为属性名,undefined 为属性值的属性(如果变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性)。
伪代码表示变量对象
1 | VO = { |
当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object)。此时原先声明的变量会被赋值。
变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段。
我们可以通过以下伪代码来表示活动对象
1 | AO = { |
实际例子:
1 | function fn1(a) { |
当 fn1 函数被调用时,fn1 执行上下文被创建(创建阶段)并入栈,其变量对象如下所示
1 | fn1_EC = { |
执行阶段:
1 | fn1_EC = { |
对于全局上下文来说,由于其不会有参数传递,所以在生成变量对象的过程中只有检索当前上下文中的函数声明和检索当前上下文中的变量声明两个步骤。
在浏览器环境中,全局上下文中的变量对象(全局对象)即我们熟悉的 window 对象,通过该对象可以使用其预定义的变量和函数,在全局环境中所声明的变量和函数,也会成为全局对象的属性。
函数提升和变量提升
1 | console.log(a) // undefined |
全局上下文的创建阶段
1 | VO = { |
个人理解:每执行一行,AO都会更新一次(给变量赋值),比如执行到console.log(a) // undefined
此时由于AO中的a还未更新,所以输出undefined
所以实际执行过程:
1 | function fn() { |
作用域链(Scope Chain)
作用域链是指由当前上下文和上层上下文的一系列变量对象组成的层级链。它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
我们已经知道,执行上下文分为创建和执行两个阶段,在执行上下文的执行阶段,当需要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,若是没有找到,则会沿着上层上下文的变量对象进行查找,直到全局上下文中的变量对象(全局对象)。
那么当前上下文是如何有序地去查找它所需要的变量或函数的呢?答案就是依靠当前上下文中的作用域链,其包含了当前上下文和上层上下文中的变量对象,以便其一层一层地去查找其所需要的变量和函数。
执行上下文中的作用域链又是怎么建立的呢?我们都知道,JavaScript 中主要包含了全局作用域和函数作用域,而函数作用域是在函数被声明的时候确定的。
每一个函数都会包含一个 [[scope]] 内部属性,在函数被声明的时候,该函数的 [[scope]] 属性会保存其上层上下文的变量对象,形成包含上层上下文变量对象的层级链。**[[scope]] 属性的值是在函数被声明的时候确定的**。
当函数被调用的时候,其执行上下文会被创建并入栈。在创建阶段生成其变量对象后,会将该变量对象添加到作用域链的顶端并将 [[scope]] 添加进该作用域链中。而在执行阶段,变量对象会变为活动对象,其相应属性会被赋值。
所以,作用域链是由当前上下文变量对象及上层上下文变量对象组成的
1 | SC = AO + [[scope]] |
例如:
1 | var a = 1; |
在 fn1 函数上下文中,fn2 函数被声明,所以
1 | fn2.[[scope]]=[fn1_EC.VO, globalObj] |
当 fn2 被调用的时候,其执行上下文被创建并入栈,此时会将生成的变量对象添加进作用域链的顶端,并且将 [[scope]] 添加进作用域链
1 | fn2_EC.SC=[fn2_EC.VO].concat(fn2.[[scope]]) |
this 指向
this 的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。
关于 this 的指向,其实最主要的是三种场景,分别是全局上下文中 this、函数中 this 和构造函数中 this。
全局上下文中 this
在全局上下文中,this 指代全局对象。
1 | // 在浏览器环境中,全局对象是 window 对象: |
函数中 this
函数中的 this 指向是怎样一种情况呢?
如果被调用的函数,被某一个对象所拥有,那么其内部的 this 指向该对象;如果该函数被独立调用,那么其内部的 this 指向 undefined(非严格模式下指向 window)。
1 | var a = 1; |
构造函数中 this
要清楚构造函数中 this 的指向,则必须先了解通过 new 操作符调用构造函数时所经历的阶段。
通过 new 操作符调用构造函数时所经历的阶段如下:
- 创建一个新对象;
- 将构造函数的 this 指向这个新对象;
- 执行构造函数内部代码;
- 返回这个新对象。
所以从上述流程可知,对于构造函数来说,其内部 this 指向新创建的对象实例。
1 | function Person(name, age) { |