函数预编译是怎么一回事?
JS执行三部曲:语法分析 预编译 解释执行
语法分析:简单来说就是浏览器先不运行代码,而是检查你代码是否有语法错误比如少加了符号啊,写入了中文字符等等操作。类似于老师检查你的作文,先不看你的内容怎么样,而是看有没有错别字。(啊哈哈,只是类比一下,好像有点不恰当,老师一般是直接一遍看过去)
预编译:
预编译有两个抽象出来的知识点:
- 函数声明整体提升
- 变量 声明提升
函数声明整体提升指的是当你使用了函数声明,那么函数声明默认会提升至script顶部
function up(){
console.log("hello,world");
}
up();
//打印hello,world
up();
//也能打印出hello,world
function up(){
console.log("hello,world");
}
这就是说function up(){ console.log("hello,world"); }
这个函数被默认提升到顶部,所以第二种方法执行函数的时候没有报错并且成功了。
变量 声明提升指的是变量在声明的时候,也会被提升至逻辑的顶端。
var a;
//这个叫做变量声明
var a = 1;
//这个叫做变量初始化 它相当于变量声明加变量赋值的双重操作
//即var a; a = 1;
我们需要注意的是提升的只有变量声明,也就是说:
console.log(a);
var a = 123;
//这里打印的是undefined而不是123
上面的操作拆解一下应该是这样:
var a;
console.log(a);
a = 123;
//这里相当于只声明了变量a但是没有进行赋值,所以打印的是undefined
但是我们遇到声明的变量为a,声明的函数名也为a,那我们打印的a的时候打印的是哪一个呢?因为两者都有提升,哪一个更加优先呢?
console.log(a);
//a(){}
function a(){
}
var a;
我们发现打印的是函数体a,但是这有可能是因为函数声明写在变量声明之前,那么我们颠倒一下顺序试一试:
console.log(a);
//a(){}
var a = 123;
function a(){
}
我们发现结果没有变,还是打印的函数a,那么这个原理是什么呢?我们需要更深的去了解JS预编译的过程。
这里我们先了解两个知识点:
- imply global 暗示全局变量:即任何变量未经声明就赋值,那么这个变量就为全局对象所有。全局变量指的是window
a = 123;
console.log(a);
//123
console.log(window.a);
//123
var a = b = 123;
console.log(window.a);
//123
console.log(window.b);
//123
这里b变量未经声明就赋值所以变成了window的属性,但是a声明了为什么也变成了window的属性呢?这是因为下面一条规则
2. 一切声明的全局变量全部变为window的属性
上面代码的a是全局声明的,所以它也变成了window的属性。
function go(){
var a = b =123;
}
go();
console.log(window.a);
//undefined
console.log(window.b);
//123
而当我们在函数内部声明变量的时候 它就不是全局变量了,它变成了局部变量,就不满足第二条规则了,所以打印window.a的时候,结果显示的是undefined,而b还是未声明就赋值,所以它还是一个全局变量,因此打印了出来。
我们继续讲回预编译:
预编译,它发生在函数执行的前一刻,它有以下几个环节:
- 创建AO对象 Activation Object(执行期上下文)
- 找形参和变量声明,将变量和形参名作为AO对象的属性名,值为undefined
- 将实参形参相统一
- 在函数体里找到函数声明,把函数声明当做值赋予函数体
让我们根据一个例子操作一下:
function test(a) {
console.log(a);
console.log(b);
var b = 234;
console.log(b);
a = 123;
console.log(a);
function a() {};
var a;
b = 234;
var b = function() {};
console.log(a);
console.log(b);
function d() {};
}
test(1);
- 创建AO对象
AO{
}
- 找形参和变量声明,将变量和形参名作为AO对象的属性名,值为undefined
我们首先寻找形参,发现函数的形参有a同时变量名它也有a我们只需要保留一个就行了,然后赋值undefined。
AO{
a : undefined,
b : undefined,
}
- 将实参形参相统一
test(1)传的值是1
AO{
a : 1,
b : undefined,
}
4.在函数体里找到函数声明,把函数声明当做值赋予函数体。关于函数体,比如:
function test() {
console.log("hello");
}
test();
console.log(test);
// function test() {
console.log("hello");
}
这里我们打印test,输出的是test这整个函数,test就指代着函数体。
这里我们还需要注意的是 function fn(){}
这种形式才叫做函数声明,而var fn = function(){}
j叫做函数表达式
AO{
a : function a (){},
b : undefined,
d : function d (){},
}
然后我们就得到了该函数完整的执行期上下文,然后以此为基础我们开始重新赋值
function test(a) {
console.log(a); 1
console.log(b); 2
var b = 234;
console.log(b); 3
a = 123;
console.log(a); 4
function a() {};
var a;
b = 234;
var b = function() {};
console.log(a); 5
console.log(b); 6
function d() {};
}
test(1);
- 第一个打印的a根据AO对象我们知道应该是
function a (){}
- 第一个打印的b我们根据AO对象知道应该是
undefined
- 第二个打印的b我们这里要注意一下,因为上面有一句赋值语句:
var b = 234;
,所以这个时候打印的b变成了234
- 同理,第二个打印的a因为上面又进行了赋值,所以变成了
123
- 第三个打印的a并没有重新赋值,所以还是
123
- 第三个打印的b因为上面将函数体赋给了它,所以它变成了
function (){}
好的,完成了!
那么接下来我们解决刚才的那个问题
console.log(a);
//a(){}
function a(){
}
var a;
按照步骤首先创建AO对象
AO{
}
然后寻找形参和变量声明并赋值undefined
AO{
a : undefined
}
接着将实参形参相统一
最后在函数体里找到函数声明,把函数声明当做值赋予函数体
AO{
a : function a (){}
}
然后打印a,所以最后的结果是 function a (){}
这个其实应该用全局的预编译,具体环节和函数预编译差不多,只是我们发现好像第三步形参实参相统一多余了,所以我们全局预编译只有三步:
- 创建我们全局的执行上下文叫GO对象即(Global Object)
- 找变量声明,将变量名作为GO对象的属性名,值为undefined
- 最后在函数体里找到函数声明,把函数声明当做值赋予函数体
我们的GO先于AO生成,并且首先元素查找AO,若AO没有则去GO查找
用上面一个例子
function go(){
var a = b =123;
}
go();
console.log(window.a);
//undefined
console.log(window.b);
//123
首先 ,我们应该先创建GO对象,由于b是一个全局变量所以创建GO对象的时候我们对其进行了初始化的操作最后等于123,因此我们在AO找不到b的时候去GO中寻找,最终打印出了结果。而a在AO对象中,且被赋值为undefined,所以最终输出也为undefined。
解释执行:预编译结束之后,就是我们浏览器的解释执行了。
上一篇: 猫狗队列
下一篇: 再次讲解js中的回收机制是怎么一回事。