JavaScript不仅有变量声明,还有变量提升

起因:👇 一道面试题

最近,一位朋友参加面试时,遇到了这样一道笔试题,引起了我的兴趣:

var foo = 1;
function fn() {
    foo = 3;
    return;
    function foo() {
        // ...
    }
}
fn();
console.log(foo);

这个例子中包含了变量提升,还涉及到了函数声明与变量声明的提升差异。接下来,通过这个例子,我们探讨下JavaScript中的变量提升机制。

变量提升机制解析 🎉

JavaScript代码执行的两阶段

在了解变量提升之前,我们首先需要简单了解JavaScript代码执行的两个阶段:编译阶段和执行阶段。

  • 编译阶段,JavaScript引擎会对代码进行遍历,识别出所有的变量和函数声明,并将它们提升至它们所在作用域的顶部。
  • 紧接着,在执行阶段,代码会按照编写的逻辑顺序从上至下执行。
// 编译阶段
var a = 2; // 声明变量a并分配内存空间
function foo(b) { // 声明函数foo并分配内存空间
  return b * 2;
}
 
// 执行阶段
console.log(foo(a)); // 输出: 4

变量提升的细节

变量提升发生在JavaScript的编译阶段,具体细节我们接着往下看…

变量提升 ✨✨✨

变量提升是指var声明的变量会被提升到其作用域的最顶端。然而,值得注意的是,虽然声明被提升,但赋值操作不会提升。

这意味着,即便是变量在代码中后面被声明,其在编译阶段已经被确认,但直到执行到赋值操作时,这个变量才会被赋予实际的值。

重要提示:变量提升仅针对声明操作,而非赋值操作。

console.log(a); // undefined
var a = 10;
console.log(a); // 10

尽管变量a是在console.log(a)之后被声明的,但由于变量提升的效果,它已经在当前作用域的顶部“存在”了。因此,第一次调用console.log(a)时,输出的是undefined(因为此时尚未赋值),而非抛出引用错误。

函数声明提升 ✨✨✨

与变量提升类似,函数声明(使用function关键字的那种)也会被提升至它们所在作用域的顶端,不过不同的是,函数的提升包括函数名和函数体。

这意味着,在函数声明之前就可以调用该函数,因为在代码执行之前,JavaScript引擎已经知晓了函数的存在。

console.log(foo()); // "foo"
function foo() {
  return "foo";
}
console.log(foo()); // "foo"

函数foo被提升到全局作用域的顶部,因此在函数声明之前调用foo也能够正常获取到函数定义,而不会抛出引用错误。

变量提升与函数声明提升的区别

尽管变量提升和函数声明提升听起来类似,但它们之间存在着本质的区别:

  • 变量提升仅提升变量的声明,而不提升赋值操作。
  • 函数声明提升则将函数的整个声明(包括函数体)都提升到作用域顶部。

下表简要比较了两者的区别:

特征变量提升 (var)函数声明提升 (function)
提升内容仅变量名函数名及函数体
初始化值undefined函数定义
赋值提升
作用域作用域内部作用域内部
常见问题可能导致逻辑混乱较少导致混乱

面试题解析:😤 变量与函数声明的提升冲突

现在,让我们回到文章开头提到的面试题:

var foo = 1;
function fn() {
    foo = 3;
    return;
    function foo() {
        // todo
    }
}
fn();
console.log(foo); // 输出:1

在面试题中,fn 函数内部有一个函数声明 function foo() {},这个声明会被提升到 fn 函数作用域的顶部。

同时,foo = 3 这行代码是一个变量赋值操作,它会找到 fn 函数作用域内的 foo 变量,并尝试给它赋值。但是由于函数声明 function foo() {} 已经提升了,它成为了 fn 函数作用域内的 foo 变量,所以 foo = 3 实际上是在尝试给这个函数赋值,而不是修改全局变量 foo

然而,由于 return 语句紧随 foo = 3,这意味着 foo = 3 这行代码实际上从未被执行。return 语句会导致 fn 函数立即结束,任何在 return 之后的代码都不会执行。因此,foo = 3 这行代码被忽略了,函数内部的 foo 函数也没有被赋值。

foo = 3 这行代码并不会影响全局变量 foo 的值,因为它在 return 语句之后。因此,fn 函数执行后,全局变量 foo 的值仍然是 1。

ES6中的变量提升

let 和 const 关键字

ES6引入的letconst关键字为变量提升带来了新的规则。它们虽然也会被提升,但不会被初始化为undefined,而是处于“暂时性死区”(TDZ)直到实际的声明语句执行。这意味着,在声明之前尝试访问这些变量会抛出引用错误,从而避免了var带来的问题。

console.log(b); // 引用错误:b is not defined
let b = 10;
console.log(b); // 输出:10

大家看一下,这里使用了 let,结果是不是和 var 不一样了!

这里是一个表格,详细对比了 varletconst 在不同特性方面的区别:

特性varletconst
作用域函数作用域(function scope)块级作用域(block scope)块级作用域(block scope)
提升行为变量声明被提升,初始化不提升提升,但存在暂时性死区提升,但存在暂时性死区
重声明同一作用域内可重声明同一作用域内不能重声明同一作用域内不能重声明
可变性可重新赋值可重新赋值声明后不能重新赋值
使用建议避免使用需要变量可变时使用值不变时使用,推荐用于常量

说明:

  • var 的函数作用域意味着它在整个函数中都可见,甚至在声明前。
  • letconst 的块级作用域使它们只在定义它们的代码块(如:循环、条件语句等)内可见。

箭头函数 和 class

在 ES6 中,除了传统的函数声明,还引入了箭头函数和 class 语法。这些新的函数和类声明也遵循提升规则,但是与 ES5 中的函数声明有一些不同。

箭头函数是表达式,它们不会被提升到作用域的顶部。如果你尝试在声明之前调用箭头函数,你会得到一个引用错误。

console.log(arrowFn()); // ReferenceError: arrowFn is not defined
const arrowFn = () => console.log('Hello from arrow function');

arrowFn 作为一个常量声明(使用 const),它不会被提升。因此,尝试在声明之前调用它会导致引用错误。

Class 声明也不会被提升。class 是一种新的语法,用于创建构造函数和原型继承的语法糖。与箭头函数一样,如果你尝试在声明之前访问 class,你会得到一个引用错误。

let peter = new Person(); // ReferenceError: Cannot access 'Person' before initialization
class Person {
  constructor(name) {
    this.name = name;
  }
}

总结,在 ES6 中,函数声明提升的规则有所改变:

  • 传统函数声明:仍然会被提升到作用域的顶部,包括函数名和函数体。
  • 箭头函数:不会被提升,因为它们是表达式形式的函数。
  • Class 声明:不会被提升,因为 class 是一种新的语法,用于创建构造函数和原型继承的语法糖。

规避变量提升陷阱的策略 🔖🔖🔖

那么,了解了变量提升的机制和潜在问题后,我们可以采取以下措施规避陷阱:

1. 优先使用letconst

它们解决了 var 的变量提升问题,可以避免var带来的变量提升问题。

// 使用 var
for (var i = 0; i < 3; i++) {
    var bar = i;
    setTimeout(function() {
        console.log(bar); // 2, 2, 2
    }, 1000);
}
// 使用 let 和 const
for (let i = 0; i < 3; i++) {
    let bar = i;
    setTimeout(function() {
        console.log(bar); // 0, 1, 2
    }, 1000);
}

2. 将函数声明放在逻辑的顶部

如果你在函数内部使用函数声明,确保将这些声明放在函数体的顶部,这样就不会因为变量提升而导致意外的行为。

function example() {
    function foo() {
        // 函数声明被提升到 example 函数的顶部
    }
    // 其他逻辑
}

foo 函数声明被放在 example 函数体的顶部,这样可以避免因为函数声明提升而导致的混淆。

3. 使用立即执行函数表达式(IIFE)

立即执行函数表达式可以创建独立作用域,隔离变量。例如:

(function() {
    var foo = 'Hello World';
    // foo 在这个函数作用域内是局部的,不会影响到外部作用域
})();
// foo 在这里是不可访问的

IIFE 创建了一个新的作用域,foo 在这个作用域内是局部的,不会影响到外部作用域。

4. 使用函数表达式而不是函数声明

函数表达式不会被提升,因此你可以控制函数的创建和执行时机。例如:

const myFunction = function() {
    // 函数表达式不会被提升
};

myFunction 是一个函数表达式,它不会被提升到顶部,因此你可以控制它的创建和执行时机。

总结 🎉🎉🎉

在开发过程中,我们通常按照从上到下的顺序编写代码逻辑,而不去刻意考虑变量提升和函数声明提升。为了避免提升带来的潜在问题,我们可以考虑以下最佳措施:

  1. 优先使用letconst来声明变量。这样可以避免变量提升导致的意外行为,因为 letconst 声明的变量在赋值之前是不可访问的。
  2. 在需要的时候才声明函数和类。避免在作用域顶部之外的地方引用尚未声明的函数或类。

这样,我们可以在编写代码时最大程度地保持逻辑的清晰和正确性,减少由变量提升和函数声明提升引起的错误。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值