执行上下文、做用域链和JS内部机制(Execution context, Scope chain and JavaScript internals)javascript
执行上下文(Execution context EC)是js代码的执行环境,它包括 this的值、变量、对象和函数。
全局上下文是文件第一次加载到浏览器,js代码开始执行的默认执行上下文。在浏览器环境中,严格模式下this的值为undefined,不然this的值为window对象。GEC只能有一个(由于js执行的全局环境只能有一个)。java
函数执行时建立函数执行上下文,每一个函数都有本身的执行上下文。FEC能够获取到GEC中的内容。当在全局上下文中执行代码时js引擎发现一个函数调用,则建立一个函数执行上下文。浏览器
执行eval时建立ide
执行上下文栈Execution context stack (ECS)是执行js代码时建立的执行栈结构。 GEC默认在栈的最里层,当js引擎发现一个函数调用,则建立这个函数的 FEC并push进栈,js引擎执行栈顶上下文关联的函数,一旦函数执行完,则将其 FEC pop出栈,并往下执行。
看个例子(动图插不了栈动图连接)函数
var a = 10; function functionA() { console.log("Start function A"); function functionB(){ console.log("In function B"); } functionB(); } functionA(); console.log("GlobalContext");
上面讨论了js引擎如何处理执行上下文(push和pop),下面讨论js引擎如何建立执行上下文,这个过程分为两个阶段:建立阶段和执行阶段。this
js引擎调用函数,但函数还没开始执行阶段。
js引擎在这个阶段对整个函数进行一个编译(compile the code),主要干了下面三件事:code
可变对象是包含全部变量、函数参数和内部函数声明信息的特殊对象,它是一个特殊对象且没有__proto__属性。cdn
一旦可变对象建立完,js引擎就开始初始化做用域链。做用域链是一个当前函数所在的可变对象的列表,其中包括GEC的可变对象和当前函数的可变对象。对象
初始化this的值ip
下面经过一个例子进行说明
function funA (a, b) { var c = 3; var d = 2; d = function() { return a - b; } } funA(3, 2);
当调用funA和执行funA前的这段时间,js引擎为funA建立了一个executionContextObj以下
executionContextObj = { variableObject: {}, // All the variable, arguments and inner function details of the funA scopechain: [], // List of all the scopes inside which the current function is this // Value of this }
可变对象包含参数对象(包含函数参数的细节),声明的变量和函数,以下所示
variableObject = { argumentObject : { 0: a, 1: b, length: 2 }, a: 3, b: 2 c: undefined, d: undefined then pointer to the function defintion of d }
在此阶段,js引擎会重扫一遍函数,用具体的变量的值来更新 可变对象,并执行代码内容。
执行阶段执行完后,可变对象的值以下:
variableObject = { argumentObject : { 0: a, 1: b, length: 2 }, a: 3, b: 2, c: 3, d: undefined then pointer to the function defintion of d }
代码以下
a = 1; var b = 2; cFunc = function(e) { var c = 10; var d = 15; a = 3 function dFunc() { var f = 5; } dFunc(); } cFunc(10);
当浏览器加载上面的代码后,js引擎进入编译阶段,只处理声明,不处理值。下面走读一遍代码:
此时的
globalExecutionContextObj = { variableObject: { // 原文中有时用activationObj argumentObj : { length:0 }, b: undefined, cFunc: Pointer to the function definition }, scopeChain: [GLobal execution context variable object], this: value of this }
再接着上面,js引擎进入执行阶段并再过一遍。此时将会更新变量名和执行
此时
globalExecutionContextObj = { variableObject: { argumentObj : { length:0 }, b: 2, cFunc: Pointer to the function definition, a: 1 }, scopeChain: [GLobal execution context variable object], this: value of this }
因为cFunc有个参数e,js引擎会在cFunc执行上下文对象可变对象添加e属性,并初始化为2
此时
cFuncExecutionContextObj = { activationbj: { argumentObj : { 0: e, length:1 }, e: 10, c: undefined, d: undefined dFunc: Pointer to the function definition, }, scopeChain: [cFunc variable object, Global exection context variable object], this: value of this }
cFuncExecutionContextObj = { activationbj: { argumentObj : { 0: e, length:1 }, e: 10, c: 10, d: 15 dFunc: Pointer to the function definition, }, scopeChain: [cFunc variable object, Global exection context variable object], this: value of this }
调用dFunc,js引擎再次进入编译阶段,建立dFunc执行上下文对象。
dFunc执行上下文对象能够访问到cFunc和全局做用域中的全部变量和函数;一样cFunc能够访问到全局的,但不能访问dFunc中的;全局上下文对象不能访问cFunc和dFunc中的变量和对象。
有了上面的概念,对hoisting(变量提高)应该更容易理解了。
做用域链是当前函数所在的可变对象列表
看下面一段代码
a = 1; var b = 2; cFunc = function(e) { var c = 10; var d = 15; console.log(c); console.log(a); function dFunc() { var f = 5; console.log(f) console.log(c); console.log(a); } dFunc(); } cFunc(10);
当cFunc被调用时,cFunc的做用域链以下
Scope chain of cFunc = [ cFunc variable object, Global Execution Context variable object]
当dFunc被调用时,dFunc在cFunc中,dFunc的做用域链包含dFunc、cFunc和全局可变对象
Scope chain of dFunc = [dFunc variable object, cFunc variable object, Global execution context variable object]
当咱们尝试访问dFunc中的f,js引擎查看f是否可从dFunc的可变对象中获取,找到console输出;
访问c变量,js引擎首先在dFunc的可变对象中获取,不能获取,则到cFunc的可变对象中去获取,找到console输出;
访问a变量,同上,最后找到GEC的可变对象,获取到并console输出
一样,cFunc中获取c和a相似
在cFunc中访问不到f变量,但dFunc中能够经过做用域链获取到c和d