今天我们来详细说说什么是JavaScript的预编译,深入了解它的底层原理,读完这篇文章之后,就再也不怕面试官在js预编译的本质上对你刨根问底的追问啦!
声明提升
声明提升可是与js的预编译紧密相连的呢,所以我们先来学习一下声明提升又是怎么一回事。
简单来说:
1. var 声明的变量会存在声明提升
以代码为例:
css
console.log(a);
var a = 1
在这个例子中,变量a在声明之前被使用,但由于 var 声明 a 得到了声明提升,不会抛出错误,而是输出undefined。上面的代码等同于:
css
var a //声明提升
console.log(a);
a = 1
2. 函数声明会整体提升
scss
foo()
function foo(){
console.log('hello');
}
函数调用在声明之前,但执行后仍然可以输出 hello ,同样,函数声明被提升了,即:
scss
function foo(){
console.log('hello');
}
foo()
对于声明提升的介绍在我之前那篇《写给小白的JavaScript作用域及声明提升详解》有详细解释,如果想深入了解的话欢迎去看━(*`∀´ *)ノ亻!
接下来就是我们今天的重点了(敲黑板!!!),我们将分为两个部分来讲js的预编译,一起来看看吧。
预编译发生在函数体内时
我们需要记住这四个步骤:
- 创建函数的AO对象 (Action Object)
- 找形参和变量声明,将形参和变量声明作为AO的属性名,值赋予undefined
- 将形参和实参统一
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
它们是什么意思呢?我们等下再来解释,我们先思考以下代码:
css
var a = 1
function foo(a){
var a = 2
function a(){}
var b = a
console.log(a);
}
foo(3)
看完这段代码你能想到最后结果会得到什么吗?函数体内这么多个a到底是输出了哪个呢?我们刚刚讲了声明提升,变量a会声明提升,函数a也会声明提升,那函数a的声明提升是在变量a的声明提升前还是后呢?
这样?
css
var a = 1
function foo(a){
var a
function a(){}
a = 2
var b = a
console.log(a);
}
foo(3)
还是这样?
css
var a = 1
function foo(a){
function a(){}
var a
a = 2
var b = a
console.log(a);
}
foo(3)
执行后我们会得到结果为2,看到这里,你是不是会觉得:哦~我懂了是第二种,原因是变量a先声明提升,函数a再声明提升,于是函数a的声明提升跑到最顶上了。那如果我们将代码改成这样,结果又是什么呢?
css
var a = 1
function foo(a){
function a(){}
var a = 2
var b = a
console.log(a);
}
foo(3)
结果还是2,现在你真正懂了,原来是函数名和变量名冲突的时候,函数名会提升到变量名前面。
那我们现在再来看一段代码,先不用想它会输出什么:
javascript
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(){}
var d = a
console.log(d);
}
fn(1)
看到这代码的第一眼,你说:我嘞个豆,还有这么恶心的代码?五个输出!?现在你还要傻傻的继续一个一个看它们的声明提升看到脑袋发晕吗?不!!我们用js的预编译来解决它,让你轻松得出答案!记得我们最开始说的四个步骤吗,我们复习一遍:
- 创建函数的AO对象 (Action Object)
- 找形参和变量声明,将形参和变量声明作为AO的属性名,值赋予undefined
- 将形参和实参统一
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
执行这段代码,在调用函数fn后,会先进行函数体内的编译,再去执行函数体内的命令,而函数体内的编译是怎么进行的呢?
首先我们创建函数的AO对象,也就是整个函数的作用域对象:
再来第二个步骤,找形参和变量声明,将形参和变量声明作为AO的属性名,值赋予undefined:
为什么要找形参和变量声明呢,因为它们都是函数体内的有效标识符。我们找的时候,会发现形参也是a,有个变量声明也是a,但是js这门编程语言当中,对象里是不允许有两个重复的key,所以其中一个a会被覆盖,只剩一个a。
第二步完成之后,进行第三步:将形参和实参统一
我们可以看到实参为1,那么我们将1给形参:
然后我们进行最后一步,在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体,我们找到了a的函数声明,d的函数声明:
好啦!预编译的四个步骤都完成了,现在开始执行函数体内的命令:
首先第一条就是输出a,此时a的值是函数体,所以这条命令会输出一个函数体;下一个命令将a赋值123,此时a的值为123:
接下来又是输出a的命令,这条则会输出123;再下一行为函数声明不用执行,之后又是输出a,依然为123;再之后给b赋值函数体:
命令输出b则会输出一个函数体;下一条命令将a赋值给d,a的值为123,则d也为123,最后一条命令输出会123,所以最终的结果是:
学会了请扣1,没学会再看一遍!!!
预编译发生在全局
牢牢记住这三个步骤~~
- 创建GO对象 (Global Object)
- 找变量声明,将变量名作为GO的属性名,值赋予undefined
- 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
看以下代码:
php
var global = 100
function fn(){
console.log(global);
}
fn()
和在函数体中的预编译很相似,第一步创建一个GO对象 ,第二步找变量声明,将变量名作为GO的属性名,值赋予undefined ,第三步在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体,完成后GO对象是这样子的:
之后执行代码,第一行命令将100赋值给global:
之后再执行fn的调用,此时执行函数体,在此之前进行函数体的编译,建立一个AO对象,进行我们前文所说的在函数体内的预编译,最后AO里面为空,进行函数体内命令的执行,输出global,即为100。
来看一个难度提升版:
最后输出为undefined和200,你挑战成功了吗?
到这里我们的知识就讲完啦!!今天又进步了一点点,一起加油!!