读懂JS核心(二)--变量提升与函数声明提升

1,545 阅读6分钟

变量提升与函数声明提升

在上一节中,我们详细介绍了JS中的执行栈和执行上下文,还简单解释了出现变量提升的原因。这一节,我们会对变量声明、函数声明做详细的介绍,深入地了解变量提升和函数声明提升,最后我们还会介绍一下class的声明,以及let、const是如何工作的。

首先,我们来看一个问题:

console.log(a);
function a() {
    console.log('fa')
}

console.log(b);
var b = 'b';

上述代码执行后会有怎样的打印结果呢?

答案揭晓: ƒ a() { console.log('fa') }undefined

为什么会有这样的结果呢?我们首先需要了解一下JavaScript中的预处理机制。

预处理机制

JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义。

var声明--变量提升

var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。

// 示例
console.log(b);
var b = 'b';

在上述代码中,JavaScript 执行前会先对var b = 'b'做预处理,在全局环境声明了一个值为undefinedb变量,所以在第一行的console.log(b)时,会打印出undefined

立即执行的函数表达式(IIFE

因为早年 JavaScript 没有 let 和 const,只能用 var,又因为 var 除了脚本和函数体都会穿透,人民群众发明了“立即执行的函数表达式(IIFE)”这一用法,用来产生作用域。

// 为文档添加了 20 个 div 元素,并且绑定了点击事件,打印它们的序号
for(var i = 0; i < 20; i ++) {
    void function(i){
        var div = document.createElement("div");
        div.innerHTML = i;
        div.onclick = function(){
            console.log(i);
        }
        document.body.appendChild(div);
    }(i);
}

我们通过 IIFE 在循环内构造了作用域,每次循环都产生一个新的环境记录,这样,每个 div 都能访问到环境中的 i。

如果我们不用 IIFE:


for(var i = 0; i < 20; i ++) {
    var div = document.createElement("div");
    div.innerHTML = i;
    div.onclick = function(){
        console.log(i);
    }
    document.body.appendChild(div);
}

这段代码的结果将会是点每个 div 都打印 20,因为全局只有一个 i,执行完循环后,i 变成了 20。

有了let关键词之后,可以用let来声明块级作用域,于是我们就可以不用IIFE了。

function声明--函数声明提升

在全局(脚本、模块和函数体),function 声明表现跟 var 相似,不同之处在于,function 声明不但在作用域中加入变量,还会给它赋值。

// 示例
console.log(a);
function a() {
    console.log('fa')
}

在上述代码中,JavaScript 执行前会先对function a (){...} 做预处理,在全局环境声明了一个值为ƒ a() { console.log('fa') }a变量,所以在第一行的console.log(a)时,会打印出ƒ a() { console.log('fa') }

注意:当function 声明出现在 if 等语句中的情况有点复杂,它仍然作用于脚本、模块和函数体级别,在预处理阶段,仍然会产生变量,但它不再被提前赋值:

// 示例
console.log(foo);
if(true) {
    function foo(){

    }
}

这段代码得到 undefined,如果没有函数声明,则会抛出错误。这说明 function 在预处理阶段仍然发生了作用,在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段。出现在 if 等语句中的 function,在 if 创建的作用域中仍然会被提前,产生赋值效果。

console.log(foo);
if(true) {
    console.log(foo);
    function foo(){

    }
}

这段代码得到 undefinedƒ foo(){}

class声明

class声明在全局的行为与function、var都不一致,在class声明前使用class名,会抛出错误。

// 示例
console.log(C);
class C {
}

上述代码会抛出异常Uncaught ReferenceError: c is not defined,这个行为很像是class没有预处理,但事实上并非如此。

class 声明也是会被预处理的,它会在作用域中创建变量,并且要求访问它时抛出错误,class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用。

// 示例
const a = 2;
if(true){
    console.log(a); //抛错
    class a {

    }
}

这段代码在全局声明了一个值为2的a变量,但是在函数块中,又进行了一次class声明。class声明被预处理,在if的作用域中不会再访问外部声明的a变量,而是访问class声明的a类,所以抛出错误。

let、const

let 和 const 是都是变量的声明,它们的特性非常相似,与var声明有着很大的差异。

let 和 const 声明虽然看上去是执行到了才会生效,但是实际上,它们还是会被预处理。如果当前作用域内有声明,就无法访问到外部的变量。

备注:let、const 与 class的声明方式类似

//示例
const a = 2;
if(true){
    console.log(a); //抛错
    const a = 1;   
}

在if的作用域中,const声明被预处理,JS引擎就已经知道后面的代码将会声明变量a,从而不允许我们访问外层作用域中的变量a。

私货课堂

先提出一个小问题:如果在同一个作用域中,同时存在函数声明和变量声明,他们的优先级是什么样子的?

console.log(a);
var a = 'varA';
console.log(a);
function a() {
    console.log('funA');
}
a();

上述代码会依次输出ƒ a() { console.log('funA'); }varA、抛出异常Uncaught TypeError: a is not a function

这是因为函数声明会优先于var变量声明,在第一行中,读取的a是函数声明(函数声明时会提前赋值),所以会打印ƒ a() { console.log('funA'); };继续运行到第二行时,因为已经声明过a了,所以会将varA赋值给变量a,这时a的值为varA。所以第三行会打印varA;继续运行到4~6行时,因为已经声明执行过function a(){}语句,所以跳过;继续执行到最后一行时,此时的a是字符串varA,而非函数,所以会抛出异常:Uncaught TypeError: a is not a function

在同一个作用域中,如果同时存在函数声明和变量声明,只需要记住两句话即可解决问题:

  • 函数声明会优先于var变量声明
  • 同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明

放几道有意思的题目,供大家更好地理解:

//Example1
foo;
var foo = function () {
    console.log('foo1');
}

foo();

var foo = function () {
    console.log('foo2');
}

foo();
//Example2
foo();
function foo() {
    console.log('foo1');
}

foo();

function foo() {
    console.log('foo2');
}

foo();
//Example3
foo();
var foo = function() {
    console.log('foo1');
}

foo();

function foo() {
    console.log('foo2');
}

foo();

总结

在这一节,我们详细介绍了JS中变量声明提升、函数声明提升,还补充了class声明和let/const的声明方式。接下来我们会继续介绍其他JS核心知识。

我是何以庆余年,如果文章对你起到了帮助,希望可以点个赞,谢谢!

如有问题,欢迎在留言区一起讨论。