一、介绍
本篇继上一篇深入理解js执行--单线程的JS,这次我们来深入了解js执行过程中的执行上下文。
本篇涉及到的名词:预执行,执行上下文,变量对象,活动对象,作用域链,this等
二、预执行
在上一篇说到,在js代码被执行,执行上下文会被压进执行栈中,但是在此之前还有一步工作要做,就是创建好执行上下文,因为创建好才能被压进去啊。
创建执行上下文就是预执行过程: 接下来说说创建执行上下文的细节部分。
三、创建执行上下文
(1)执行上下文组成
执行上下文:也叫一个执行环境,有全局执行环境和函数执行环境两种。每个执行环境中包含这三部分:变量对象/活动对象,作用域链,this的值
代码模拟
//可以把执行上下文看作一个对象exeContext = {
VO = [...], //VO代表变量对象,保存变量和函数声明
scopeChain = [...] //作用域链
thisValue = {...} //this的值}
创建执行上下文就是创建变量对象,作用域链和this过程
接下来就分别细说创建变量对象/活动对象,作用域链,this值的过程。
(2)变量对象(variable object)
变量对象中存储了在上下文(环境)中定义的变量和函数声明
创建变量对象(VO)时就是将各种变量和函数声明进行提升的环节:
//用下面代码为例子console.log(a)console.log(b)console.log(c)console.log(d)var a = 100
b = 10function c(){}var d = function(){}
上述代码的变量对象:
//这里用VO表示变量对象VO = {
a = undefined//有a,a使用var声明,值会被赋值为undefined
//没有b,因为b没用var声明
c = function c (){} //有c,c是函数声明,并且c指向该函数
d = undefined//有d,d用var声明,值会被赋值为undefined}
解说:执行上述代码的时候,会创建一个全局执行上下文,上下文中包含上面变量对象,创建完执行上下文后,这个执行上下文才会被压进执行栈中。开始执行后,因为js代码一步一步被执行,后面赋值的代码还没被执行到,所以使用console.log函数打印各个变量的值是变量对象中的值。
在运行到第二行时会报错(报错后就不再执行了),因为没有b(b is no defined)。把第二行注释掉后,再执行各个结果就是VO里面的对应的值。
讲到这里我想大家对变量对象理解了吧,以及对变量提升和函数提升有个深入了解。
(3)活动对象(activation object)
活动对象是在函数执行上下文里面的,其实也是变量对象,只是它需要在函数被调用时才被激活,而且初始化arguments,激活后就是看做变量对象执行上面一样的步骤。
//例子function fn(name){ var age = 3 console.log(name)
}
fn('ry')
当上面的函数fn被调用,就会创建一个执行上下文,同时活动对象被激活
//活动对象AO = { arguments : {0:'ry'}, //arguments的值初始化为传入的参数
name : ry, //形参初始化为传进来的值
age : undefined //var 声明的age,赋值为undefined}
活动对象其实也是变量对象,做着同样的工作。其实不管变量还是活动对象,这里都表明了,全局执行和函数执行时都有一个变量对象来储存着该上下文(环境内)定义的变量和函数。
(4)作用域链(scope chain)
在创建执行上下文时还要创建一个重要的东西,就是作用域链。每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。
创建作用域链过程:
//以本段代码为例function fn(a,b){ var x = 'string',
}
fn(1,2)
1.函数被调用前,初始化function fn,fn有个私有属性[[scope]],它会被初始化为当前全局的作用域,fn.[[scope]="globalScope"。
2.调用函数fn(1,2),开始创建fn执行上下文,同时创建作用域链fn.scopeChain = [fn.[[scope]]],此时作用域链中有全局作用域。
3.fn活动对象AO被初始化后,把活动对象作为变量对象推到作用域链前端,此时fn.scopeChain = [fn.AO,fn.[[scope]]],构建完成,此时作用域链中有两个值,一个当前活动对象,一个全局作用域。
fn的作用域链构建完成,作用域链中有两个值,第一个是fn函数自身的活动对象,能访问自身的变量,还有一个是全局作用域,所以fn能访问外部的变量。这里就说明了为什么函数中能够访问函数外部的变量,因为有作用域链,在自身找不到就顺着作用域链往上找。
(5)this的值
上面说过执行上下文有两种,一个全局执行上下文,一个函数执行上下,下面分别说说这两种上下文的this。
a.全局执行上下文的this
指向window全局对象
b.函数执行上下文的this(主要讲函数的this)
在《JavaScript权威指南》中有这么几句话:
1.this是关键字,不是变量,不是属性名,js语法不允许给this赋值。
2.关键字this没有作用域限制,嵌套的函数不会从调用它的函数中继承this。
3.如果嵌套函数作为方法调用,其this指向调用它的对象。
4.如果嵌套函数作为函数调用,其this值是window(非严格模式),或undefined(严格模式下)。
解读一下: 上面说的概括了this两种值的情况:
1.函数直接作为某对象的方法被调用则函数的this指向该对象。
2.函数作为函数直接独立调用(不是某对象的方法),或是函数中的函数,其this指向window。
我们看几个栗子便可理解:
栗子1:(这个例子我相信都能理解)当函数被独立运行时,其this的值指向window对象。
function a(){ console.log(this)
}//独立运行a() //window
栗子2:(函数中函数,这里嵌套了个外围函数)这里也是指向window对象,也相当于函数作为函数调用,就是独立运行。其实这个例子也说明闭包的this指向Window。
//外围函数function a(){ //b函数在里面
function b(){ console.log(this)
} //虽然在函数中,但b函数独立运行,不是那个对象的方法
b()
}
a() //window
栗子3:(再写复杂点的话)x函数即使在对象里面,但它是函数中的函数,也是作为函数运行,不是Object的方法。getName才是objcet的方法,所以getName的this指向object(在下个栗子有)。
//一个对象var object = { //getName是Object的方法
getName : function(){ //x是getName里面的函数,它是作为函数调用的,this就是window啦
function x(){ console.log(this)
}
x()
}
}
object.getName() //window
以上三个都是输出window,下面是this指向某个对象的情况。
栗子4:函数作为某个对象的方法被调用。
//一个对象var object = {
name : "object", //getName是Object的方法
getName : function(){
console.log(this === object)
}
}object.getName()//true , 说明this指向了object
这里的getName中的this是指向objct对象的,因为getName是object的一个方法,它作为对象方法被调用。
栗子5:再来个栗子。
var name = "window"var obj = {
name : "obj"}function fn (){ console.log(this.name)
}//将fn通过call或bind或apply直接绑定给obj,从而成为obj的方法。fn.call(obj) //obj
再总结一下this的值
全局执行上下文:this的值是window
函数执行上下文:this的值两种:
1.函数中this指向某对象,因为函数作为对象的方法:怎么看函数是对象的方法,一种是直接写在对象里面(不是嵌套在对象方法中的函数,不懂再看看栗子3),另一种是通过call等方法直接绑定在对象中。
2.函数中this指向window:函数独立运行,不是对象的方法,函数中的函数(闭包),其this指向window。
四、总结整个js代码执行过程
(1)JS执行过程
js代码执行分成了两部分:预执行和执行
预执行:创建好执行上下文,有两种,一种是开始执行js代码就创建全局的执行上下文,一种是当某个函数被调用时创建它自己的函数执行上下文。这里也就是本节主要讲的东西,创建执行上下文的三个重要成分。
执行:在执行栈中执行,栈顶的执行上下文获得执行权,并按顺序执行当前上下文中的代码,执行完后弹栈销毁上下文,执行权交给下一个栈顶执行上下文。
(2)放上图示
某个执行上下文生命周期:
五、后话
整个js的执行过程就这样了,一开始可能有点难理解,但看多几遍就慢慢领会了。希望大家能够理解。如果觉得写得好,记得点赞,关注哦。
执行上下文每当控制器到达ECMAScript可执行代码的时候,控制器就进入了一个执行上下文.
执行上下文是个抽象概念,标准中没有从技术实现上定义执行上下文的具体结构和类型.
就是一系列活动的执行上下文从逻辑上形成一个栈(比较抽象).
栈底总是全局上下文,栈顶是当前(活动的)执行上下文.
当在不同的执行上下文间切换(退出而进入新的执行上下文)的时候,栈会被修改(通过压栈或者出栈的形式).
变量对象
执行上下文的数据是以变量对象的属性形式进行存储的.
一个变量对象(简写为VO)是一个和执行上下文相关的特别对象,存储以下内容:
变量(声明的变量,var)
函数声明(简写为FD)
在上下文中,函数声明的形式参数
作用域链
作用域链是一条变量对象的链,它和执行上下文有关,用于在处理标识符的时候进行变量查询.
函数上下文的作用域链在函数调用的时候创建出来,它包含了活跃对象和该函数的内部[[Scope]]属性.
执行上下文变量大致如下:
activeExecutionContext = {
VO:{...},//或者AO
this:thisValue,
Scope:[
//作用域链,所有变量对象的列表,用来查询标识符
]
}
上面Scope可以定义如下:
Scope = AO+[[Scope]]
可以用数组进行表示:
var Scope = [VO1,VO2,...,VOn]//作用域链
简单的说:第一种是构造函数式,即通过new运算符调用构造函数Function来创建函数
第二种不是实例化,只是调用函数把返回值赋给变量。
扩展:
JavaScript 中并没有真正的类,但JavaScript 中有构造函数和new 运算符。构造函数用来给实例对象初始化属性和值。任何JavaScript 函数都可以用做构造函数,构造函数必须使用new 运算符作为前缀来创建新的实例。
new 运算符改变了函数的执行上下文,同时改变了return 语句的行为。实际上,使用new和构造函数很类似于传统的实现了类的语言:
// 实例化一个Me
var alice = new Me('alice', 18, 'Coder')
// 检查这个实例
assert( alice instanceof Me )
构造函数的命名通常使用驼峰命名法,首字母大写,以此和普通的函数区分开来,这是
一种习惯用法。
// 不要这么做!
Me('alice', 18, 'Coder')//=>undefined
这个函数只会返回undefined,并且执行上下文是window(全局)对象,无意间创建了3个全局变量name,age,job。调用构造函数时不要丢掉new 关键字。
当使用new 关键字来调用构造函数时,执行上下文从全局对象(window)变成一个空的上下文,这个上下文代表了新生成的实例。因此,this 关键字指向当前创建的实例。尽管理解起来有些绕,实际上其他语言内置类机制的实现也是如此。
默认情况下,如果你的构造函数中没有返回任何内容,就会返回this——当前的上下文。
要不然就返回任意非原始类型的值.