通过几道题简单分析浏览器底层运行原理以及作用域链
概念
ECStack:执行上下文栈,浏览器运行js代码的栈空间;
EC(G):全局执行上下文;
VO:变量对象,存储当前上下文中的变量;
AO对象:Activation Object,指活动性对象,也叫执行期上下文,就是我们通常所说的作用域。AO可以理解为VO的一个实例,也就是VO是一个构造函数,然后VO(Context) === AO,所以VO提供的是一个函数中所有变量数据的模板。
GO:Gobel Object,是全局对象,GO对象跟window对象是同一个对象。可以理解为window对象有两个名字 window == GO。
VO和AO其实是一个东西,只是处于不同的执行上下文生命周期。AO存在于执行上下文堆栈顶部(就是上边说的’当控制进入函数代码的执行上下文时’)的时期。再粗暴点,就是函数调用时,VO被激活成了AO。
第一题
let x = [12, 23];
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
fn(x);
console.log(x);
- 在执行上下文栈(ECStack)中,形成一个全局执行上下文(EC(G)),并将window对象赋值给EC(G),在(EC(G))生成变量对象(VO),存储当前上下文中的变量,此题中,生成变量x和fn,由于变量x和fn指向的是引用类型,就在浏览器堆内存中开辟两个空间,存放 [12, 23]和fn函数,其中函数代码字符串的形式存储在堆内存中,然后将数组的堆内存地址赋值给下,存储函数的堆内存地址赋值给fn。 执行fn(x)实际上是执行fn(AAAFFF000),每一个函数执行都会形成一个全新的执行上下文EC(FN)。
2. 由于js是单线程的,执行fn的时候,就不再执行EC(G),会先将EC(G)放置到全局执行上下文(ECStack)的底部,然后将fn的执行上下文EC(FN)放置到ECStack中执行。
- 执行EC(FN)里面的代码
function fn(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y);
}
* 初始化实参集合 arguments = {0:AAAFFF000};
* 创建形参变量并且赋值,y=x=AAAFFF000;
* 代码执行。
4. 在非严格模式下,会建立映射机制,严格模式下不会,而且ES6箭头函数中没有arguments实参集合。
y[0] = 100将AAAFFF000对象里面的值变成了[100, 23],所以x也变成了[100, 23]。
y=[100],相当于在堆内存中,开辟了一块新的空间存放[100]这个数组,并让y指向[100]这个数组的堆内存地址BBBFFF00,所以y=BBBFFF00,y[1]=200相当于在BBBFFF00这个数组中新增一项,变成[100,200]。至此,EC(FN)里面的代码执行完毕。
并且EC(FN)里面的值没有被其他变量占用,所以EC(FN)出栈。
第二题
var x = 10;
~(function(x) {
console.log(x);
x = x || (20 && 30) || 40;
console.log(x);
})();
console.log(x);
- 在执行上下文栈(ECStack)中,形成一个全局执行上下文(EC(G)),并将window对象赋值给EC(G),在(EC(G))生成变量对象(VO),存储当前上下文中的变量,生成x,10为基本类型,所有x=10。
- 自执行函数,创建加执行。
在堆内存中开辟空间存储函数,形成全新的执行上下文EC(fn),并且拿到ECStack中执行。x没有传值,为undefined, x = x || (20 && 30) || 40执行,先执行20 && 30,因为&&优先级高于||,x为undefined,所以x为30,30为真,返回30,不返回后面的40。~(function(x) { console.log(x); x = x || (20 && 30) || 40; console.log(x); })()
- 在全局执行上下文中x=10。
第三题
let x = [1, 2];
let y = [3, 4];
~(function(x) {
x.push("A");
x = x.slice(0);
x.push("B");
x = y;
x.push("C");
console.log(x, y);
})(x);
console.log(x, y);
- 在执行上下文栈(ECStack)中,形成一个全局执行上下文(EC(G)),并将window对象赋值给EC(G),在(EC(G))生成变量对象(VO),存储当前上下文中的变量,在堆内存中存放数组,x = AAAFFF000,y = AAAFFF111。
- 自执行函数,创建加执行,在堆内存中开辟空间存储函数,形成全新的执行上下文EC(fn),将x传给自执行函数,并且拿到ECStack中执行。
~(function(x) {
x.push("A");
x = x.slice(0);
x.push("B");
x = y;
x.push("C");
console.log(x, y);
})(x);
- 在EC(fn)中,形参x=AAAFFF000,x.push("A"),AAAFFF000变成[1, 2, "A"], x = x.slice(0),返回一个新数组并让x指向新数组BBBFFF00,值为[1, 2, "A"], x.push("B")后,值为[1, 2, "A","B"]。
- x = y,y不是私有的,查找上级作用域中的y,x=AAAFFF111, x.push("C"), AAAFFF111值为[3,4,"C"]。
- 在自执行函数里面,x=AAAFFF111=[3,4,"C"],y是全局的,变成[3,4,"C"]。 在EC(G)中,x=AAAFFF000=[1, 2, "A"],y=AAAFFF111=[3,4,"C"]。
浏览器的垃圾回收机制
谷歌浏览器的垃圾回收机制,浏览器会在空闲的时候,把所有不被占用的堆内存,进行释放和销毁。
IE浏览器的垃圾回收机制:当前堆内存被占用一次,数字加1,再被占用一次,数字累加,取消占用,数字减一,一直减到0销毁。
作用域链
创建函数的时候:
创建一个堆(存储代码字符串和对应的键值对)
初始化了当前函数的作用域链[[scope]] = 所在上下文EC中的变量对象VO/AO
函数执行的时候:
创建一个新的执行上下文EC(压缩到ECStack里执行);
初始化this的指向;
初始化作用域链[[scopeChain]]:xxx;
创建AO变量对象用来存储变量:初始化arguments,新参赋值。
第一题
function A(y) {
let x = 2;
function B(z) {
console.log(x + y + z);
}
return B;
}
let c = A(2);
c(3);
第二题
let x = 5;
function fn(x) {
return function(y) {
console.log(y + ++x);
};
}
let f = fn(6);
f(7);
在这一题中,fn的作用域为VO(G),函数fn执行的时候的作用域链是scopeChain:<AO(fn1),VO(G)>,return出来的函数的作用域是AO(fn1),作用域链是<AO(f1),AO(fn2),VO(G)>。
函数的执行,形成一个私有作用域,形参和当前私有作用域中声明的变量都是私有变量,保存在内部的一个变量对象中,其下一个外部环境可能是函数,也就包含了函数的内部变量对象,直到全局作用域。
当在内部函数中,需要访问一个变量的时候,首先会访问函数本身的变量对象,是否有这个变量,如果没有,那么会继续沿作用域链往上查找,直到全局作用域。如果在某个变量对象中找到则使用该变量对象中的变量值。
由于变量的查找是沿着作用域链来实现的,所以也称作用域链为变量查找的机制。