执行上下文 能够理解为当前代码的执行环境,同一个函数在不一样的环境中执行,会由于访问数据的不一样产生不同的结果。
执行上下文分为三种:前端
window
对象),使this
指向这个全局对象eval
函数中的代码时建立的执行上下文,少用且不建议使用执行上下文栈(Execution context stack,ECS),也叫函数调用栈(call stack
),是一种拥有 LIFO
(后进先出)数据结构的栈,用于存储代码执行时建立的执行上下文数组
因为JS是单线程的,每次只能作一件事情,经过这种机制,咱们可以追踪到哪一个函数正在执行,其余函数在调用栈中排队等待执行。浏览器
JS引擎第一次执行脚本时,会建立一个全局执行上下文压到栈顶,而后随着每次函数的调用都会建立一个新的执行上下文放入到栈顶中,随着函数执行完毕后被执行上下文栈顶弹出,直到回到全局的执行上下文中。数据结构
代码实例闭包
var color = 'blue'; function changeColor() { var anotherColor = 'red'; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor(); console.log(color); // red
执行过程能够在 devTool
的 call stack
中看到,其中 anonyomus
为全局上下文栈;其他为函数上下文栈app
图解: 函数
执行过程:this
全局执行上下文
,压入执行栈,其中的可执行代码开始执行。changeColor
函数,JS引擎中止执行全局执行上下文,激活函数 changeColor
建立它本身的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。changeColor
调用了 swapColors
函数,此时暂停了 changeColor
的执行上下文,建立了 swapColors
函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。swapColors
函数执行完后,其执行上下文从栈顶出栈,回到了 changeColor
执行上下文中继续执行。changeColor
没有可执行代码,也没有再遇到其余执行上下文了,将其执行上下文从栈顶出栈,回到了 全局执行上下文
中继续执行。全局执行上下文
。注意:函数中,遇到return能直接终止可执行代码的执行,所以会直接将当前上下文弹出栈。
使用 ECStack
来模拟调用栈:es5
ECStack=[]
JS第一次执行代码时就会遇到全局代码,执行上下文栈会压入一个全局上下文,咱们用 globalContext
表示它,只有当整个应用程序结束的时候,ECStack
才会被清空,因此 ECStack
最底部永远有个 globalContext
:spa
ECStack.push(globalContext)
使用伪代码模拟上述代码行为:
ECStack.push(<changeColor> functionContext); ECStack.push(<swapColors> functionContext); // swapColors出栈 ECStack.pop(); // changeColor出栈 ECStack.pop();
为了巩固一下执行上下文的理解,咱们再来绘制一个例子的演变过程,这是一个简单的闭包例子。
function f1() { var n = 999; function f2() { console.log(n); } return f2; } f1()() // 999
使用伪代码模拟上述代码行为:
ECStack.push(<f1> functionContext); // f1出栈 ECStack.pop(); ECStack.push(<f2> functionContext); // f2出栈 ECStack.pop();
由于f1中的函数f2在f1的可执行代码中,并无被调用执行,所以执行f1时,f2不会建立新的上下文,而直到f2执行时,才建立了一个新的。具体演变过程以下。
es3版本执行上下文内有三个重要属性:
能够将每一个执行上下文抽象为一个对象。
执行上下文的组成代码示例:
executionContextObj = { scopeChain: { /* 变量对象(variableObject)+ 全部父执行上下文的变量对象*/ }, [variableObject | activationObject]: { /*函数 arguments/参数,内部变量和函数声明 */ arguments, ... }, this: {} }
变量对象 是与执行上下文相联的数据做用域,用来存储上下文中定义的变量和函数声明。
不一样执行上下文中的变量对象也不同:
console.log(this) //window var a=1 //挂到window上的属性 console.log(window.a) //1 console.log(this.a) //1
活动对象就是变量对象,只不过处于不一样的状态和阶段而已。
对于 JavaScript
来讲做用域及做用域链的变量查询是经过存储在浏览器内存中的执行上下文实现的。当查找变量时,首先从当前上下文中的变量对象查找,若是没有就会往上查找父级做用域中的变量对象,最终找到全局上下文的变量对象,若是没有就报错。这样由多个执行上下文的变量对象构成的链表就叫作做用域链。
那么有同窗就有疑问了,做用域和执行上下文有什么 区别 呢 :
函数执行上下文是在调用函数时, 函数体代码执行以前建立,函数调用结束时就会自动释放。由于不一样的调用可能有不一样的参数:
var a = 10; function fn(x) { var a = 20; console.log(arguments) console.log(x) } fn(20) fn(10) // 不一样的调用可能有不一样的参数
而JavaScript采用的是词法做用域,fn 函数建立的做用域在函数定义时就已经肯定了;
关联 :
做用域只是一个“地盘”,其中没有变量,要经过做用域对应的执行上下文环境来获取变量的值,因此做用域是静态观念的,而执行上下文环境是动态的。也就是说,做用域只是用于划分你在这个做用域里面定义的变量的有效范围,出了这个做用域就无效。
同一个做用域下,对同一个函数的不一样的调用会产生不一样的执行上下文环境,继而产生不一样的变量的值,因此,做用域中变量的值是在执行过程当中肯定的,而做用域是在函数建立时就肯定的。
执行上下文的生命周期有三个阶段,分别是:
建立阶段
生成变量对象
执行阶段
生成变量对象
arguments
对象,给变量对象添加形参名称和值。VO
中,若是 VO
中已经有同名函数,那么就进行覆盖(重写引用指针)。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 这一步
创建做用域链
在执行期上下文的建立阶段,做用域链是在变量对象以后建立的。做用域链自己包含变量对象。
[[scope]]
表示,它里面保存父变量对象,因此[[scope]]
就是一条层级链。person.[[scope]] = [ globalContext.variableObject ]
personContext = { scopeChain:person.[[scope]] }
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的指向
若是当前函数被做为对象方法调用或使用 bind
、call
、apply
等 API
进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,不然默认为全局对象调用。
执行阶段 中,执行流进入函数而且在上下文中运行/解释代码,JS 引擎开始对定义的变量赋值、开始顺着做用域链访问变量、若是内部有函数调用就建立一个新的执行上下文压入执行栈并把控制权交出
此时代码从上到下执行的时候激活阶段的过程是:
console.log
; 此时 name
在 VO
中是函数。getName
未指定值在 VO
中的值是 undefined
。getName
被赋值成函数表达式,name
被赋值为 abby
console.log
; 此时的 name
因为函数被字符串赋值覆盖所以是 string
类型getName
是 function
类型。console.log
; 此时的 name
因为又被覆盖所以是 function
类型所以理解执行上下文以后很好解释了变量提高(Hoisting):实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被JavaScript引擎放入内存中
这就解释了为何咱们能在 name
声明以前访问它,为何以后的 name
的类型值发生了变化,为何 getName
第一次打印的时候是 undefined
等等问题了。
ES6 引入了 let
和 const
关键字,从而使 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();
一、执行全局代码,生成全局上下文,而且压入执行栈
ECStack=[ globalContext ] 复制代码
二、全局上下文初始化
globalContext={ variableObject:[global,scope,checkscope], this:globalContext.variableObject, scopeChain:[globalContext.variableObject] }
三、建立 checkscope 函数时生成内部属性 [[scope]],并将全局上下文做用域链存入其中
checkscope.[[scope]] = [ globalContext.variableObject ]
四、调用 checkscope 函数,建立函数上下文,压栈
ECStack=[ globalContext, checkscopeContext ]
五、此时 checkscope 函数还未执行,进入执行上下文
checkscopeContext = { activationObject: { arguments: { length: 0 }, scope: undefined, f: pointer, // reference to function f(), }, scopeChain: [activationObject, globalContext.variableObject], this: undefined }
六、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 ]
七、f 函数调用,生成 f 函数上下文,压栈
ECStack=[ globalContext, checkscopeContext, fContext ]
八、此时 f 函数还未执行,初始化执行上下文
fContext = { activationObject: { arguments: { length: 0 }, }, scopeChain: [fContext.activationObject, checkscopeContext.activationObject, globalContext.variableObject], this: undefined }
九、f 函数执行,沿着做用域链查找 scope 值,返回 scope 值
十、f 函数执行完毕,f函数上下文从执行上下文栈中弹出
ECStack=[ globalContext, checkscopeContext ]
十一、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
ECStack=[ globalContext ]
示例二:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
checkscope
函数时生成内部属性 [[scope]]
,并将全局上下文做用域链存入其中checkscope
函数,建立函数上下文,压栈此时 checkscope
函数还未执行,进入执行上下文
[[scope]]
属性建立做用域链arguments
属性建立活动对象checkscope
函数执行,对变量 scope
设值,f
函数被建立生成 [[scope]]
属性,并保存父做用域的做用域链f
函数调用,生成 f
函数上下文,压栈此时 f
函数还未执行,初始化执行上下文
[[scope]]
属性建立做用域链arguments
属性建立活动对象f
函数执行,沿着做用域链查找 scope
值,返回 scope
值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 规范去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。
es5 执行上下文的生命周期也包括三个阶段:建立阶段 → 执行阶段 → 回收阶段
建立阶段作了三件事:
伪代码大概以下:
ExecutionContext = { ThisBinding = <this value>, // 肯定this LexicalEnvironment = { ... }, // 词法环境 VariableEnvironment = { ... }, // 变量环境 }
ThisBinding
是和执行上下文绑定的,也就是说每一个执行上下文中都有一个 this
,与 es3
的this
并无什么区别,this
的值是在执行的时候才能确认,定义的时候不能确认
词法环境的结构以下:
GlobalExectionContext = { // 全局执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Object", // 全局环境 // 标识符绑定在这里 outer: <null> // 对外部环境的引用 } } FunctionExectionContext = { // 函数执行上下文 LexicalEnvironment: { // 词法环境 EnvironmentRecord: { // 环境记录 Type: "Declarative", // 函数环境 // 标识符绑定在这里 // 对外部环境的引用 outer: <Global or outer function environment reference> } }
能够看到词法环境有两种类型 :
null
。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this
的值指向这个全局对象。arguments
对象。对外部环境的引用能够是全局环境,也能够是包含内部函数的外部函数环境。词法环境有两个组件 :
环境记录器也有两种类型 :
在全局环境中使用 对象环境记录器,用来定义出如今全局上下文中的变量和函数的关系。
所以:
outer
值为 null
;outer
值为全局对象,或者为父级词法环境(做用域)变量环境也是一个词法环境,所以它具备上面定义的词法环境的全部属性。
在 ES6 中,词法环境和 变量环境的区别在于前者用于存储函数声明和变量( let
和const
关键字)绑定,然后者仅用于存储变量( 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: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 g: undefined }, outer: <GlobalLexicalEnvironment> } }
变量提高的缘由:在建立阶段,函数声明存储在环境中,而变量会被设置为 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
执行上下文出栈等待虚拟机回收执行上下文
对象环境记录器
,接着建立他的外部环境引用 outer
,值为 null声明式环境记录器
,接着建立他的外部环境引用 outer
,值为 null,值为全局对象,或者为父级词法环境将词法环境中 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(c) console.log(d) } foo()
第一步: 调用 foo
函数前先编译并建立执行上下文,在编译阶段将 var
声明的变量存放到变量环境中,let
声明的变量存放到词法环境中,须要注意的是此时在函数体内部块做用域中 let
声明的变量不会被存放到词法环境中,以下图所示 :
第二步: 继续执行代码,当执行到代码块里面时,变量环境中的 a 的值已经被设置为1,词法环境中 b 的值已经被设置成了2,此时函数的执行上下文如图所示:
从图中就能够看出,当进入函数的做用域块时,做用域块中经过 let
声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响做用域块外面的变量,所以示例中在函数体内块做用域中声明的变量的 b 与函数做用域中声明的变量 b 都是独立的存在。
在词法环境内部,实际上维护了一个小型栈结构,栈底是函数最外层的变量,进入一个做用域块后,就会把该做用域内部的变量压到栈顶;当该块级做用域执行完成以后,该做用域的信息就会从栈顶弹出,这就是词法环境的结构。
第三步: 当代码执行到做用域块中的 console.log(a)
时,就须要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,若是在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,若是没有查找到,那么继续在变量环境中查找。
这样一个变量查找过程就完成了,你能够参考下图:
第四步: 当函数体内块做用域执行结束以后,其内部变量就会从词法环境的栈顶弹出,此时执行上下文以下图所示:
第五步: 当foo函数执行完毕后执行栈将foo函数的执行上下文弹出。
因此,块级做用域就是经过词法环境的栈结构来实现的,而变量提高是经过变量环境来实现,经过这二者的结合,JavaScript 引擎也就同时支持了变量提高和块级做用域了。
outer 是一个外部引用,用来指向外部的执行上下文,其是由词法做用域指定的
function bar() { console.log(myName) } function foo() { var myName = " 极客邦 " bar() } var myName = " 极客时间 " foo()
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量, 好比上面那段代码在查找 myName 变量时,若是在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。为了直观理解,你能够看下面这张图:
从图中能够看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着若是在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。咱们把这个查找的链条就称为做用域链。 如今你知道变量是经过做用域链来查找的了,不过还有一个疑问没有解开,foo 函数调用的 bar 函数,那为何 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
这是由于在 JavaScript 执行过程当中,其做用域链是由词法做用域决定的。词法做用域指做用域是由代码中函数声明的位置来决定的,所以是静态的做用域
结合变量环境、词法环境以及做用域链,咱们看下下面的代码:
function bar() { var myName = " 极客世界 " let test1 = 100 if (1) { let myName = "Chrome 浏览器 " console.log(test) } } function foo() { var myName = " 极客邦 " let test = 2 { let test = 3 bar() } } var myName = " 极客时间 " let myAge = 10 let test = 1 foo()
对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的状况以下图所示:
解释下这个过程。首先是在 bar 函数的执行上下文中查找,但由于 bar 函数的执行上下文中没有定义 test 变量,因此根据词法做用域的规则,下一步就在 bar 函数的外部做用域中查找,也就是全局做用域。