执行上下文EC(Execution Context)
- 执行上下文就是一个函数或者全局代码运行时的环境,里面包括执行时需要用到的数据,可以理解为是一个对象;
- 函数每次执行都会创建一个新的执行上下文;
- 函数每次执行完毕都会销毁这个执行上下文;
执行栈CS(call stack)
- 每次有函数执行就会新生成一个执行上下文,组织管理执行上下文的栈就是执行栈;
- 当一个函数运行时,会在运行前生成一个执行上下文并入栈,函数运行结束就会出栈;
- 每次正在运行的函数使用的都是栈顶的上下文;
- 栈底永远是全局上下文。
举个例子:
function foo() {
console.log('foo函数执行了');
function bar() {
console.log('bar函数执行了');
}
bar();
}
foo();
代码执行时,执行栈如何变化
-
创建全局上下文
G_EC
,并入栈。执行栈:
G_EC
-
执行foo函数,创建
foo
的执行上下文foo_EC
, 并入栈。执行栈:
foo_EC
G_EC -
执行
console.log
函数,创建console.log
函数的执行上下文clg_EC
, 并入栈。执行栈:
clg_EC
foo_EC
G_EC -
console.log
执行完毕,打印出 'foo函数执行了',销毁clg_EC
,出栈。执行栈:
foo_EC
G_EC -
执行
bar
函数,创建bar
的执行上下文bar_EC
, 并入栈。执行栈:
bar_EC
foo_EC
G_EC -
执行
console.log
函数,创建console.log
函数的执行上下文clg_EC
, 并入栈。执行栈:
clg_EC
bar_EC
foo_EC
G_EC注意:这里的
clg_EC
是一个全新的上下文,和上一个不一样 -
console.log
执行完毕,打印出 'bar函数执行了',销毁clg_EC
,出栈。执行栈:
bar_EC
foo_EC
G_EC -
bar
函数执行完毕,销毁bar_EC
, 出栈。执行栈:
foo_EC
G_EC -
foo
函数执行完毕,销毁foo_EC
, 出栈。执行栈:
G_EC
-
全部代码执行完毕,销毁
G_EC
, 出栈。执行栈:
空
执行上下文中的内容
//用代码表示一个执行上下文
EC = {
VO = {...},
SC = [...],
this = [...]
}
1. 变量对象VO(Varibale Object)
- 在函数运行的前一刻生产;
- 执行栈顶部的VO,又被称之为执行对象AO(Active Object);
- 执行栈底部的VO,又被称之为全局对象GO(Global Object)。
2. 作用域链SC(Scope Chain)
- 作用域链(Scope Chain)是一个数组,是执行上下文的集合,每个函数都有一个[[scope]]属性,指向一个数组,数组中保存的是除自己的执行上下文以外的其他执行上下文;
- 作用域链是在函数定义时就产生的;
- 当一个函数要使用一个变量时,会先从自己的执行上下文中查找,如果找不到,就会沿着作用域链往上找。
3. this
不影响执行作上下文的理解,可跳过。
this
的指向问题:
-
全局环境中,
this
指向window
对象console.log(this); //window
-
函数中的
this
,指向window
(严格模式中指向undefined
)function foo(){ console.log(this); //window } foo();
-
使用
call
、apply
调用,this
指向call
/apply
的第一个参数var obj = {a: 1}; function foo(){ console.log(this); //obj } foo.call(obj); foo.apply(obj);
-
调用对象中的函数,使用
obj.fun
方式调用,this
指向obj
var obj = { a: 1, foo: function(){ //obj.foo():this指向obj, //bar():相当于将函数放在全局中执行,this指向window console.log(this); } }; obj.foo(); var bar = obj.foo; bar();
VO的创建过程
在一个函数的VO创建时,js引擎做的事情
- 确定形参的值
- 变量的声明提升
- 将实参的值赋给形参
- 函数声明整体提升
遇到同名属性则覆盖
举个例子:
function foo(a, b){
console.log(bar); //ƒ bar(){}
console.log(a); //2
function bar(){}
var a = 1;
}
foo(2, 3);
按照创建步骤生成foo函数的VO——foo_VO
//1. 确定形参的值,一开始有两个形参的值
foo_VO = {
arguments: {...}, //arguments一开始就会在
a: undefined,
b: undefined,
}
//2. 变量声明提升, 内部有一个变量声明a,当前VO对象已经有a属性,所以不变
foo_VO = {
arguments: {...},
a: undefined,
b: undefined,
}
//3. 将实参的值赋给形参,执行foo(2, 3)时传入了实参2, 3,分别赋值给a, b
foo_VO = {
arguments: {...},
a: 2,
b: 3,
}
//4. 函数声明提升, 有一个函数foo,将foo函数作为VO的属性
foo_VO = {
arguments: {...},
a: 2,
b: 3,
foo: function(){}
}
所以最后foo函数产生的VO对象就是
foo_VO = {
arguments: {...},
a: 2,
b: 3,
foo: function(){}
}
因为VO的创建是在函数执行前的,函数在运行的时候就可以在当前VO中查找变量,所以这就解释了为什么console.log
放在函数的最前面也可以打印a
和foo
的值。
SC的内容
SC = 上一层执行上下文栈的AO + 上一层执行上下文栈的SC
举个例子:
function foo() {
function bar() {
function baz(){
}
}
}
全局执行上下文,一开始的SC为空
g_EC = {
SC: [],
AO: {...},
this: {...},
}
foo函数执行上下文,其中SC = 全局上下文的AO + 全局上下文的SC
foo_EC = {
SC: [g_EC.AO], //[g_EC.AO, ...g_EC.SC]
AO: {...},
this: {...},
}
bar函数执行上下文,其中SC = foo函数执行上下文的AO + foo函数执行上下文的SC
bar_EC = {
SC: [foo_SC.AO, g_EC.AO], //[foo_SC.AO, ...foo_SC.SC],
AO: {...},
this: {...},
}
baz函数执行上下文,其中SC = bar函数执行上下文的AO + bar函数执行上下文的SC
baz_EC = {
SC: [bar_SC.AO, foo_SC.AO, g_EC.AO], //[bar_SC.AO, ...bar_SC.SC],
AO: {...},
this: {...},
}
函数运行时,查找变量,会先查找自己的AO。如果没有,再依次沿着SC的第0项、第1项... 往后找。找到SC的最后一项都没有找到就会报错:
Uncaught ReferenceError: xxx is not defined
闭包
当一个函数中的子函数被保存到了该函数的外部就会形成闭包。
function foo(){
var a = 1;
return function bar(){
return a;
}
}
var baz = foo();
var qux = baz();
console.log(qux); //1
foo
在运行的时候
foo_EC = {
SC: [GO],
AO: {
a: 1,
...
},
this: {...},
}
当foo
运行完毕后会销毁自己的执行上下文,其中的AO也被销毁
但是由于bar
被保存到了外部, 也就是baz
中,而bar
的作用域SC中有foo
的AO,所以这就解释了为什么形成闭包时,外部的函数可以使用函数内部的变量。
baz_EC = {
SC: [foo_EC.AO, GO],
AO: {...},
this: {...},
}
因为bar
函数被保存到全局作用域中,其中的foo
的AO一直存在,无法被销毁,这就会造成内存泄露;在使用完毕后应该去掉原来的闭包baz = null
闭包的作用:
- 实现公有变量;
- 可以做缓存;
- 实现属性私有化;
- 模块化开发,防止污染全局变量。