带你彻底搞懂执行上下文

创建阶段

**生成变量对象 **

  1. 创建arguments:如果是函数上下文,首先会创建 arguments 对象,给变量对象添加形参名称和值。

  2. 扫描函数声明:对于找到的函数声明,将函数名和函数引用(指针)存入 VO 中,如果 VO 中已经有同名函数,那么就进行覆盖(重写引用指针)。

  3. 扫描变量声明:对于找到的每个变量声明,将变量名存入 VO 中,并且将变量的值初始化为undefined 。如果变量的名字已经在变量对象里存在,不会进行任何操作并继续扫描。

让我们举一个栗子来说明 :

function person(age) {

console.log(typeof name); // function

console.log(typeof getName); // undefined

var name = ‘abby’;

var hobby = ‘game’;

var getName = function getName() {

return ‘Lucky’;

};

function name() {

return ‘Abby’;

}

function getAge() {

return age;

}

console.log(typeof name); // string

console.log(typeof getName); // function

name = function () {};

console.log(typeof name); // function

}

person(20);

在调用person(20)的时候,但是代码还没执行的时候,创建的状态是这样:

personContext = {

scopeChain: { … },

activationObject: {

arguments: {

0: 20,

length: 1

},

age: 20,

name: pointer, // reference to function name(),

getAge: pointer, // reference to function getAge(),

hobby: undefined,

getName : undefined,

},

this: { … }

}

函数在执行之前,会先创建一个函数执行上下文,首先是指出函数的引用,然后按顺序对变量进行定义,初始化为 undefined存入到 VO 之中,在扫描到变量 name 时发现在 VO 之中存在同名的属性(函数声明变量),因此忽略。

全局执行上下文的创建没有创建 arguments 这一步

建立作用域链

在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。

  1. 当书写一段函数代码时,就会创建一个词法作用域,这个作用域是函数内部的属性,我们用[[scope]]表示,它里面保存父变量对象,所以[[scope]]就是一条层级链。

person.[[scope]] = [

globalContext.variableObject

]

  1. 当函数调用,就意味着函数被激活了,此时创建函数上下文并压入执行栈,然后复制函数 [[scope]] 属性创建作用域链:

personContext = {

scopeChain:person.[[scope]]

}

  1. 创建活动对象(前面的生成变量对象步骤),然后将活动对象(AO)推到作用域链的前端。

personContext = {

activationObject: {

arguments: {

0: 20,

length: 1

},

age: 20,

name: pointer, // reference to function name(),

getAge: pointer, // reference to function getAge(),

hobby: undefined,

getName : undefined,

},

scopeChain:[activationObject,[[scope]]]

}

确定this的指向

如果当前函数被作为对象方法调用或使用 bindcallapplyAPI 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

执行阶段

执行阶段 中,执行流进入函数并且在上下文中运行/解释代码,JS 引擎开始对定义的变量赋值、开始顺着作用域链访问变量、如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出

此时代码从上到下执行的时候激活阶段的过程是:

  1. 第一次执行 console.log; 此时 nameVO 中是函数。getName 未指定值在 VO 中的值是 undefined

  2. 执行到赋值代码,getName 被赋值成函数表达式,name 被赋值为 abby

  3. 第二次执行 console.log; 此时的 name 由于函数被字符串赋值覆盖因此是 string 类型getNamefunction 类型。

  4. 第三次执行 console.log; 此时的 name 由于又被覆盖因此是 function 类型

因此理解执行上下文之后很好解释了变量提升(Hoisting):实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被JavaScript引擎放入内存中

这就解释了为什么我们能在 name 声明之前访问它,为什么之后的 name 的类型值发生了变化,为什么 getName 第一次打印的时候是 undefined 等等问题了。

ES6 引入了 letconst 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域,很好解决了变量提升带来的一系列问题。

最后执行 console 时候的函数执行上下文:

personContext = {

scopeChain: { … },

activationObject: {

arguments: {

0: 20,

length: 1

},

age: 20,

name: pointer, // reference to function name(),

getAge: pointer, // reference to function getAge(),

hobby: ‘game’,

getName:pointer, pointer to function getName(),

},

this: { … }

}

销毁阶段

一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且等待虚拟机回收,控制权被重新交给执行栈上一层的执行上下文。

完整示例

示例一

var scope = “global scope”;

function checkscope(){

var scope = “local scope”;

function f(){

return scope;

}

return f();

}

checkscope();

1、执行全局代码,生成全局上下文,并且压入执行栈

ECStack=[

globalContext

]

复制代码

2、全局上下文初始化

globalContext={

variableObject:[global,scope,checkscope],

this:globalContext.variableObject,

scopeChain:[globalContext.variableObject]

}

3、创建 checkscope 函数时生成内部属性 [[scope]],并将全局上下文作用域链存入其中

checkscope.[[scope]] = [

globalContext.variableObject

]

4、调用 checkscope 函数,创建函数上下文,压栈

ECStack=[

globalContext,

checkscopeContext

]

5、此时 checkscope 函数还未执行,进入执行上下文

  • 复制函数 [[scope]] 属性创建作用域链

  • 用 arguments 属性创建活动对象

  • 初始化变量对象,加入变量声明、函数声明、形参

  • 活动对象压入作用域链顶端

checkscopeContext = {

activationObject: {

arguments: {

length: 0

},

scope: undefined,

f: pointer, // reference to function f(),

},

scopeChain: [activationObject, globalContext.variableObject],

this: undefined

}

6、checkscope 函数执行,对变量 scope 设值

checkscopeContext = {

activationObject: {

arguments: {

length: 0

},

scope: ‘local scope’,

f: pointer, // reference to function f(),

},

scopeChain: [activationObject, globalContext.variableObject],

this: undefined

}

f 函数被创建生成 [[scope]] 属性,并保存父作用域的作用域链

f.[[scope]]=[

checkscopeContext.activationObject,

globalContext.variableObject

]

7、f 函数调用,生成 f 函数上下文,压栈

ECStack=[

globalContext,

checkscopeContext,

fContext

]

8、此时 f 函数还未执行,初始化执行上下文

  • 复制函数 [[scope]] 属性创建作用域链

  • 用 arguments 属性创建活动对象

  • 初始化变量对象,加入变量声明、函数声明、形参

  • 活动对象压入作用域链顶端

fContext = {

activationObject: {

arguments: {

length: 0

},

},

scopeChain: [fContext.activationObject, checkscopeContext.activationObject, globalContext.variableObject],

this: undefined

}

9、f 函数执行,沿着作用域链查找 scope 值,返回 scope 值

10、f 函数执行完毕,f函数上下文从执行上下文栈中弹出

ECStack=[

globalContext,

checkscopeContext

]

11、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

ECStack=[

globalContext

]

示例二

var scope = “global scope”;

function checkscope(){

var scope = “local scope”;

function f(){

return scope;

}

return f;

}

checkscope()();

  1. 执行全局代码,生成全局上下文,并且压入执行栈

  2. 全局上下文初始化

  3. 创建 checkscope 函数时生成内部属性 [[scope]],并将全局上下文作用域链存入其中

  4. 调用 checkscope 函数,创建函数上下文,压栈

  5. 此时 checkscope 函数还未执行,进入执行上下文

  • 复制函数 [[scope]] 属性创建作用域链

  • arguments 属性创建活动对象

  • 初始化变量对象,加入变量声明、函数声明、形参

  • 活动对象压入作用域链顶端

  1. checkscope 函数执行,对变量 scope 设值,f 函数被创建生成 [[scope]] 属性,并保存父作用域的作用域链

  2. 返回函数f,此时 checkscope 函数执行完成,弹栈

  3. f 函数调用,生成 f 函数上下文,压栈

  4. 此时 f 函数还未执行,初始化执行上下文

  • 复制函数 [[scope]] 属性创建作用域链

  • arguments 属性创建活动对象

  • 初始化变量对象,加入变量声明、函数声明、形参

  • 活动对象压入作用域链顶端

  1. f 函数执行,沿着作用域链查找 scope 值,返回 scope

  2. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

可以看到和前面唯一的区别就是 checkScope 函数执行完先出栈了,之后再执行 f 函数,步骤与示例一一致

fContext = {

scopeChain: [activationObject, checkscopeContext.activationObject, globalContext.variableObject],

}

这里在 checkscopeContext 函数执行完销毁后,f 函数依然可以读取到 checkscopeContext.AO 的值,也就是说 checkscopeContext.AO 依然活在内存中,f 函数依然可以通过 f 函数的作用域链找到它。而为什么 checkscopeContext.AO 没有被销毁,正是因为 f 函数引用了 checkscopeContext.AO 中的值,又正是因为JS实现了在子上下文引用父上下文的变量的时候,不会销毁这些变量的效果实现了闭包 这个概念!

es5版本


ES5 规范去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。

生命周期

es5 执行上下文的生命周期也包括三个阶段:创建阶段 → 执行阶段 → 回收阶段

创建阶段

创建阶段做了三件事:

  1. 确定 this 的值,也被称为 This Binding

  2. LexicalEnvironment(词法环境) 组件被创建

  3. VariableEnvironment(变量环境) 组件被创建

伪代码大概如下:

ExecutionContext = {

ThisBinding = ,     // 确定this

LexicalEnvironment = { … },   // 词法环境

VariableEnvironment = { … },  // 变量环境

}

This Binding

ThisBinding 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this,与 es3this 并没有什么区别,this 的值是在执行的时候才能确认,定义的时候不能确认

创建词法环境

词法环境的结构如下:

GlobalExectionContext = {  // 全局执行上下文

LexicalEnvironment: {       // 词法环境

EnvironmentRecord: {     // 环境记录

Type: “Object”,           // 全局环境

// 标识符绑定在这里

outer:            // 对外部环境的引用

}

}

FunctionExectionContext = { // 函数执行上下文

LexicalEnvironment: {     // 词法环境

EnvironmentRecord: {    // 环境记录

Type: “Declarative”,      // 函数环境

// 标识符绑定在这里      // 对外部环境的引用

outer: 

}

}

可以看到词法环境有两种类型 :

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。

  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

词法环境有两个组件 :

  • 环境记录器 :存储变量和函数声明的实际位置。

  • 外部环境的引用 :它指向作用域链的下一个对象,可以访问其父级词法环境(作用域),作用与 es3 的作用域链相似

环境记录器也有两种类型 :

  • 在函数环境中使用 声明式环境记录器,用来存储变量、函数和参数。

  • 在全局环境中使用 对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。

因此:

  • 创建全局上下文的词法环境使用 对象环境记录器 ,outer 值为 null;

  • 创建函数上下文的词法环境时使用 声明式环境记录器 ,outer 值为全局对象,或者为父级词法环境(作用域)

创建变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。

在 ES6 中,词法环境和 变量环境的区别在于前者用于存储函数声明和变量( letconst关键字)绑定,而后者仅用于存储变量( var )绑定,因此变量环境实现函数级作用域,通过词法环境在函数作用域的基础上实现块级作用域。

🚨 使用 let / const 声明的全局变量,会被绑定到 Script 对象而不是 Window 对象,不能以Window.xx 的形式使用;使用 var 声明的全局变量会被绑定到 Window 对象;使用 var / let / const 声明的局部变量都会被绑定到 Local 对象。注:Script 对象、Window 对象、Local 对象三者是平行并列关系。

箭头函数没有自己的上下文,没有arguments,也不存在变量提升

使用例子进行介绍

let a = 20;

const b = 30;

var c;

function multiply(e, f) {

var g = 20;

return e * f * g;

}

c = multiply(20, 30);

遇到调用函数 multiply 时,函数执行上下文开始被创建:

GlobalExectionContext = {

ThisBinding: ,

LexicalEnvironment: {

EnvironmentRecord: {

Type: “Object”,

// 标识符绑定在这里

a: < uninitialized >,

b: < uninitialized >,

multiply: < func >

}

outer:

},

VariableEnvironment: {

EnvironmentRecord: {

Type: “Object”,

// 标识符绑定在这里

c: undefined,

}

outer:

}

}

FunctionExectionContext = {

ThisBinding: ,

LexicalEnvironment: {

EnvironmentRecord: {

Type: “Declarative”,

// 标识符绑定在这里

Arguments: {0: 20, 1: 30, length: 2},

},

outer:

},

VariableEnvironment: {

EnvironmentRecord: {

Type: “Declarative”,

// 标识符绑定在这里

g: undefined

},

outer:

}

}

变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化 uninitialized(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

图解变量提升:

var myname = “极客时间”

function showName(){

console.log(myname);

if(0){

var myname = “极客邦”

}

console.log(myname);

}

showName()

在 showName 内部查找 myname 时会先使用当前函数执行上下文里面的变量 myname ,由于变量提升,当前的执行上下文中就包含了变量 myname,而值是 undefined,所以获取到的 myname 的值就是 undefined。

执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

回收阶段

执行上下文出栈等待虚拟机回收执行上下文

过程总结
  1. 创建阶段 首先创建全局上下文的词法环境:首先创建 对象环境记录器,接着创建他的外部环境引用 outer,值为 null

  2. 创建全局上下文的语法环境:过程同上

  3. 确定 this 值为全局对象(以浏览器为例,就是 window )

  4. 函数被调用,创建函数上下文的词法环境:首先创建 声明式环境记录器,接着创建他的外部环境引用 outer,值为 null,值为全局对象,或者为父级词法环境

  5. 创建函数上下文的变量环境:过程同上

  6. 确定 this 值

  7. 进入函数执行上下文的 执行阶段

  8. 执行完成后进入 回收阶段

实例讲解

将词法环境中 outer 抽离出来,执行上下文结构如下:

下面我们以如下示例来分析执行上下文的创建及执行过程:

function foo(){

var a = 1

let b = 2

{

let b = 3

var c = 4

let d = 5

console.log(a)

console.log(b)

}

console.log(b)

console.log©

console.log(d)

}

foo()

第一步: 调用 foo 函数前先编译并创建执行上下文,在编译阶段将 var 声明的变量存放到变量环境中,let 声明的变量存放到词法环境中,需要注意的是此时在函数体内部块作用域中 let 声明的变量不会被存放到词法环境中,如下图所示 :

第二步: 继续执行代码,当执行到代码块里面时,变量环境中的 a 的值已经被设置为1,词法环境中 b 的值已经被设置成了2,此时函数的执行上下文如图所示:

从图中就可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,因此示例中在函数体内块作用域中声明的变量的 b 与函数作用域中声明的变量 b 都是独立的存在。

在词法环境内部,实际上维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域内部的变量压到栈顶;当该块级作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。

第三步: 当代码执行到作用域块中的 console.log(a) 时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

这样一个变量查找过程就完成了,你可以参考下图:

第四步: 当函数体内块作用域执行结束之后,其内部变量就会从词法环境的栈顶弹出,此时执行上下文如下图所示:

第五步: 当foo函数执行完毕后执行栈将foo函数的执行上下文弹出。

所以,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

outer引用

outer 是一个外部引用,用来指向外部的执行上下文,其是由词法作用域指定的

function bar() {

console.log(myName)

js基础

1)对js的理解?
2)请说出以下代码输出的值?
3)把以下代码,改写成依次输出0-9
4)如何区分数组对象,普通对象,函数对象
5)面向对象、面向过程
6)面向对象的三大基本特性
7)XML和JSON的区别?
8)Web Worker 和webSocket?
9)Javascript垃圾回收方法?
10)new操作符具体干了什么呢?
11)js延迟加载的方式有哪些?
12)WEB应用从服务器主动推送Data到客户端有那些方式?

js基础.PNG

前16.PNG

以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,因此示例中在函数体内块作用域中声明的变量的 b 与函数作用域中声明的变量 b 都是独立的存在。

在词法环境内部,实际上维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域内部的变量压到栈顶;当该块级作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。

第三步: 当代码执行到作用域块中的 console.log(a) 时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

这样一个变量查找过程就完成了,你可以参考下图:

第四步: 当函数体内块作用域执行结束之后,其内部变量就会从词法环境的栈顶弹出,此时执行上下文如下图所示:

第五步: 当foo函数执行完毕后执行栈将foo函数的执行上下文弹出。

所以,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

outer引用

outer 是一个外部引用,用来指向外部的执行上下文,其是由词法作用域指定的

function bar() {

console.log(myName)

js基础

1)对js的理解?
2)请说出以下代码输出的值?
3)把以下代码,改写成依次输出0-9
4)如何区分数组对象,普通对象,函数对象
5)面向对象、面向过程
6)面向对象的三大基本特性
7)XML和JSON的区别?
8)Web Worker 和webSocket?
9)Javascript垃圾回收方法?
10)new操作符具体干了什么呢?
11)js延迟加载的方式有哪些?
12)WEB应用从服务器主动推送Data到客户端有那些方式?

[外链图片转存中…(img-9sK8W75E-1718863193668)]

[外链图片转存中…(img-mH28IHYN-1718863193669)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值