变量提升和函数提升的优先级问题

写在前面的话

⭐ 首先在最开始介绍一下,JS中存在提升的原因?
JS引擎在读取代码时,进行两步操作:

  • 第一步:对整个JS代码进行读取解析
  • 第二步:代码执行

提升只会发生在第一步中,所以在代码执行之前,浏览器的解析器遇到变量名和函数声明时,会将其提升至当前作用域的最前面。

注意:

  1. 上述提及的变量提升,只针对var声明的变量,使用letconst声明的变量不存在提升。
  2. JS引擎严格规定句首用 function 的才能算作函数声明,其余的杂牌声明只能算作函数表达式.

变量提升 & 函数提升

  • 变量提升
    1
    2
    3
    console.log(a);//undefined
    var a = 'hi';
    console.log(a);//'hi'
    上述代码相当于:
    1
    2
    3
    4
    var a; // 只定义不声明就默认是undefined
    console.log(a);//undefined
    a='hi';
    console.log(a);//hi
  • 函数提升
    1
    2
    3
    4
    5
    6
    7
    8
    9
    console.log(a);    //f a(){console.log('hi');}
    console.log(b); //undefined

    function a() {
    console.log('hi');
    }
    var b = function() {
    console.log('ok');
    }
    上述代码相当于:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function a () {
    console.log('hi');
    }
    var b;//undefined;
    console.log(a); //f a(){console.log('hi');}
    console.log(b); //undefined
    b = function () {
    console.log('ok');
    }

二者的优先级

  • 函数提升会优先于变量提升,而且不会被同名的变量覆盖。
  • 但是,如果这个同名变量被赋值了,那函数变量就会被覆盖。
  • 当二者同时存在时,会先指向函数声明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    console.log(a);    //f a() {...}
    console.log(a()); //2
    var a = 1;
    function a() {
    console.log(2);
    }
    console.log(a); //1
    a = 3;
    console.log(a()); //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()
    上述代码相当于:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function a() {
    console.log(2);
    }
    var a;
    console.log(a); //f a() {...}
    console.log(a()); //2
    a = 1;
    console.log(a); //1
    a = 3;
    console.log(a()); //报错,现在的函数a已经被赋值过后的变量a给覆盖了,无法再调用a()

牛客网上的一道题目:
以下代码执行之后,控制台的输出是:

1
2
3
var a = 10;
function a(){}
console.log(typeof a);

答案是:Number。
原因是函数提升优先级高于变量提升,先执行函数提升,再执行变量提升。执行完函数提升之后,又执行了变量提升并进行赋值操作。此时函数变量被覆盖。

1
2
3
var a;
function a () { };
console.log(typeof a);

答案是:function。
此时先执行了函数提升,又执行了变量提升,但是变量没有进行赋值,因此指向函数。【当二者同时存在时,会先指向函数声明】

为什么会有提升?

Dmitry Soshnikov早些年在twitter上提出关于变量提升的问题,同时Brendan Eich给出了一些解答:


答案的大致意思是由于第一代JS虚拟机中的抽象纰漏导致的,编译器将变量放入栈槽内并编入索引。然后在(当前作用域)入口处将变量名绑定到栈槽内的变量上。(注:这里提到的抽象是计算机术语,是对内部发生的更加复杂的事情的一种简化。)
随后,Dmitry Soshnikov又提到了函数提升,他提到了函数的相互递归。

Brendan Eich又对其进行了解答。

大致意思是函数提升就是为了解决函数相互递归问题,大体上可以解决像ML语言这样自下而上的顺序问题。


对于函数递归的一些阐述:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 验证偶数
function isEven(n) {
if (n === 0) {
return true;
}
return isOdd(n - 1);
}

console.log(isEven(2)); // true

// 验证奇数
function isOdd(n) {
if (n === 0) {
return false;
}
return isEven(n - 1);
}

对于中间语句console.log(isEven(2));而言,它在执行的时候,isOdd函数还没有被声明,如果没有函数提升,那就必然会报错,这段程序就会陷入一种死循环的状态。

因此,在JS设计之初,就人为加入了函数提升。在代码执行时,将函数提升至当前作用域的顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 验证偶数
function isEven(n) {
if (n === 0) {
return true;
}
return isOdd(n - 1);
}

// 验证奇数
function isOdd(n) {
if (n === 0) {
return false;
}
return isEven(n - 1);
}

console.log(isEven(2)); // true

最后,Brendan Eich还对变量提升和函数提升做了总结:


大概是说,变量提升是人为实现的问题,而函数提升在当初设计时是有目的的。

后续的补充

在stackoverflow中用户提出一道相关问题Javascript function scoping and hoisting


大意就是他阅读了Ben Cherry写的一篇关于JS范围和提升的文章,对其中给出的一个案例存在疑惑,为什么代码执行结果是浏览器输出1?

然后下面有人对其问题进行了解答。

函数提升意味着函数被移动到作用域的开头。(红框中的代码的执行过程实际上是绿框中代码的顺序)

文章作者: qinwei
文章链接: https://qw-null.github.io/2022/03/09/变量提升和函数提升的优先级问题/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 QW's Blog