🌟🌟🌟【大坑】JavaScript不仅有变量声明,还有变量提升

前言:在JavaScript的执行流程中,变量提升是一个经常被忽略的点。它不仅关系到变量的访问权限,还可能导致代码逻辑的执行混乱(大坑)。通过本篇文章,我们将深入探讨变量提升的机制,思考如何规避可能遇到的陷阱。

起因:👇 一道面试题

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

js 复制代码
var foo = 1;
function fn() {
    foo = 3;
    return;
    function foo() {
        // ...
    }
}
fn();
console.log(foo);

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

变量提升机制解析 🎉

JavaScript代码执行的两阶段

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

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

变量提升的细节

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

变量提升 ✨✨✨

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

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

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

js 复制代码
console.log(a); // undefined
var a = 10;
console.log(a); // 10

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

函数声明提升 ✨✨✨

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

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

js 复制代码
console.log(foo()); // "foo"
function foo() {
  return "foo";
}
console.log(foo()); // "foo"

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

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

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

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

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

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

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

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

js 复制代码
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带来的问题。

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

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

箭头函数 和 class

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

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

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

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

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

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

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

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

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

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

1. 优先使用letconst

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

js 复制代码
// 使用 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. 将函数声明放在逻辑的顶部

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

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

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

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

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

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

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

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

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

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

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

总结 🎉🎉🎉

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

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

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

相关推荐
PAK向日葵9 分钟前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT4 小时前
promise & async await总结
前端
Jerry说前后端4 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化