执行上下文
执行上下文(Exceution Context
),每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,js 中的运行环境大概包括三种情况:
- 全局环境:js 代码运行起来会首先进入该环境;
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码;
- eval(不建议使用,可忽略)。
一个程序中一般有多个执行上下文,js 使用栈的方式管理这些执行上下文(call stack
),栈底永远是全局,栈顶永远是当前的执行上下文。每当执行到一个函数时,会讲这个函数的执行上下文加入栈顶,执行完毕后再推出。浏览器关闭后,全局执行上下文也推出,栈被清空。
每个执行上下文都有一系列的属性,主要有如下 3 个:
- 变量对象(
VO
)- 作用域链(
Scope Chain
)- this
变量对象(以下简称 VO)
以下例中 func 的执行上下文的生命周期为例:
function func(p1, p2, p3) {
console.log(p1);
console.log(p2);
console.log(p3);
console.log(a);
console.log(b);
console.log(p2);
var a = 'a';
var b = 'b';
function p2() {};
function b() {};
}
func('p1', 'p2');
1. 创建阶段
该阶段创建VO
,VO
的属性不可访问。VO
中有具体包含如下属性:
- 函数的所有形参为
key
,对应的实参(未传为undefined
)为value
;一个arguments
对象;
// p3实参未传入,所以p3在VO中为undefined
VO = {
arguments: {
0: 'p1',
length: 1
},
p1: 'p1',
p2: 'p2',
p3: undefined
}
- 函数声明,即
function b() {}
这种,函数名为key
,方法作为value
;如果函数名与形参重复则覆盖形参
// 形参p2和函数声明p2重复,形参被覆盖
VO = {
arguments: {
0: 'p1',
length: 1
},
p1: 'p1',
p2: f p2(),
p3: undefined
b: f b(),
}
- 变量声明,即
var
定义的变量,变量名为key
,undefined
为value
如果和函数声明或者形参重复将被忽略
// 变量声明初始值都为undefined
VO = {
arguments: {
0: 'p1',
length: 1
},
p1: 'p1',
p2: f p2(),
p3: undefined
b: f b(),
a: undefined
}
根据以上的分析,示例代码在上下文创建阶段将变为如下:
2. 代码执行阶段
该执行上下文的VO
会变成活动对象(以下简称AO
,当前执行上下文的VO
就称为AO
,同一个东西在两种环境下的不同叫法),里面的属性都可以访问,将会按顺序执行代码,完成变量赋值,以及执行其他代码;上述例子执行分析:
// 代码执行前AO:
VO = {
arguments: {
0: 'p1',
length: 1
},
p1: 'p1',
p2: f p2(),
p3: undefined
b: f b(),
a: undefined
}
// 执行代码,
function func(p1, p2, p3) {
// 以下打印的值都是从AO中读取的
console.log(p1); // AO.p1 = 'p1'
console.log(p2); // AO.p2 = f p2()
console.log(p3); // AO.p3 = undefined
console.log(a); // AO.a = undefined
console.log(b); // AO.b = f b()
var a = 'a'; // 重新赋值 AO.a = 'a'
var b = 'b'; // 重新赋值 AO.b = 'b'
function p2() {};
function b() {};
console.log(p1); // AO.p1 = 'p1'
console.log(p2); // AO.p2 = f p2()
console.log(p3); // AO.p3 = undefined
console.log(a); // AO.a = 'a'
console.log(b); // AO.b = 'b'
}
func('p1', 'p2');
// 代码执行后AO:
VO = {
arguments: {
0: 'p1',
length: 1
},
p1: 'p1',
p2: f p2(),
p3: undefined
b: f b(),
a: 'a'
}
掌握了以上函数创建执行时的原理,再也不怕这种面试题啦!
3. 销毁阶段
可执行代码执行完毕之后,执行上下文出栈,对应的内存空间失去引用,等待被回收。
作用域和作用域链
作用域: 决定了代码区块中变量和其他资源的可见性。js 中采用的是词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,如下例:
var a = 2;
function foo(){
console.log(a)
}
function bar(){
var a = 3;
foo(); // bar的VO不在foo的作用域链上,也证明了每个函数都有自己独立的作用域链
}
n(); // 输出的是2,不是3
作用域链: 函数被调用时,除了创建 VO
,还创建了属性 [[scope]]
会保存所有的父级VO
。一般情况下当前执行上下文的变量取值会到当前上下文的VO
中取值。但是如果在当前VO
中没有查到值,就会去[[scope]]
中查找,直到查到全局作用域,这个查找过程形成的链条就叫做作用域链。
this
this 指向则主要看是谁调用的该函数(或者说函数调用时被哪个对象所拥有)。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数,如下例:
function outer() {
const a = 1;
return function () {
console.log(a);
}
}
const n = outer();
我们来分析一下,outer
函数返回一个匿名函数,该函数的作用域链([[scope]]
)里包含了outer
函数的VO
,outer
函数执行完毕后,他的作用域链被销毁,但是他的VO
还在变量n
指向的匿名函数的作用链中,这也是为什么还能访问到数据a
的原因,直到变量n
被回收。
使用场景:节流防抖函数封装、vuejs
响应式原理将Dep
对象一直持有在属性的get
和set
方法的作用域中...