作为一名前端小白,不知道大家是否遇到和我一样的问题。看了一道面试题的解析,当时觉得会了,可是过两天以后再看又不会了;盲目追求各种新技术,感觉什么都会点,但是一上手就不行了... 痛定思痛后,我终于认识到了问题所在,开始专注于基本功的修炼。近半年来通读了(其实是囫囵吞枣)《JavaScript高级程序设计》、《你不知道的JavaScript上、中、下》等书籍,本系列文章是我读书过程中对知识点的一些总结。喜欢的同学记得帮我点个赞😁。
懂王系列(一)之彻底搞懂JavaScript函数执行机制
懂王系列(二)之彻底搞懂JavaScript作用域
懂王系列(三)之彻底搞懂JavaScript对象
懂王系列(四)之彻底搞懂JavaScript类
懂王系列(五)之彻底搞懂JavaScript原型
懂王系列(六)之彻底搞懂JavaScript中的this
懂王系列(七)之彻底搞懂JavaScript数据类型
懂王系列(八)之彻底搞懂JavaScript语句
懂王系列(九)之彻底搞定JavaScript类型转换
当Javascript代码执⾏的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆⾥存放着⼀些对象。⽽栈中则存放着⼀些基础类型变量以及对象的指针。 但是我们这⾥说的执⾏栈和上⾯这个栈的意义却有些不同。
这里的栈除了执行代码外,基础类型的值也会存储在栈中,引用类型存储在堆中。
null的作用,null不会开辟内存,而是把之前的指针指向空指针。
null和0区别:0需要开启一个内存,null不需要
null和undefined的区别:
null想要赋值,但是还没有赋值,拿null占位
undefined:未赋值
js 在执⾏可执⾏的脚本时,⾸先会创建⼀个全局可执⾏上下⽂(globalContext),每当执⾏到⼀个函数调⽤时都会创建⼀个可执⾏上下⽂(execution context)
EC。当然可执⾏程序可能会存在很多函数调⽤,那么就会创建很多EC,所以 JavaScript 引擎创建了执⾏上下⽂栈(Execution contextstack,ECS)来管理执⾏上下⽂。
当函数调⽤完成,js会退出这个执⾏环境并把这个执⾏环境销毁,回到上⼀个⽅法的执⾏环境...这个过程反复进⾏,直到执⾏栈中的代码全部执⾏完毕,如下是以上的⼏个关键词,我们来⼀次分析⼀下:
执⾏栈(Execution Context Stack)
全局对象(GlobalContext)
活动对象(Activation Object)
变量对象(Variable Object)
函数执行的阶段可以分文两个:函数建立阶段、函数执行阶段
1. 函数创建阶段
当调用函数时,还没有执行函数内部的代码,会创建一个创建执行上下文对象
fn.EC = {
variableObject: // 函数中的 arguments、参数、局部成员
scopeChains: // 当前函数所在的父级作用域中的活动对象
this: {} // 当前函数内部的 this 指向
}
创建阶段做了三件事:
1.创建一个堆(存储代码字符串和对应的键值对)
2. 初始化当前函数的作用域(没错,创建时就定义好了)
3. [[scope]] = 所在上下文EC中的变量对象VO/AO
这里的变量对象VO是与执⾏上下⽂相关的特殊对象,⽤来存储上下⽂的函数声明,函数形参和变量。 VO分为全局上下⽂的变量对象VO,函数上下⽂的变量对象VO
VO(globalContext) === global;
我们以下面这个函数为例,来分析一下:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
// 全局上下⽂的变量对象
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// test函数上下⽂的变量对象
VO(test functionContext) = {
x: 30,
b: 20
};
2. 函数执行阶段
fn.EC = {
activationObject: // 函数中的 arguments、参数、局部成员
scopeChains: // 当前函数所在的父级作用域中的活动对象
this: {} // 当前函数内部的 this 指向
}
函数执行阶段将变量对象VO变为了活动对象AO,具体做了以下几件事:
- 创建一个新的执行,上下文EC (压缩到栈ECStack里执行)
- 初始化THIS的指向
- 初始化作用域链[[scopeChain]] : xxx
- 创建AO变量对象用来存储变量
这里我们先来简单看下存储变量时的步骤:
- 找形参和变量声明(变量提升),将变量和形参名作为AO属性名,值为undefined
- 将实参值和形参统一
- 在函数体里面找函数声明,值赋予函数体
举个例子:
function fn(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
fn(1);
储存变量过程:
// 1
AO: {
a: undefined,
b: undefined
}
// 2
AO: {
a: 1,
b: undefined
}
// 3
AO: {
a: function a() {},
b: undefined
d: function d() {}
}
注意:
- 函数提升优先级比变量高
- 当函数声明的函数名被重新var,但是还未赋值时,该变量还是指向该函数
下面我们来正式看一下AO
// 1.在函数执⾏上下⽂中,VO是不能直接访问的,此时由活动对象扮演VO的⻆⾊。
// 2.Arguments对象它包括如下属性:callee 、length
// 3.内部定义的函数
// 4.以及绑定上对应的变量环境;
// 5.内部定义的变量
VO(functionContext) === AO;
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
// 当进⼊带有参数10的test函数上下⽂时,AO表现为如下:
// AO⾥并不包含函数“x”。这是因为“x” 是⼀个函数表达式(FunctionExpression, 缩写为FE) ⽽不是函数声明,函数表达式不会影响VO
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
3. 完整过程分析
我们以下面代码为例,来完整分析一下函数执行过程:
let x = 1;
function A(y) {
let x = 2;
function B(z) {
console.log(x + y + z);
}
return B;
}
let C = A(2);
C(3);
函数执行过程模拟:
/*第一步:创建全局执行上下文,并将其压入ECStack中*/
ECStack = [ //=>全局执行上下文 EC(G) = { //=>全局变量对象 VO(G): { ... //=>包含全局对象原有的属性 x = 1; A = function (y) {...}; A[[scope]] = VO(G); // =>创建函数的时候就确定了其作用域
}
}
];
/*第二步:执行函数A(2)*/
ECStack = [ //=>A的执行上下文 EC(A) = { //=>链表初始化为:AO(A)->VO(G) [scope]: VO(G)
scopeChain: <AO(A), A[[scope]]>
// =>创建函数A的活动对象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function (z) {...},
B[[scope]] = AO(A);
this: window;
}
},
//=>全局执行上下文
EC(G) = {
//=>全局变量对象
VO(G): {
... //=>包含全局对象原有的属性
x = 1;
A = function (y) {...};
A[[scope]] = VO(G); //=>创建函数的时候就确定了其作用域
}
}
];
/*第三步:执行B/C函数 C(3)*/
ECStack = [ //=>B的执行上下文 EC(B) { [scope]: AO(A)
scopeChain: < AO(B), AO(A), B[[scope]]>
//=>创建函数B的活动对象
AO(B): {
arguments: [0: 3],
z: 3,
this: window;
}
},
//=>A的执行上下文
EC(A) = {
//=>链表初始化为:AO(A)->VO(G)
[scope]: VO(G)
scopeChain: < AO(A), A[[scope]] >
//=>创建函数A的活动对象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function (z) {...},
B[[scope]] = AO(A);
this: window;
}
},
//=>全局执行上下文
EC(G) = {
//=>全局变量对象
VO(G): {
... //=>包含全局对象原有的属性
x = 1;
A = function (y) {...};
A[[scope]] = VO(G); //=>创建函数的时候就确定了其作用域
}
}
]
4. 作用域链
每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。
作用域链: [[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
函数执行时,会在作用域顶层(0)创建自己的AO,所以查找变量时,也是从作用域链的顶端依次向下查找
我们以下代码为例,来看一下函数的作用域链
function a() {
function b() {
var b = 234;
}
var a=123;
b();
}
var glob = 100 ;
a();
我们来修改一下原来的代码,
function a() {
function b() {
var b = 234;
}
var a=123;
return b
}
var glob = 100 ;
let b = a();
b()
我们将b函数返回出去,此时我们发现,当a函数执行完时,a的AO就已经断开了,但是b依然保留着对它的引用。所以a的AO没有被销毁掉。这就是闭包,符合以下两个条件就是闭包:
- fn 外部对内部有引用
- 在另一个作用域访问到 fn 作用域中的局部成员
5. this
this对象是在运行时基于函数的执行环境绑定的,在全局函数中this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。
每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函 数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
6. 描述一下EventLoop的执行过程
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
- 执行浏览器UI线程的渲染工作
- 检查是否有Web Worker任务,有则执行
- 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空