01-EC 与 ScopeChain
码路教育 6/22/2022
# 1. 什么是预解析
代码段是指一个script标签就是一个代码段。JS代码在执行时,是一个代码段一个代码段执行。
<!-- 代码段是彼此独立的,上面的代码段报错了,不会影响下面的代码段 -->
<!-- 1个script标签就是一个代码段 -->
<script>
// 在一个代码段中就可以写JS代码
var a = 110;
// 上面代码段不能使用下面的代码段中定义的数据
console.log(b); // ReferenceError: b is not defined
</script>
<!-- 一个网页中可以有多个代码段 -->
<script>
var b = 220;
// 可以在下面的代码段中使用上面的代码段中的数据
console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JS代码的执行分两个阶段:一个叫预解析,一个叫执行,预解析结束后,才会进入到执行阶段。
什么是预解析?
- 浏览器在执行JS代码的时候会分成两部分操作:预解析以及逐行执行代码
- 也就是说浏览器不会直接执行代码, 而是加工处理之后再执行,
- 这个加工处理的过程, 我们就称之为预解析
预编译期间做了什么?
- 把声明提升:加var的变量就是被提升,function声明的函数也会提升,提升到代码段的最前面。
- 函数内部的局部变量,提升到函数体的最前面。
- 注意:变量的提升仅仅是声明 函数的提升不只提升了声明,也提升赋值
练习题:
<script>
// --------- 原题
console.log(a);
var a = 110;
console.log(a);
fn();
function fn(){
console.log("我是一个fn函数~");
}
</script>
<script>
// --------- 分析
// 下面的代码中有两个声明 加var的变量a function声明的函数 fn
// 两者都要提升 变量仅仅提升声明 function提升的整体
console.log(a); // und
var a = 110;
console.log(a); // 110
fn(); // 也可以执行
function fn(){
console.log("我是一个fn函数~");
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
// --------- 原题
g();
var g = function(){
console.log("g函数执行了....");
};
</script>
<script>
// --------- 分析
// g是一个变量 只不这个变量的值是一个函数 函数是一种数据类型
// 进行预编译时,仅仅是提升了var g;
// var g; g的值是und und(); 报错了
g(); // TypeError: g is not a function 只有函数才可以加()调用
var g = function(){
console.log("g函数执行了....");
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
// --------- 原题
console.log(i);
for(var i=0; i<10; i++){ }
console.log(i);
</script>
<script>
// --------- 分析
// i是全局变量 只有定义在函数内部的变量才是局部变量
// var i会提升,所以前面输出的i是und
console.log(i); // und
for(var i=0; i<10; i++){ } // for循环对i进行了赋值
console.log(i); // 10
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// --------- 原题
var a = 666;
fn()
function fn(){
var b = 777;
console.log(a);
console.log(b);
console.log(c);
var a = 888;
var c = 999;
}
</script>
<script>
// --------- 分析
// 提升:
// 全局的:var a; fn整体; 提升到代码段最前面
// fn内部中的var b; var a; var c;都提升; 提升到函数体的最前面
//
var a = 666;
fn()
// fn产生一个局部作用域,它的父级作用域是全局作用域
function fn(){
// var b; var a; var c;都提升了
var b = 777;
// 找a 自己有 使用自己的 如果自己没有 使用父级作用域的数据
console.log(a); // und 自己函数体内如果有数据,就使用自己的数据
console.log(b); // 777
console.log(c); // und
var a = 888;
var c = 999;
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script>
// --------- 原题
console.log(value);
var value = 123;
function value(){
console.log("我是value函数...");
}
console.log(value);
</script>
<script>
// --------- 分析
// 预编译:
// 提升:
// var value;会提升 value的值是und
// value函数整体也会提升
// 如果变量名和函数名一样,变量提升了,函数也提升了,提升后只会存在一个名字
// 问题是value的值是函数,也就意味着,变量的value就被覆盖了
console.log(value); // value是函数
var value = 123; // 123赋值给了value,value又变成了123
function value(){
console.log("我是value函数...");
}
console.log(value); // 123
// 提升的同名变量名和函数名,函数会覆盖变量,函数在JS中是一等公民,优先级高
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
// --------- 原题
console.log(a);
a = 666;
console.log(a);
</script>
<script>
// --------- 分析
// 没有加var的变量不会提升
//
console.log(a); // a is not defined / und
a = 666;
console.log(a); // 在一个代码段中,上面的代码出错了,下面的代码不会执行了
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// --------- 原题
function fn(){
console.log(a);
a = 666;
console.log(a);
}
fn();
console.log(a);
</script>
<script>
// --------- 分析
// 提升:fn整体
//
function fn(){
console.log(a); // 报错 找a 没a a is not defined
a = 666;
console.log(a); // 不执行
}
fn();
console.log(a); // 不执行
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
// --------- 原题
function fn(){
a = 110;
}
console.log(a);
</script>
<script>
// --------- 分析
// 函数没有调用,相当于函数没有写,没有执行
// 函数体中定义的所有的数据,都不会执行
function fn(){
a = 110; // 没有加var的变量是全局变量
}
// fn(); // 如果调用了,结果是110
console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
// --------- 原题
gn()
function gn(){
var k = 123;
console.log(k);
}
console.log(k);
</script>
<script>
// --------- 分析
// gn整体要提升
gn()
function gn(){
var k = 123; // var k要提升到函数体最前面
console.log(k); // 123
}
console.log(k); // 报错
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. 执行上下文和作用域链
内存分区:
- 我们只需要掌握两个区,一个是栈区,一个是堆区。
- 基本数据类型,是存储在栈区的,引用数据类型是存储在堆区,堆区的地址,还是保存在栈区
# 2.1 全局代码和函数代码中产生的EC
JS代码分两类:
- 全局代码:函数外面的代码都是全局代码
- 函数代码:一个函数就是一个局部代码
<script>
// 函数外面的就是全局代码
var a = 1;
var b = 2;
function fn(){
// 函数里面的是局部代码
var c = 3;
var d = 4;
}
fn()
fn()
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
分析如下:
- 全局代码执行产生ECG(Execution Context Gloable),每当调用一个函数,就产生一个函数的EC。每产生一个EC,需要放到ECS(Execution Context Stack),当函数调用完毕,这个EC就是出栈,出栈后的EC,会被销毁,所谓的销毁指是它们分配的内存空间都要被释放掉。
总结:
- 当全局代码执行时,就会产生一个全局的执行上下文,EC(G);
- 当函数代码执行时,就会产生一个局部的执行上下文,EC(Fn)。只要调用一个函数,就会产生一个局部执行上下文。调用100个函数,就会产生100个执行上下文。
执行上下文栈:
- js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。
- Execute Context Stack ===> ECS
代码执行流程如下:
- JS在执行代码时,肯定先执行全局代码,就会产生EC(G),这个EC(G)就要入栈。当我们调用一个函数,就会产生一个局部的执行上下文,此时这个局部的执行上下文也要入栈。当函数调用完毕后,这个EC就要出栈,又进入EC(G),当全局代码执行完后,EC(G)也要出栈。
执行上下文的作用:
- 提供数据,全局代码,肯定需要去全局的执行上下文中找数据。
# 2.2 全局代码和函数代码中产生的EC
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象 所有的作用域(scope)都可以访问;
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
- 其中还有一个window属性指向自己;说白了,GO就是window
- 只要我们写的全局变量或在全局中写的函数,都会挂载到window上面
代码如下:
<script>
console.log(window); // GO
var a = 110;
var b = 666;
var res = a+b;
function fn(){
console.log("fn...");
}
console.log(window.a);
console.log(window.b);
window.fn()
// 代码开始执行之前,JS引擎会帮我们创建一个全局对象,叫GO
// 换句话,说到window,指的就是GO
// GO中还需要放我们创建出来的全局变量和全局函数
var globalObject = {
String:"类",
Data:"类",
setTimeout:"函数",
alert:"函数",
// ....
window:globalObject, // GO中有一个特殊的属性,叫window window还是指向GO
a:und,
b:und,
res:und,
fn:"函数"
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
var n = 110;
console.log(n); // 110
console.log(window.n) // 110
m = 220;
console.log(m); // 220
console.log(window.m); // 220 m可以放到GO中,因为是全局变量
console.log(window.name); // 人家给GO中放了一个name, 现在是空串
console.log(window.x); // 访问对象中不存在的属性,结果是Und
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
ECG被放入到ECS中里面包含两部分内容:
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
遇到函数如何执行?
- 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中。
<script>
var a = 1;
var b = 2;
var res = a+b;
console.log(res);
function fn(){
console.log("我是fn函数....");
}
// 当调用函数时,就会产生一个EC(fn)
fn();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
ECF中包含三部分内容:
- 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO): AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
- 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
- 第三部分:this绑定的值:这个我们后续会详细解析;
练习题(面试题)
<script>
var n = 110;
console.log(n);
console.log(window.n);
m = 220;
console.log(m);
console.log(x);
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
var a = 1;
var b = "hello";
function fn(){
console.log("fn...");
}
var arr = ["a","b","c"];
var obj = {name:"wc",age:100}
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
function fn(a){
console.log(a);
}
fn(110)
console.log(a);
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
var arr = [11, 22];
function fn(arr) {
arr[0] = 100;
arr = [666];
arr[0] = 0;
console.log(arr);
}
fn(arr);
console.log(arr);
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
var a = 1;
var b = 1;
function gn(){
console.log(a,b);
var a = b = 2;
console.log(a,b);
}
gn();
console.log(a,b);
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
<script>
var a = 1;
var obj = { uname:"wangcai" }
function fn(){
var a2 = a;
obj2 = obj;
a2 = a;
obj.uname = "xiaoqiang";
console.log(a2);
console.log(obj2);
}
fn();
console.log(a);
console.log(obj);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// 同名的变量名,只会提升第1个
var a = 1;
var a = 2;
var a = 3;
console.log(a);
// 预编译后的代码如下:
var a;
a = 1;
a = 2;
a = 3;
console.log(a);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 提升:var a;
// a函数整体提升 a = funciton(){};
// 执行:a = 1;
var a = 1;
function a(){
console.log("a...");
}
console.log(a); // 函数a
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<script>
var a = 10;
// 赋值操作就是把栈区(GO,AO)中的数据,copy一份,赋值给变量
var b = a;
console.log(a,b); // 10 10
b = 1; // 把1重新赋值给b所对应的内存空间
console.log(a,b); // 10 1
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
var a = [1,2];
// 把栈区的地址赋赋值给b, a和b指向同一个内存空间
var b = a;
console.log(a,b); // [1,2] [1,2]
// 把一个新堆的地址重新赋值给b b指向一个新堆空间
b = [3,4];
console.log(a,b); // [1,2] [3,4]
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
var a = [1,2];
var b = a;
console.log(a,b); // [1,2] [1,2]
b[0] = 110;
console.log(a,b); // [110, 2] [110, 2]
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
var a = [1,2];
var b = [1,2];
// a 和 b 是地址 == 比较时比较的是地址 不同的堆,地址是不可能一样
console.log(a == b); // false
console.log(a === b); // false
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
var a = 110;
var b = 110;
console.log(a == b); // true
</script>
1
2
3
4
5
2
3
4
5
<script>
var a = [1,2];
// a 和 b中保存的地址是一样的
var b = a;
console.log(a == b); // true
console.log(a === b); // true
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
console.log(a,b); // 报错:b is not defined
var a = b = 2;
</script>
1
2
3
4
2
3
4
<script>
var a = {m:666};
var b = a;
b = {m:888};
console.log(a.m); // {m:666}
</script>
1
2
3
4
5
6
2
3
4
5
6
<script>
var a = {n:12};
var b = a;
b.n = 13;
console.log(a.n); // 13
</script>
1
2
3
4
5
6
2
3
4
5
6
<script>
console.log(a); // 报错
a = 111;
</script>
1
2
3
4
2
3
4
<script>
var m = 1;
n = 2;
console.log(window.m); // 1
console.log(window.n); // 2
</script>
1
2
3
4
5
6
2
3
4
5
6
<script>
function fn(){
var a = 111;
}
fn();
console.log(window.a); // 访问一个对象上不存在的属性,结果是und
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
var a = -1;
if(++a){ // ++a 整体是新值 0 => false
console.log("666");
}else{
console.log("888");
}
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
console.log(a,b); // und und
if(true){
var a = 1;
}else{
var b = 2;
}
console.log(a,b); // 1 und
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
var obj = {
name:"wc",
age:18
}
// in是一个运算符 判断一个属性是否属性某个对象
console.log("name" in obj); // true
console.log("score" in obj); // false
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
var a;
console.log(a); // und
if("a" in window){
a = 110;
}
console.log(a); // 110
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
console.log(a); // und
if("a" in window){
var a = 110;
}
console.log(a); 110
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
<script>
var a = 100;
function fn(){
console.log(a);
return
var a = 110;
}
fn();
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
<script>
var n = 100;
function foo(){
n = 200;
}
foo()
console.log(n);
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
function fn(){
var a = b = 100;
}
fn();
console.log(a);
console.log(b);
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
<script>
var n = 100;
// 找一个函数的父的EC,看的是函数的定义处,不是调用处
function fn(){
console.log(n);
}
function gn(){
var n = 200;
console.log(n);
fn()
}
gn()
console.log(n);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14