🌟🌟🌟【大坑】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. 在需要的时候才声明函数和类。避免在作用域顶部之外的地方引用尚未声明的函数或类。

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

相关推荐
测试199828 分钟前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
栈老师不回家40 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
马剑威(威哥爱编程)1 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app