这篇文章会集合介绍关于js运行过程中的VO
、AO
、形参、实参arguments
、执行上下文EC
、作用域scope
、作用域链scopeChain
、闭包等概念,并以画图或代码方式串起来说明js
的执行过程。
- ECStack:执行栈,所有的代码都要放到执行栈中执行
- EC:
excution context
,执行上下文,当执行一个块级作用域({}中有let,const,function定义
)或函数时,会形成一个新的执行环境,这个环境就被称为执行上下文 - VO:
avarible object
,变量对象,存储当前上下文中的变量 - AO:
actived object
,活动的变量对象,当函数执行时,VO=>AO
,一般函数执行生成的变量对象,称之为AO
- 实参:函数执行时,实际传给函数的参数集合
arguments
- 形参:在函数定义时,定义的参数名
- 作用域:所在的执行上下文中的变量对象
VO/AO
- 作用域链:如果查找一个变量,会先在自己的执行上下文的
AO
对象中查找,如果没有,会到所在的执行上下文中的AO
对象中查找,如果再没有,就再找所在的执行上下文的上一级执行上下文中的AO
对象中查找,直到找到变量或找到全局VO
对象为止,这条链,我们就称之为作用域链 - 闭包:最常见的说法是内部函数调用外部函数的变量
在js
代码执行过程中,会生成一个执行栈Ecstack
,所有的代码执行都会在这个栈中进行:
- 在执行过程中先在全局创建一个全局执行上下文
EC(G)
,当执行全局代码时,会将当前栈压入执行栈Ecstack
,也就是入栈; - 当运行到一个
{}
代码片段或者一个函数,就会创建对应的执行上下文EC(xx)
,并将其入栈。 - 执行栈按照先进后出的特性,执行代码,当执行完当前代码后,会将当前执行上下文销毁,并将其移出执行栈,也就是出栈,依次执行并进行出栈,直至执行栈为空为止。
- 如果有些代码执行完后不释放,就会将此栈压入栈的最底层,等待被其它栈调用,这也就是我们常说的闭包。
接下来,会详细说明每个过程中会做到的事:
函数声明(创建)时
函数声明(创建)时会做以下两件事:
- 创建一个堆内存,这个堆内存中会存放:
- 代码字符串,即将要执行的代码部分以字符串形式存储
- 使用键值对存储方法对象
- 初始化当前函数的作用域
当前函数的作用域:[[scope]] = 函数所在的执行上下文中的变量对象VO/AO
如下,在全局定义一个函数fn
,在浏览器中可以看到对象上的键值对信息,因为它是在全局环境下创建的,所以[[scope]]
指向的是全局执行上下文中的VO
变量对象window
function fn(x) {
console.log(x)
}
// 比如AAAFFF000堆内存:
// 1. 代码字符串:
// "console.log(x)"
// 2. 键值对存储方法对象等:
// length: 1
// name: "fn"
// arguments: null
// caller: null
// ...
image.png
声明对象时
创建一个堆内存,使用键值对存储对象:
let a = {
n: 1
}
// 比如,AAAFFF000堆内存:
// n: 1
函数执行时
- 创建一个新的执行上下文
EC(函数)
,并将这个执行上下文压到执行栈ECStack
中执行 - 初始化
this
的指向 - 初始化作用域链(本质上是一个链表):
[[scopeChain]]:当前上下文AO => 所在的上下文AO => 再上一级上下文AO => ... => 直到全局上下文中的VO(G)
- 创建
AO
变量对象用来存储变量:
a. 初始化实参集合arguments
b. 创建形参变量并且赋值
c. 执行代码
需要注意的是,在非严格模式下,a.b
步骤会建立映射关系;在严格模式下则不会。ES6
的 箭头函数中没有arguments
实参集合
// 非严格模式
function fn(x, y) {
// 1. 实参集合初始化:arguments: {0:10, 1:20}
// 2. 形参变量和赋值:
// x = 10, y = 20
// 形参映射:x = 10, y = 20 => arguments
console.log(x, y, arguments); // 10 20 [Arguments] { '0': 10, '1': 20 }
// 3. arguments[0] = 100 => arguments: {0:100, 1:20} => x = 100
arguments[0] = 100;
// 4. y = 200 => arguments: {0:100, 2:200}
y = 200;
console.log(x, y, arguments); // 100 200 [Arguments] { '0': 100, '1': 200 }
}
fn(10, 20)
// 严格模式
function fn(x, y) {
"use strict"
// 1. 实参集合初始化:arguments: {0:10, 1:20}
// 2. 形参变量和赋值:
// x = 10, y = 20
// 无形参映射
console.log(x, y, arguments); // 10 20 [Arguments] { '0': 10, '1': 20 }
// 3. arguments[0] = 100 => arguments: {0:100, 1:20}
arguments[0] = 100;
// 4. y = 200
y = 200;
console.log(x, y, arguments); // 10 200 [Arguments] { '0': 100, '1': 20 }
}
fn(10, 20)
来几道面试题实践
1. 360面试题
// 1. x => BBBFFF000:
// 0: 12
// 1: 23
// length: 2
let x = [12, 23]
// 2. fn => AAAFFF000
function fn(y) {
// 4. 初始化实参集合:
// x => arguments: [[12,23]] => BBBFFF000(0: 12, 1: 23, length: 2)
// 5. 创建形参并赋值:
// 映射:y => x => BBBFFF000(0: 12, 1: 23, length: 2)
// 6. y => x => BBBFFF000(0: 100, 1: 23, length: 2)
y[0] = 100
// 7. 指向一个新的堆内存:CCCFFF000(0: 100, length: 1)
// y => [100] => CCCFFF000(0: 100, length: 1)
y = [100]
// 8. CCCFFF000之前没有1的索引,所以CCCFFF000添加此索引(0: 100, 1: 200, length: 2)
// y => [100, 200] => CCCFFF000(0: 100, 1: 200, length: 2)
y[1] = 200;
console.log(y); // [ 100, 200 ]
}
// 3. 执行fn
fn(x)
console.log(x); // x指向的是arguments,也就是BBBFFF000:[ 100, 23 ]
2. 输出以下结果
var x = 10
~function(x) {
console.log(x); // undefined
x = x || 20 && 30 || 40
console.log(x); // 30
}();
console.log(x); // 10
分析:
-
js
代码执行时,会创建一个ECStack
执行栈,用于执行代码 -
创建全局执行上下文
创建全局执行上下文EC(G)
,并创建其VO
对象,有一个全局变量x
和自执行函数的堆内存AAAFFF000
,并将EC(G)
入栈
-
自执行函数,创建自执行函数的执行上下文
创建自执行函数的执行上下文EC(自执行函数)
,并为其创建AO
对象,其声明过程,包括实参初始化和形参赋值,因为没有实参,所以实参arguments:{}
,形参x
没有赋值,所以是undefined
,并将这个执行上下文入栈
-
自执行函数开始执行,
console.log(x) => undefined
,接着执行x = x || 20 && 30 || 40
赋值,这里考察的是运算符的计算和优化级问题:-
A || B
:如果A
为真,返回A
,否则返回B
-
A && B
:如果A
为真,返回B
,否则返回A
- 优先级:
&&
运算符 优先级高于||
所以x = x || 20 && 30 || 40 => x = undefined || 30 || 40 => x = 30
,执行console.log(x) => 30
执行自执行函数
-
-
自执行函数完成后,将其执行上下文出栈,并销毁
出栈 -
继续执行全局上下文
image.pngconsole.log(x)
,输出10
-
全局执行上下文
EC(G)
出栈,清空执行栈
3. 输出以下结果
let x = [1, 2], y= [3, 4]
~function(x) {
x.push('a')
x = x.slice(0)
x.push('b')
x = y;
x.push('c')
console.log(x, y); // [ 3, 4, 'c' ] [ 3, 4, 'c' ]
}(x)
console.log(x, y); // [ 1, 2, 'a' ] [ 3, 4, 'c' ]
分析:
- 最初,执行栈ECStack中,创建全局执行上下文
EC(G)
入栈,并开始创建全局变量对象VO(G)
:
a.x => AAAFFF000({0: 1, 1: 2, length: 2})
,
b.y => AAAFFF111({0:3, 1: 4, length: 2})
,
c.自执行函数=> AAAFFF222
d. 自执行函数的作用域:[[scope]] = VO(G)
- 开始执行自执行函数,生成
EC(自执行函数)
,并将其压入执行栈 - 初始化自执行函数的作用域链:
[[scopeChain]]:AO(自执行函数) => VO(G)
- 开始创建
AO
对象:
a. 实参初始化arguments => AAAFFF000({0: 1, 1: 2, length: 2})
,
b. 创建形参并赋值,x => arguments => AAAFFF000({0: 1, 1: 2, length: 2})
c. 开始执行函数的代码串 - 执行
x.push('a')
,x => arguments => AAAFFF000({0: 1, 1: 2, 3: 'a', length: 3})
- 执行
x = x.slice(0)
,x.slice(0)会创建一个新的堆内存空间,值拷贝x,x => AAAFFF333({0: 1, 1: 2, 3: 'a', length: 3})
- 执行
x.push('b')
,EC(自执行函数)
中私有x,x => AAAFFF333({0: 1, 1: 2, 3: 'a', 4: 'b', length: 4})
- 执行
x = y;
,EC(自执行函数)
中因为自己的VO
对象中没有私有变量y
,所以在其作用域链[[scopeChain]] = <VO(自执行函数),VO(G)>
中找向上一级作用域VO(G)
中查找y
,发现全局VO(G)
中有y
,所以私有x => y => AAAFFF111({0:3, 1: 4, length: 2})
- 执行
x.push('c')
,x => y => AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3})
- 执行
console.log(x, y)
,打印的是EC(自执行函数)
中私有变量x和y
,此时它们都指向AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3})
,所以值为[ 3, 4, 'c' ]
- 自执行函数执行完毕,出栈,执行
EC(G)
中的console.log(x, y)
,全局中的x => arguments => AAAFFF000({0: 1, 1: 2, 3: 'a', length: 3})
,y => AAAFFF111({0: 3, 1: 4, 3: 'c', length: 3})
,所以输出[ 1, 2, 'a' ] [ 3, 4, 'c' ]
- 全局执行上下文中代码执行完成,出栈,此时清空了执行栈
4. 关于闭包题目
function A(y) {
let x = 2;
function B(z) {
console.log(x + y + z); // 7
}
return B
}
let C = A(2)
C(3)
分析:
-
在执行栈中
image.pngECStack
,先创建全局执行上下文EC(G)
,并将其入栈,并创建全局变量对象VO(G)
:A =>AAAFFF000
,并初始化其作用域,即指向其所在的VO对象:A[[scope]]: VO(G)
-
执行代码
image.pnglet C = A(2)
,执行了A(2)
,所以会创建一个新的执行上下文EC(A)
,并入栈:
1) 初始化其this
指向,因为没有对象调用它,所以它指向全局对象window
2) 初始化其作用域链,指向其作用域 :A[[scopeChain]] = VO(G)
3) 创建AO(A)
对象: 初始实参、形参赋值、执行内部代码
-
执行内部代码时,会发现
B
的值(AAAFFF111
)返回出去被C
占用了,也就是C指向了AO(A)
中的堆内存在,导致EC(A)
不能出栈销毁,否则C
就找不到它了,这就形成了闭包,闭包有两个作用:
1) 因为B
的值(AAAFFF111
),其实只跟EC(A)
环境有关,跟EC(G)
无关,这就形成了变量保护,也就是我们说的变量私有化
2) 保存变量,因为变量一直被占用,所以无法销毁,这就起到在ECStack
中保存变量的目的 -
image.pngA(2)
执行完后,执行C(3)
,所以会创建一个新的执行上下文EC(C)
,并入栈:
1) 初始化其this
指向,因为没有对象调用它,所以它指向全局对象window
2) 初始化其作用域链,指向其作用域,因为C实际上指向的是B,而B的作用域又是AO(A)
,所以 :C[[scopeChain]] = AO(A)
3) 创建AO(C)
对象: 初始实参、形参赋值、执行内部代码
-
执行到内部
image.pngconsole.log(x + y + z)
时,发现x和y
变量没有在AO(C)
中,所以向其作用域链AO(A)
查找,发现AO(A)
中有x = 2 , y = 2
;z
在自身AO(C)
中有z= 3
,所以结果为7
。
下图中的这两条,就形成了一个完整的作用域链AO(C) => AO(A) => VO(G)
网友评论