一、对提升的理解
引擎会在解析JavaScript代码之前首先对它进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。
简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。
提升优先度:
- 语言自身定义(Language-defined): 所有的作用域默认都会包含this和arguments。
- 函数形参(Formal parameters): 函数有名字的形参会进入到函数体的作用域中。
- 函数声明(Function decalrations): 通过function foo() {}的形式(通过函数表达式:var fun = function(a){console.log(a)} 和 构造函数:var fun = new Function("a",console.log(a)) 定义的函数不会提升)
- 变量声明(Variable declarations): 通过var foo;的形式。
二、变量提升
var a = 3
function fn () {
console.log(a) //undefined
var a = 4
}
fn()
js编译的时候
var a = 3
function fn () {
var a;
console.log(a) //当声明一个变量,但并不给空对象赋值就使用是它的值就是undefined
a = 4
}
fn()
三、函数提升
通过function声明的函数, 在提升阶段会连函数体一起提升,而如果是函数表达式声明的只会提升名称,函数体在执行到赋值语句时才会被赋值,
function test() {
foo(); // TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { // 函数表达式被赋值给变量'foo'
alert("this won't run!");
}
function bar() { // 名为'bar'的函数声明
alert("this will run!");
}
}
test();
四、变量提升和函数提升的顺序
思考这么一道题:
console.log(a)
var a = 10
function a(){
console.log(20)
}
你觉得这个打印出来的a会是什么?
要是不知道提升的先后顺序,那肯定觉得输出的是10。这里就说一个知识点:你不知道的JavaScript(上卷)一书的第40页中写到:函数会首先被提升,然后才是变量。也就是说,同一作用域下提前,函数会在更前面。尽管a=10,在函数a的前面定义的,但是提升的时候还是先提升函数。所以这道题输出函数本身,以上代码等同于以下代码:
function a(){
console.log(20)
}
var a //由于上面函数已声明a,相同的变量名声明会被直接忽略
console.log(a) //输出函数体
a = 10
再来看一个例子,加深理解:
function a(){
console.log(10);
}
var a;//再次声明a,并未修改a的值,忽略此处声明
console.log(a)//输出函数本体
a();//函数声明提前,可调用,输出10
a =3;//这里修改值了,a=3,函数已不存在
console.log(a);//输出3
a = 6;//再次修改为6,函数已不存在
a();//a已经为6,没有函数所以没法调用,直接报错
五、为什么会有提升
搬运:
下面是Dmitry Soshnikov早些年的twitter,他也对这个问题十分感兴趣, Jeremy Ashkenas想让Brendan Eich聊聊这个话题:
大致的意思就是:由于第一代JS虚拟机中的抽象纰漏导致的,编译器将变量放到了栈槽内并编入索引,然后在(当前作用域的)入口处将变量名绑定到了栈槽内的变量。(注:这里提到的抽象是计算机术语,是对内部发生的更加复杂的事情的一种简化。)
然后,Dmitry Soshnikov又提到了函数提升,他提到了相互递归(就是A函数内会调用到B函数,而B函数也会调用到A函数),
Brendan Eich很确定的说,函数提升就是为了解决相互递归的问题,大体上可以解决像ML语言这样自下而上的顺序问题。
最后,Brendan Eich还对变量提升和函数提升做了总结:
大概是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。