JS解析器执行原理和声明提升机制
文章目录
一.概念
浏览器中有一套专门解析JS代码的程序,这个程序称为JS的解析器
浏览器运行整个页面文档时,遇到< script > 标签时JS解析器开始解析JS代码
二.JS解析器的工作步骤
1.预解析代码
主要找一些关键字如 var,function,以及函数的参数等,并存储进仓库里面,也就是内存
先扫描全局的代码,在函数执行的时候,然后扫描局部的,也就是函数内部的
-
变量的初始值是undefind
//声明一个a变量,放进仓库scope console.log(a);//undefined var a = 1;
-
函数的初始值就是该函数的代码块(而不是undefined)
console.log(test);//打印函数代码块 //声明一个函数,放进仓库scope function test() {...} //控制台打印函数代码块 ƒ test() { return 1; }
-
当变量和函数重名时,不管顺序谁前谁后,只留下函数的值
console.log(test);//打印函数代码块 function test() { return 1; } var test=2; //控制台打印函数代码块 ƒ test() { return 1; }
注意:这里是预解析的时候,函数是一等公民,比变量优先级高
-
当函数和函数重名的时候,只会留下后面的那个函数,会遵从上下文机制
console.log(test); function test() { return 1; } //重名时,预解析只会留下后面这个函数 function test() { return 234; } var test=2; //控制台打印函数代码块 ƒ test() { return 234; }
2.逐行执行代码
当预解析完成之后,就开始执行代码,仓库中变量的值,随时都可能发生变化
1. alert(a);// function a(){alert(3);}
2. var a = 1;
3. alert(a);//1
4. function a() { alert(2); }
5. alert(a);//1
6. var a = 3;
7. alert(a);//3
8. function a() { alert(3); }
9. alert(a);//3
/*
解读代码
预解析过程:
第2行:找到了一个var 关键字,声明一个变量a
第4行:找到了一个function关键字 把a变成了一个函数
第6行:找到了一个var关键字,但是由于声明变量名字与函数重名,所以不起作用,a的值还是一个函数,因为
函数比变量的优先级高
第8行:找到了一个function关键字,a还是一个函数,但是把第4行的给覆盖了,因为函数重名,会遵从上下
文机制
最后预解析得到的结果是:
a => fn -> function a(){alert(3);}
原因: 1.当变量和函数重名时,不管顺序谁前谁后,只留下函数的值
2.当函数和函数重名的时候,只会留下后面的那个函数,会遵从上下文机制
逐行代码执行过程:
第1行: 弹出最后的名字叫a的函数代码块
alert(a);// function a(){alert(3);}
第3行: 因为第2行将a从函数变成了变量,并赋值为1
alert(a);//1
第5行: 因为第4行只是声明了一个函数,不会自己执行,所以a的值保持不变
alert(a);//1
第7行:因为第6行将a的值变成了3
alert(a);//3
第9行:因为第8行只是声明一函数,不会自己执行,所以a的值保持不变
*/
3. 示例解读
像这种示例,如果一眼无法看出来的话,使用解析器原理去看待题目,就没有解析不出来的,如果熟了,基本上就一眼就可以看出来,因为一步一步解析太麻烦了.
3.1 示例1(全局变量和函数内部变量名相同)
1.var a = 1;
2.function test(x) {
3. alert(x);
4. alert(a);
5. var a = 2;
6. alert(a);
7. }
8. test();
/*
解读代码
预解析过程:扫描代码,寻找var,function,以及函数的参数,放进仓库
先扫描全局的var function
global scope=>{
a => undefined
text =>function test(){}
}
逐行代码执行过程:
第1行,a赋值为1
第2行,只是声明函数,不会执行
第8行,调用函数,开始进行局部扫描,也就是函数内部进行预解析
test scope=>{
x => undefined
a => undefined
}
test()逐行执行函数内部的代码:
第3行:alert(x)=> undefined
第4行:alert(a)=> undefined
第5行,a赋值为2
第6行:alert(a)=> 2
*/
3.2 示例2 (变量和函数重名)
当变量和函数重名时,不管顺序谁前谁后,只留下函数的值
1.alert(typeof fn);//function
2.var fn = 10;
3.function fn() { };
4.alert(typeof fn);//number
/*
解析代码
预解析:
global scope=>{
fn=> function fn() { };
}
执行代码:
1.alert(typeof fn); 会弹出function
2.赋值fn=10,此时fn类型变成了number
3.不会执行
4.alert(typeof fn); //number
*/
3.3 示例3(函数内部使用全局变量)
1.var a = 1;
2.function fn() {
3. alert(a);//1
4. a = 2;
5.}
6.fn();
7.alert(a);//2
/*
解析代码
预解析:
global scope=>{
a=>undefined
fn=> function fn() { };
fn scope=>{
a=>undefined
}
}
执行代码:
1.a赋值等于1
2.不会执行
6.执行函数,函数没有参数,也没有var关键字进行声明,不会预解析
3.第3行:alert(a) 访问的是全局变量 a 弹出1
4.第4行:将全局变量a 赋值为了2
7.第7行:弹出2
*/
3.4 示例4(局部变量不会改变全局变量)
1.var a = 1;
2.function fn(a) {
3. alert(a);//undefined
4. a = 2;
5. alert(a);//2
6.}
7.fn();
8.alert(a);//1
/*
解析代码
预解析:
global scope=>{
a=> undefined
fn=>function fn(a) {...}
fn scope=>{
a=>undefined//预解析形参
}
}
执行代码:
先调用fn函数 fn函数内部弹出a为undefined
函数内部 a赋值为2
函数内部 执行第二个alert(a) 为2
函数执行完之后,继续执行下一步 alert(a) 为1因为函数函数内部虽然给了a=2
但是至少改的函数内部的值,不会改全局变量 的a
*/
3.5 示例5
console.log(num);// undefined
var num = 24;
console.log(num);// 24
func(100, 200);
function func(num1, num2) {
var total = num1 + num2;
console.log(total);// 300
}
很自然的一段代码,不要受前面的影响,而不知道最最简单的代码了
3.6 示例6.(同名函数)
fn();//2
function fn() { console.log(1); }
fn();//2
var fn = 10;
fn();//报错 fn is not a function
function fn() { console.log(2); }
fn();
/*
解析代码
预解析:
global scope=>{
fn =>function fn() { console.log(2); }
}
同名函数时,后面覆盖前面
变量和函数同名时,函数是一等公民,优先级高
逐行执行代码时:
第4行:将fn变成了一个number类型,再执行fn()时,函数不存在了,会直接报错,停止执行
*/
三.声明提升机制
在 JavaScrip 中变量声明和函数声明,声明会被提升到当前作用域的顶部。
讲声明提升机制之前,要先说说JavaScript中的作用域,这里排除ES6
1.作用域
在Javascript中,作用域为可访问变量(包含对象和函数)的集合
也就是说:作用域就是起作用的范围
1.全局作用域
整个页面起作用,在script内部都能访问到
全局作用域中有全局对象window,代表一个浏览器窗口,可以直接调用
全局作用域中声明的变量和函数,会作为window对象的属性和方法保存
变量在所有函数外声明,也就是全局变量,拥有全局作用域
<script>
var a = 123;//全局变量
function fn() {
console.log(a);//123
}
fn();
console.log(a);//123
</script>
在JavaScript中,函数是唯一拥有自身作用域的代码块
2.局部作用域
局部作用域内的变量只能在函数内部使用,所以也叫函数作用域
变量在函数内声明,即为局部变量,拥有局部作用域
<script>
function fn() {
var a = 123;//全局变量
console.log(a);//123
}
fn();
console.log(a);//报错:Uncaught ReferenceError: a is not defined
</script>
注意:
- 可以直接给一个未声明的变量赋值(全局变量),但不能直接使用未声明的变量!
- 由于局部变量只作用于函数内部,所以不同的函数内部可以相同名称的变量
- 当全局与局部有同名变量的时候,访问该变量将遵循"就近原则"
2.变量的生命周期
全局变量在页面打开时创建,在页面关闭后销毁
局部变量在函数开始执行时创建,函数执行完之后局部变量自动销毁
3.声明提升机制
JavaScript 的变量声明具有声明提升机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的顶部
我们可以从JavaScript解析器的执行原理来理解声明提升机制,就很好理解了,同样,上述解析器执行的示例代码中,都是声明提升机制的一种体现.当然了,这里是不考虑ES6的.
总结一下:
-
javascript是没有块级作用域的,函数是javascript中唯一拥有自身作用域的结构
-
声明变量,实际上就是定义了一个名字,在内存中开辟了存储空间,并且初始为undefined,提升到当前作用域顶部
-
函数的参数是原始类型的值(数值,字符串,布尔值),传值方式是传值传递,也就是在函数体内修改参数值,不会影响到函数外部
var a = 1; function fn(a) { console.log(a);//3 } fn(3); console.log(a);//1 不会改变原始值
-
函数的参数是复合类型,也就是引用类型(数组,对象,函数)的时候,传值方式是传址传递,传入的是原始值的地址,所以在函数内部修改参数,会改变原始的值(可参考深浅拷贝原理)
var obj = { a: 1, b: 2 }; function fn(obj) { obj.a=2; } fn(obj); console.log(obj);//{a: 2, b: 2}
注意:如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值!
比如数组:
var arr = [1, 2, 3] function fn(array) array = [1, 2]; } fn(arr); console.log(arr);//[1, 2, 3]
4.声明提升机制示例
- var a 变量声明,提升到作用域顶部,
- var a=1; 变量声明,提升到作用域顶部,但是赋值部分不会被提升
- var a=function(){ … } 函数表达式,但也只是变量的声明提升,并不会提升函数值
- function a() {…} 函数声明,会全部提升,而且如果函数名字和变量名字相同的话.优先级比变量高,因为是一等公民
总之呢,理解声明提升机制,好好去理解JS解析器的原理就好了.