Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript执行上下文 #13

Open
wolfdu opened this issue Dec 18, 2017 · 0 comments

Comments

@wolfdu
Copy link
Owner

commented Dec 18, 2017

https://wolfdu.fun/post?postId=5a26838fc7ad1346411b7265

最近正在拜读JavaScript深入系列文章,初读发现文章简洁明了,知识循序渐进,虽然有些知识点在文章中介绍的不够完全但是在评论区中的讨论却是十分火热,引人思考,所以觉得有必要学习整理梳理其中的观点与知识。

我们从一道引发过很多人思考🤔的面试题开始

//比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

这两段代码的返回结果是相同的都会返回“local scope”,那么他们的不同之处在哪里呢?

从代码的行文来看我们会发现:

  • A代码段:checkscope()返回的是内部函数f()的执行结果
  • B代码段:checkscope()返回的是内部函数f,然后再执行返回的函数

so他们在执行过程中到底由什么区别?

我们都知道在JavaScript引擎在执行一段代码前会做很多事情例如:变量的声明,提升,this的绑定等等。
ECMAScript规范-10.3 执行环境来描述就是

当控制器转入 ECMA 脚本的可执行代码时,控制器会进入一个执行环境。

那么执行什么样的代码段控制器才会进入执行环境呢或者我们常说的执行上下文(Execution Context)?

首先引入一个概念👇

可执行代码类型

一共由3种可执行代码。
*全局代码(Global code):*脚本“程序”处理的源代码文本,也就是代码首次执行的默认环境
*函数代码(Function code ):*一个函数的body,不包括内部嵌套函数的body
*Eval代码(Eval code ):*在eval函数中执行的文本

所以在执行例子中代码的时候首先进入的是全局执行上下文(global context)
(⊙o⊙)…执行上下文(这里只是引入概念,后面有详细解释)是神马👇

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

talking is cheap😂

我们可以发现图中这么多执行上下文(global Context,Execution Context),这些执行上下文是如何管理的呢?

执行上下文栈(ECS Execution Context Stack)

所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。当前活动的多个执行上下文(EC)在逻辑上形成一个栈结构。该逻辑栈的最顶层的执行环境称为当前运行的执行环境。
ess
那么接下来我们从执行上下文栈的角度来分析分析A代码和B代码。
我们使用空数组来模拟ECS:

ECS = [];

当JavaScript遇到A代码段时:

// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
  1. JavaScript解析器解析这段代码时首先遇到了,全局代码,所以这里就会创建全局执行上下文globalContext,并将globalContext压入执行上下文栈中:
ECS = [
	globalContext
];
  1. 接下来在执行checkscope()时会创建函数的执行上下文checkscopeContext,并将其压入执行上下文栈中:
ECS = [
	checkscopeContext,
	globalContext
];
  1. 然后执行f()会创建函数执行上下问fContext,并将其压入执行上下文栈:
ECS = [
	fContext,
	checkscopeContext,
	globalContext
];
  1. 函数f执行完毕,执行上下文从栈中弹出
ECS = [
	checkscopeContext,
	globalContext
];
  1. 函数checkscope执行完毕,执行上下文从栈中弹出
ECS = [
	globalContext
];

机智的你是不是已经发现了A代码段和B代码段的区别了呢?🤓

这里我们简单用数组来描述B代码段的执行过程:

// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
ECS = [];

// 执行global code
ECS.push(globalContex); // [ globalContex ]

// 执行checkscope()
ECS.push(checkscopeContex); // [ globalContex, checkscopeContex]

// checkscope执行完毕
ECS.pop(); // [ globalContex ]

// 执行f()
ECS.push(fContext); // [ globalContex, fContext ]

// f执行完毕
ECS.pop(); // [ globalContex ]

至此我们知道了A代码与B代码执行上下文出入栈的区别,但是在代码执行前的更多细节我们无从得知,其中又有什么区别呢?
例如在创建执行上下文时做了什么?
那么接下来分析分析执行上下文(EC)多么平(生)滑(硬)的转折^_^

执行上下文(EC Execution Context)

这里我们知道每当函数被执行前,就会创建一个执行上下文。
在JavaScript解析器中每个执行上下文都有两个阶段:

  1. 创建阶段(函数被调用,但是未执行任何代码前)
    • 建立作用域链
    • 创建变量对象
    • 确定this的值
  2. 激活或者代码执行阶段
    • 变量赋值,函数引用,以及执行其他代码

这里我们可以将EC抽象为一个对象来表示:

executionContextObj = {
    'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
    'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
    'this': {}
}

这里我们先来聊聊创建阶段的创建变量对象(VO Variable Object)因为建立作用域链会涉及到变量对象相关内容,所以这里了解VO的创建阶段

创建变量对象(VO)

以下是创建变量过程:

  1. 创建arguments object,检查当前上下文参数,初始化参数名称和值,并创建一个参考副本(全局环境下没有该过程)。
  2. 检查当前上下文函数声明(使用function声明的函数):
    • 在VO中创建一个对应函数名的属性其值指向该函数所在内存地址的引用
    • 如果VO中函数名已经存在,则覆盖原有的引用
  3. 检查当前上下文变量声明:
    • 在VO中创建一个对应变量名的属性,并将该值初始化为undefined
    • 如果VO中变量名已经存在,则什么也不做,原属性值不会被修改

从创建变量对象步骤2,3可以发现这就是我们常说的函数声明优先级高于变量声明的原因

我们通过A代码段中执行checkscope函数时创建EC为例,来分析EC创建阶段中变量对象的创建过程:

// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

这里主要关注执行上下文中的VO

checkscopeContextObj = {
    scopeChain: {...},
    variableObject: {
		arguments: {
			length: 0 // 函数checkscope没有参数所以为length为0
		},
		scope: undefined,  // 初始化变量属性值
		f: <f reference function f()>  // 表示f的地址引用
	},
    this: {...}
}

在全局执行上下文中,global object就是变量对象(variable object)。变量对象对于程序而言是不可读的,只有编译器才有权访问变量对象。在浏览器端,global object被具象成window对象,也就是说 global object === window === 全局执行上下文的variable object。因此global object对于程序而言也是唯一可读的variable object。

代码的执行阶段

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

这里需要引入一个概念:活动对象(activation object)

当函数被激活,那么一个活动对象(activation object)就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象(variable object)用于变量初始化。

举个栗子:

function person(name, age){
    var gender = "male";
    function say(){}
}
person(“k”,10);

person被调用时,在person的执行上下文会创建一个活动对象AO,并且被初始化为 AO = [arguments]。
随后AO又被当做变量对象VO(没错就是EC创建阶段的变量对象variable object)进行变量初始化,此时 VO = [arguments].concat([name,age,gender,say])

The global execution context gets some slightly different handling as it does not have arguments so it does not need a defined Activation object to refer to them. [...] The global object is used as the Variable object, which is why globally declared functions become properties of the global object.

全局EC有一些细微的区别,因为它没有arguments所以它不需要定义AO来引用他们,所以global object就是VO,这也是为什么全局声明的函数会成为global object的属性。
这里解释说明了在全局EC中为啥没有AO只有VO

所以VO和AO的关系我们可以这么理解:

函数未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。
它们其实都是同一个对象,只是处于执行上下文的不同生命周期。

所以我们接着创建变量对象中的栗子继续执行代码就是:

// 执行阶段
VO -> AO

activationObject: {
        arguments: {
            length: 0 // 函数checkscope没有参数所以为length为0
        },
        scope: 'local scope',  // 初始化变量属性值
        f: <f reference function f()>  // 表示f的地址引用
    }

接下来,继续执行checkscope执行流,执行return f(),执行f函数。

万事俱备,我们了解了VO的创建和代码执行阶段,接下来看一看作用链的建立。

建立作用域链

作用域链,它在解释器进入到一个执行上下文时初始化完成并将其分配给当前执行上下文。每个执行上下文的作用域链由当前上下文的变量对象(VO)及父级执行上下文的作用域链构成。

可以理解为:currentScopeChain = currentVO + all parents scopes

废话少说,刚正面吧!!!
这里分析A代码段中每个EC中的的作用域链是怎么建立起来(包含其他执行过程):

// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
  1. JavaScript分析器执行全局代码,全局代码的执行上下文被压入栈中
ECS = [ globalContext ];
  1. 初始化全局执行上下文
globalContextObj = {
	VO: {
		scope: undefined,
		checkscope: <checkscope reference function checkscope()>
	},
	scopeChain: globalContextObj.VO, // 由于是全局EC没有父级EC,所以作用域链由当前VO组成
	this: {...}
}
  1. 全局上下文进入代码执行阶段
globalContextObj = {
	 VO: { // 因为全局EC中没有AO所以这里没有转变还是VO
		 scope: 'global scope',
		 checkscope: <checkscope reference function checkscope()>
	 },
	 scopeChain: globalContextObj.VO, // 由于是全局EC没有父级EC,所以作用域链由当前VO组成
	 this: {...}
}
  1. 函数checkscope函数被创建,初始化内部属性[[scope]]

我们知道JavaScript采用的词法作用域,所以在函数定义的时候就决定了函数的作用域。
在函数内部有一个属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

```
checkscope.[[scope]] = globalContextObj.VO
```
  1. 执行checkscope函数代码,将函数EC压如ECS中
ECS = [ globalContext, checkscopeContext ];
  1. 初始化checkscope函数执行上下文,它会复制checkscope函数的[[Scope]]属性并构建作用域
checkscopeContextObj = {
	scopeChain: checkscope.[[scope]]
}
  1. 创建checkscope 函数EC的VO
checkscopeContextObj = {
	VO: {
		arguments: {
			length: 0 
		},
		scope: undefined,
		f: <f reference function f()>
	},
	scopeChain: checkscope.[[scope]],
	this: {...}
}
  1. checkscope 函数EC开始代码执行阶段(VO-->AO)
checkscopeContextObj = {
	AO: {
		arguments: {
			length: 0 
		},
		scope: 'local scope',
		f: <f reference function f()>
	},
	scopeChain: [ checkscope.[[scope]]],
	this: {...}
}
  1. 随后活动对象AO被压入checkscope函数EC的作用域链前端
checkscopeContextObj = {
	AO: {
		arguments: {
			length: 0 
		},
		scope: 'local scope',
		f: <f reference function f()>
	},
	scopeChain: [checkscopeContextObj.AO,  checkscope.[[scope]]],
	this: {...}
}
  1. 函数f被创建,初始化内部属性[[scope]]
f.[[scope]] = checkscopeContextObj.scopeChain
  1. checkscope执行流继续往下走到 return f()
    接下来就是执行一个函数代码的完整流程:
    • 执行f函数代码,将函数EC压如ECS中
    • 初始化f函数执行上下文,它会复制f函数的[[Scope]]属性并构建作用域
    • 创建f 函数EC的VO
    • f EC开始代码执行阶段(VO-->AO)
    • 随后活动对象AO被压入f函数 EC的作用域链前端
ECS = [ globalContext, checkscopeContext, fContext ]
fContextObj = {
		AO: {
			arguments: {
				length: 0 
			}
		},
		scopeChain: [fContextObj.AO, checkscopeContextObj.AO,  checkscope.[[scope]]],
		this: {...}
}
  1. 函数f执行完毕,函数EC从ECS中弹出
ECS = [ globalContext, checkscopeContext ]

同时返回 scope, 解释器根据fContextObj.scopeChain查找变量scope,在checkscopeContextObj.scopeChain中找到scope: 'local scope'

  1. checkscope函数执行完毕,函数EC从ECS中弹出
ECS = [ globalContext ]

到这里,借助作用域链的建立,我们就分析完了A代码段的整个执行过程。咻!!!💀💀💀

那么接下来我们来看看B代码段(⊙o⊙)…👻👻👻
其实这两段代码区别,我们早就说过了,就是执行上下文的栈的变化存在不同点,其他的处理过程基本都是一样的😤

那为什么还要进行执行上下文的分析呢?因为我不知道里面到底做了什么,万一有很多小秘密呢😱😱😱

总结

通过深入系列的文章加上一些拓展的阅读,基本上可以让我理清楚,JavaScript解析器在执行代码的前前后后的一系列事情。也深度的解释了一些问题,变量、函数声明的提升,作用域链的建立等等。
然而。。。
是不是突然发现搞懂了这些你依然还是写不出牛逼闪闪的代码😱😱😱😱

阅读文章:
JavaScript深入之执行上下文栈
JavaScript深入之变量对象
JavaScript深入之作用域链
JavaScript深入之执行上下文
一道js面试题引发的思考
What is the Execution Context & Stack in JavaScript?

若文中有知识整理错误或遗漏的地方请务必指出,非常感谢。如果对你有一丢丢帮助或引起你的思考,可以点赞鼓励一下作者=^_^=

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant
You can’t perform that action at this time.