像变量提升和函数提升这种偏学院派的问题在面试中出现的概率很高,在实际开发中也会影响到编程的效率。
前段时间在网上做面试题,发现自己对这方面知识的了解还是差了点,于是翻书看博客查文档写demo,最终有了这篇文章(偏新手向。
1.变量提升
通常JS在引擎在正式执行之前会进行一次预编译,在这个过程中,首先将 变量声明 和 函数声明 提升到当前作用域的顶端,然后再进行下一步的处理。
在下面的代码中,我们在函数声明里定义了一个变量,只不过是在 if 判断的语句块中定义的:
function fn(){
if(!foo){
var foo = 5
}
console.log(foo); // 5
}
fn();
运行代码之后,我们发现结果是 5 ,为什么? 在进入 if 的判断条件时,函数内并没有声明过 foo 这个变量,那么它是怎么通过判断的呢?实际上,这就是JS引擎在预编译时所作的事情,我们尝试重现一下预编译时的情形:
function fn(){
var foo;
if(!foo){
foo = 5;
}
console.log(foo);
}
fn();
我们可以看到,不同的地方就在于 JS 把 变量声明提升到了当前作用域的顶部(只是声明,并没有赋值),随后来看 if 的判断条件 !foo , 因为if判断之前已经声明了 foo ,但没有赋值,所以 foo 的值为 undefined,转换为布尔值之后再进行取反,得到true,所以进入if判断,将foo赋值为5。
类似的还有下面的这个例子:
var foo = 3;
function fn(){
var foo = foo || 5 ;
console.log(foo) ; //5
}
fn();
这个例子:同样,我们尝试还原到预编译时的样子:
var foo ;
function fn(){
var foo;
foo = foo || 5;
console.log(foo);
}
foo = 3;
fn();
首先,在全局作用域中先声明了一个变量 foo ,但没有赋值。随后通过函数声明表达式声明的函数被提升。其次,全局作用域中的foo被赋值为3 。最后,调用函数 fn 。 在 fn 内部的预编译则是 首先声明一个函数内部变量 foo,对 foo 进行赋值(等号右侧是一个表达式)。
关键在于 对 foo 赋值的这条语句。 foo = foo || 5 ,上面说过,因为函数内部有foo,所以会依照就近原则取到内部的foo ,但此时它并没有被赋值,所以是undefined。所以在进行赋值时, foo = 5
如果在同一个作用域中声明了多个同名的变量,那么也是如此:
function fn(){
var foo = 1;
{
var foo = 2;
}
console.log(foo); //2
}
fn();
注意!因为在JS没有块级作用域,只有全局作用域和函数作用域【可以通过let声明创建块级作用域】,所以预编译为:
function fn(){
var foo;
foo = 1;
{
foo = 2;
}
console.log(foo) //2
}
fn();
2.函数提升
在实际开发中,我们可能会见到这样的代码:
function fn(){
bar(); // 'this is bar'
function bar(){
console.log('this is bar')
}
}
fn();
为什么函数可以在声明之前就调用?并且和变量的声明不同,它还能得到正确的结果?其实,JS引擎在预解析的时候将函数声明整个提前到了当前作用域的顶部,预编译之后的代码如下:
function fn(){
function bar(){
console.log('this is bar')
}
bar();
}
与变量声明相似的是,如果同一个作用域中有多个同名的函数声明,那么后面声明的会覆盖前面声明的:
function fn(){
bar(); // 'this is the second bar'
function bar(){
console.log('this is the first bar')
}
bar(); // 'this is the second bar'
function bar(){
console.log('this is the second bar')
}
}
fn();
预编译之后为:
function fn(){
function bar(){
console.log('this is the first bar')
}
function bar(){
console.log('this is the second bar')
}
bar(); // 'this is the second bar'
bar(); // 'this is the second bar'
}
fn();
对于函数,除了使用上面的函数声明,我们还会用到函数表达式。下面是函数声明和函数表达式的对比:
// 函数声明
function fn(){ console.log('this is function declaration') }
// 匿名函数表达式
var fn = function(){ 'this is anonymous function expression' }
// 具名函数表达式
var fn = function bar(){ 'this is named function expression' }
可以看到,匿名函数表达式就是将一个不带名字的函数声明赋值给一个变量(本质上还变量声明,只是值换成了一个匿名函数)。而具名函数表达式,则是将一个带名字的函数声明重新赋值给一个变量,需要注意的是,这个函数名只能在此函数内部使用。我们也看到了,其实函数表达式可以通过变量访问,所以也存在变量提升的效果。
那么,当函数声明 遇到 函数表达式, 会发生什么?
function fn(){
bar(); //2
var bar = function(){
console.log(1)
}
bar(); //1
function bar(){
console.log(2);
}
bar(); //1
}
fn();
可能你会问,为什么是 2,1,1 ?我们还是先把它还原到预编译时的状态:
function fn(){
var bar;
function bar(){
console.log(2)
}
bar(); // 2
bar = function(){
console.log(1)
}
bar(); // 1
bar(); // 1
}
fn();
是不是一目了然?
我们再来看看当 变量 和 函数 重名时,会如何执行:
var foo = 3;
function hoistFunction() {
console.log(foo); // function foo() {}
foo = 5;
console.log(foo); // 5
function foo() {}
}
hoistFunction();
console.log(foo); // 3
这段代码非常有意思,我在第一次看的时候认为最后一个输出的 foo 肯定是 5 ,因为hoistFunction 函数内部的变量 foo 没有使用 var 定义,所以它会变成全局变量覆盖之前定义过的全局变量 var foo = 3 。但是最后输出的结果仍然是 3.
我们将它还原一下:
var foo = 3;
function hoistFunction(){
function foo(){}
console.log(foo); //function foo(){}
foo = 5;
console.log(foo)
}
hoistFunction();
console.log(foo);
第一个输出的结果应该没有问题,那么第二个呢?
我们先来看另一段代码
foo();
function foo(){
console.log(1000)
}
var foo = 1;
foo();
照样先预编译一下
function foo(){
console.log(1000)
}
foo();
var foo = 1;
foo();
你觉得结果是什么?思考一下
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
function foo(){
console.log(1000)
}
foo(); // 1000
var foo = 1;
foo(); // Uncaught TypeError: foo is not a function
为什么 foo 不是一个函数? 只有一种解释,通过函数声明创建的函数,它的名称标识符被绑定在了当前作用域中,你可以理解为 这个函数的名称标识符也是在当前作用域中定义的一个变量 ,这样的话,接下来的变量声明就会覆盖掉这个函数的名称标识符,所以 foo 不再是一个函数。
事实上,参考MDN的文档:
我们可以发现,官方对 以函数声明方式创建的函数 的名称标识符 的定义就是一个变量。
好,现在再回来看最初的那个函数:
var foo = 3;
function hoistFunction(){
function foo(){}
console.log(foo); //function foo(){}
foo = 5; // 将 函数 foo 的名称标识符 覆盖
console.log(foo)
}
hoistFunction();
console.log(foo); // 3
在 function foo(){} 这条函数声明出现时,我们可以理解为它在函数内部定义了一个 '变量' ,随后打印 foo ,那么毫无疑问,foo 就是被提升到当前作用域顶部的那个函数 。
接着 将 foo 这个变量(函数 foo 的名称标识符)的赋值为 5 ,那么再次打印 foo 的时候,打印的就是那个被赋值为 5 的'变量'。
那么这也解释了为什么在全局作用域中的 变量 foo 没有受到影响。
因为在hoistFunction这个函数内部,在通过函数声明创建 foo 的时候就声明了一个变量 foo ,那么根据查找规则,【在函数作用域中有这个变量的话,就不再沿着作用域链向上至全局作用域中查找该变量】,所以外部变量不受影响。
我们来试着做几个练习:
全局作用域中的变量和函数作用域中的变量:
function fn(){
a = 5;
console.log(window.a); // undefined
var a = 3;
console.log(a) // 3
};
fn();
普通的变量提升:
var name = 'Tom';
(function fn(){
if(typeof name === 'undefined'){
var name = 'Jack';
console.log('Hello '+name);
}else{
console.log('GoodBye '+name);
}
})() //Hello Jack
函数同名情况提升:
fn1(); //打印last
function fn1() {
console.log("first");
}
fn1(); //打印last
function fn1() {
console.log("last");
}
变量名与函数名相同:
console.log(a); // function a(){}
function a() {}
var a = 20;
console.log(a); // 20
如有问题,欢迎交流 :
微信:iamyexiu
邮箱:[email protected]
参考
https://my.oschina.net/u/2949632/blog/793898
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions#Function构造函数vs函数声明vs函数表达式