Js代码在引擎中是以“一段一段”的方式来执行的,而非一行一行来分析执行的。而这“一段一段”的可执行代码无非三种:Global code, Function code, Eval code。这些可执行代码在执行的时候会创建一个一个的执行上下文。例如,当执行到一个函数的时候,js引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文。那么随着我们执行上下文数量的增加,js引擎又如何管理这些执行上下文呢?这时便有了执行上下文栈。
这里我们用一段贯穿全文的例子来讲解执行上下文的执行过程
var scope = 'global scope'
function checkScope(s) {
var scope = 'local scope'
function f() {
return scope
}
return f()
}
checkScope('scope')
当js引擎去解析代码的时候,最先碰到的就是global code,所以一开始初始化的时候便奖全局上下文推入执行上下文栈,并且只有整个应用程序执行完毕的时候,全局上下文才会推出执行上下文栈。
这里我们用ECS(Excution Context Stack)来模拟执行上下文栈,用glocalContext来表示全局执行上下文栈
// 1.在执行函数之前只有全局上下文
ECS = {
globalContext
}
// 2.当代码执行fn函数的时候,会创建fn函数的执行上下文,并将其压入执行上下文栈
ECS = {
fnContext,
globalContext
}
// 3.当代码执行f函数时,会创建f函数的执行上下文,并将其压入执行上下文栈
ECS = {
fContext,
fnContext,
globalContext
}
// 4.f函数执行完毕后,f函数的执行上下文出栈,随后fn函数执行完毕,fn函数的执行上下文出栈
ECS = {
// fContext 出栈
fnContext,
globalContext
}
ECS = {
// fnContext 出栈
globalContext
}
执行上下文的三个重要属性
变量对象是与执行上下文相关的数据作用域,储存了在上下文中定义的变量和函数声明。并且不同的执行上下文也有着不同的变量对象,这里分为全局上下文中的变量对象和函数执行上下文中的变量对象。
全局上下文中的变量对象其实就是全局对象。我们可以通过this来访问全局对象,并且在浏览器环境中this=window,在node环境中this=global。
在函数上下文中的变量对象,我们用活动对象来表示(activation object,这里简称ao),为什么称其为活动对象呢,因为只有进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。
在函数执行之前,会为当前函数创建上下文,并且在此时,会创建变量对象:
还是以刚才的代码为例:
var scope = 'global scope'
function checkScope(s) {
var scope = 'local scope'
function f() {
return scope
}
return f()
}
checkScope('scope')
在执行checkscope函数之前,会为其创建执行上下文,并初始化变量对象,此时的变量对象为:
VO = {
arguments: {
0: 'scope',
length: 1
},
s: 'scope',
f: pointer to function f,
scope: undefined
}
随着checkscope函数的执行,变量对象被激活,变量对象内的属性随着代码的执行而改变:
VO = {
arguments: {
0: 'scope',
length: 1
},
s: 'scope',
f: pointer to function f,
scope: 'local scope'
}
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直查到全局上下文的变量对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
下面还是用上面的例子来讲解作用域链:
首先在checkscope函数声明的时候,内部会绑定一个[[scope]]的内部属性:
checkscope.[[scope]] = [
globalContext.VO
]
接着在checkscope函数执行之前,创建checkscopeContext,并推入执行上下文栈:
checkscopeContext = {
scope: checkscope.[[scope]],
VO: {
arguments: {
0: 'scope',
length: 1
},
s: 'scope',
f: pointer to function f,
scope: undefined
},
this: globalContext.VO
}
// 将函数的变量对象压入作用域链的最顶端
checkscopeContext = {
scope: [VO, checkscope.[[scope]]],
VO: {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope',
f: pointer to function f,
scope: undefined
},
this: globalContext.VO
}
接着,随着函数的执行,修改变量对象:
checkscopeContext = {
scope: [VO, checkscope.[[scope]]],
VO: {
arguments: {
0: 'scope',
length: 1
},
s: 'scope',
f: pointer to function f,
scope: 'local scope'
},
this: globalContext.VO
}
与此同时遇到f函数声明,f函数绑定[[scope]]属性:
f.[[scope]] = [
checkscopeContext.VO, // f函数的作用域还包括checkscope的变量对象
globalContext.VO
]
之后f函数的步骤同checkscope函数。
再来一个经典的例子:
var data = []
for (var i = 0; i < 6; i++) {
data[i] = function() {
console.log(i)
}
}
data[0]() // 6
结果很简单,data的所有元素执行结果都是6。在这里我们用分析一下他的作用域链:
在data函数执行前,此时全局上下文的变量对象为:
globalContext.VO = {
data: [pointer to function (),...],
i: 6
}
每一个data的匿名函数的执行上下文大致如下:
data[n]context = {
scope: [VO, globalContext.VO],
VO: {
arguments: {
length: 0
}
},
this: globalContext.VO
}
那么在函数执行的时候,会先去自己匿名函数的变量对象中找i,发现未找到后沿着作用域链向上查找,找到了全局执行上下文的变量对象,而此时全局上下文变量对象中的i是6,所以每次打印都是6.
javascript这门语言是基于词法作用域来创建作用域的,也就是说一个函数的作用域在函数声明的时候就已经确定了,而不是函数执行的时候。
改一下之前的例子:
var scope = 'global scope'
function f () {
console.log(scope)
}
function checkscope() {
var scope = 'localscope'
f()
}
checkscope()
因为javascript是机遇词法作用域创建作用域的,所以打印的结果是global scope而不是local scope。我们结合上面的作用域链分析一下:
首先遇到f函数声明,此时为其绑定[[scope]]属性:
f.[[scope]] = [
globalContext.VO
]
f函数执行之前的执行上下文是:
fContext = {
scope: [VO, f.[[scope]]],
VO: {
arguments: {
length: 0
}
},
this: globalContext.VO
}
f函数的执行过程,先在f函数的执行上下文的变量对象中查找,未发现后到全局上下文的变量对象中查找,此时scope的值为global scope。
在这里this绑定也分为全局执行上下文和函数执行上下文
总结起来就是谁调用,this指向谁。
var name = 'window'
var obj1 = {
name: 'obj1',
fn1: function() {
console.log(this.name)
},
fn2: () => {
console.log(this.name)
},
fn3: function() {
return function() {
console.log(this.name)
}
},
fn4: function() {
return () => {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.fn1() // obj1
// 执行过程上下文为
//fn1Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: obj1
//}
// 由上下文可知this指向obj1,所以结果为obj1.name = 'obj1'
obj1.fn1.call(obj2) // obj2
// fn1Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: obj2
// }
// 可知this指向obj2,所以结果为obj2.name = 'obj2'
obj1.fn2() // window
obj1.fn2.call(obj2) // window
// 因为箭头函数没有自己的this,他的this永远指向父级执行上下文的this,那为什么上层执行上下文是globalContext.VO呢?有上下文可知javascript中的上下文分为全局执行上下文,函数执行上下文域eval执行上下文。而不管是全局执行上下文或函数执行上下文,大致都包含创建VO,确认作用域链,确认this指向三步。也就是说,this属于上下文中的一部分,很明显对象obj1并不是一个函数,它并没有权利创建自己的上下文,所以没有自己的this,那么他的外层是谁呢?当然是全局window了,所以这里的this指向window。箭头函数的this由外部环境决定,且一旦绑定无法通过call,apply或者bind再次改变箭头函数的this。所以这里虽然使用了call方法依旧无法修改,指向window
// fn2Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: globalContext.VO
// }
obj1.fn3()() // window
obj1.fn3().call(obj2) // obj2
obj1.fn3.call(obj2)() // window
// fn3返回一个闭包, 而它的this指向它的调用者,即obj1.fn3()返回的函数的调用者
// fn3returnFnContext = {
// scope: [VO, fn3Context.VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: 返回函数的调用者
// }
obj1.fn4()() // obj1
obj1.fn4().call(obj2) // obj1
obj1.fn4.call(obj2)() // obj2
// fn4返回一个闭包,只是这个闭包是一个箭头函数,而箭头函数没有自己的this,继承于父级this,所以返回函数的this指向fn4的this,即fn4的调用者
// fn4ReturnFnContext = {
// scope: [VO, fn4Context.VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: fn4的调用者
// }
总结:创建上下文只有三中方式,全局上下文/函数上下文/eval上下文。对象不是函数,不具备创建上下文的能力
var name = 'window'
function Person(name) {
this.name = name
this.fn1 = function() {
console.log('fn1:', name)
console.log(this.name)
}
this.fn2 = () => {
console.log('fn2:', name)
console.log(this.name)
}
this.fn3 = function() {
return function() {
console.log('fn3:', name)
console.log(this.name)
}
}
this.fn4 = function() {
return () => {
console.log('fn4:', name)
console.log(this.name)
}
}
}
var obj1 = new Person('obj1')
var obj2 = new Person('obj2')
obj1.fn1() // 'wondow'- 隐式绑定,this指向new出来的对象
obj1.fn1.call(obj2) // 'obj2' - 显式绑定,this指向绑定对象
obj1.fn2() // 'obj1' - 箭头函数没有自己的this,用的是上层上下文环境中的this
obj1.fn2.call(obj2) // 'obj1' - 箭头函数的this永远指向上层执行上下文中的this
obj1.fn3()() // 'window' - 返回闭包本质上被window调用,this被修改
obj1.fn3().call(obj2) // 'obj2' - 返回闭包后利用call方法显示改变this
obj1.fn3.call(obj2)() // 'window' - 返回闭包还是被window调用
obj1.fn4()() // 'obj1' - 返回闭包是箭头函数,this同样会指向obj1,虽然返回也是被window调用,但箭头函数无法被之间修改,还是指向obj1
obj1.fn4.call(obj2)() // 'obj2' - 箭头函数可通过修改外层执行上下文中的this指向来达到间接修改的目的
obj1.fn4().call(obj2) // 'obj1' - 返回的箭头函数无法被直接修改