从前端初学者到高手的必经之路:深入理解JavaScript变量提升与执行上下文

508 阅读7分钟

前言

在编程的海洋中,JavaScript无疑是一艘航行速度极快的帆船。它灵活、强大,几乎无处不在——从网页前端到服务器后端,再到移动应用开发,JavaScript的身影随处可见。然而,这艘快速的帆船也有其复杂的一面,特别是对于那些刚刚踏上JavaScript之旅的开发者来说,变量提升与执行上下文这两个概念常常让人感到困惑。

想象一下,你正在编写一段简单的代码,却突然遇到了“变量未定义”的错误,或者函数的行为与预期不符。这时,你可能会问自己:“为什么我的代码会出现这样的问题?”答案往往就隐藏在变量提升与执行上下文的背后。本文将带你深入探索这两个核心概念,帮助你从初学者成长为一名真正的JavaScript高手。

无论你是刚刚接触JavaScript的新手,还是已经有一定经验的开发者,本文都将为你提供宝贵的知识和见解。通过详细的解释和实例分析,你将学会如何避免常见的陷阱,写出更加高效和安全的代码。下面,本文将从几个案例来讲解变量提升执行上下文这两个核心的知识点。

实例1:

console.log(name) //   变量提升 提升到最上面
var name="wql" 

大家能否不先执行结果,先去分析一下会输出什么呢?报错?还是为NULL呢?

结果:

输出为 undefined,那为什么是 undefined,因为在代码执行之前,会进行一个很短的编译阶段,目的是为了给变量赋一个初始值undefined,以占据一定的空间,所以最后不会报错,并且输出了 undefined

实例2:

console.log(fun)
function fun() {
    
}

当我们把一个变量变成一个函数之后呢?

结果:

[Function: fun],与变量不同的是,函数在编译阶段,赋予的是函数的一个值

实例3:

console.log(a)
let a=1

当用let和const去定义一个变量时,还是会输出undefined吗?

结果:

答案是错误的,因为此时出现了暂时性死区,代码将会报错。

JavaScript中的变量提升

JavaScript是一门弱类型的脚本语言,这意味着你无需在声明变量时指定其类型。在JavaScript中,变量的声明会被“提升”至其所在作用域的顶部,但请注意,这并不意味着变量的初始化也会被提升。

  • var变量的提升:当使用var关键字声明变量时,该变量会在代码执行前被提升到当前作用域的顶部,并被赋予一个初始值undefined。这意味着即使你在声明变量之前就尝试访问它,也不会抛出引用错误,而是返回undefined

  • 函数的提升:与var声明的变量类似,函数声明同样会被提升。不过,不同的是,函数声明不仅仅是名称被提升,整个函数定义都会被提升到作用域的顶部。

  • letconst的暂时性死区:ES6引入了letconst来声明变量,这些变量不会被提升。如果你试图在声明之前访问它们,将会引发一个引用错误。这是因为letconst声明的变量存在所谓的“暂时性死区”,即从块开始直到变量声明为止的区域。

  • js 代码要执行 <- 执行机制(调用栈)<- 函数入栈(操作系统)<- 执行上下(代码和变量声明的关系) <- 作用域+ 变量提升+ 可运行代码 <- 运行阶段

看完这些知识点,你可能还一头雾水,下面我们讲讲执行上下文

执行上下文与调用栈

每当JavaScript引擎开始执行一段代码(无论是全局代码还是函数内的代码),它首先会创建一个执行上下文。这个上下文包含了代码执行所需的所有信息,比如变量对象、作用域链以及this的值等。

  • 全局执行上下文:当页面加载或Node.js程序启动时,首先创建的就是全局执行上下文。在这个上下文中,所有的全局变量和函数都被初始化。
  • 函数执行上下文:每当调用一个函数时,都会为其创建一个新的执行上下文。这个上下文会被添加到调用栈的顶部,成为当前活动的执行上下文。当函数执行完毕后,其执行上下文就会从调用栈中移除。
  • 调用栈的作用:一般来说,先全局执行上下文后,将全局执行上下文压入栈底,如果有函数,再执行的函数执行上下文。
  1. 每当一个函数被调用时,JavaScript引擎会为该函数创建一个新的执行上下文,并将其压入调用栈的顶部。这个执行上下文包含了函数执行所需的所有信息,如变量环境、作用域链和this值。当函数执行完毕后,其执行上下文从调用栈中弹出,控制权返回给前一个执行上下文。
  2. 调用栈在调试和错误处理中起着重要作用。当程序抛出异常或发生错误时,调用栈可以提供详细的堆栈跟踪信息,帮助开发者定位问题发生的函数和调用路径。堆栈跟踪通常显示了从错误发生处到程序入口的完整调用链路。
  3. 调用栈还有一个重要的作用是防止无限递归。如果一个函数无限递归调用自身,调用栈会不断增长,最终导致栈溢出(Stack Overflow)。JavaScript引擎会抛出“RangeError: Maximum call stack size exceeded”错误,提示开发者存在无限递归的问题。

作用域与变量查找

作用域决定了变量的可见性和生命周期。JavaScript中有全局作用域、函数作用域以及ES6之后引入的块级作用域。当你在代码中引用一个变量时,JavaScript引擎会按照以下顺序查找:

  1. 当前作用域
  2. 外层作用域
  3. 全局作用域

如果在整个作用域链中都找不到该变量,则会抛出一个引用错误。

实例分析

让我们通过一个具体的例子来加深对上述概念的理解:

var a = 1; 
var c = 3;

function fn(a) {
    var a = 2;
    function a() {}
    var b = a;
    console.log(a); // 2
    console.log(c); // 3
}

fn(3);

流程讲解

将代码拆解:

var a = 1
var c = 3
function fn(a) {
    var a; // 变量声明被提升
    function a() {} // 函数声明被提升
    a = 2; // 变量赋值
    var b; // 变量声明被提升
    b = a; // 变量赋值
    console.log(a);
    console.log(c);
}


  1. 初始状态

    • 全局作用域:a = 1c = 3
  2. 调用 fn(3)

    • 在函数 fn 的作用域中,变量 ab 被声明,函数 a 也被声明。但由于函数声明的优先级高于变量声明,所以 a 最终会被认为是一个函数。
    • a = 2 这行代码会将 a 重新赋值为 2。此时,a 不再是函数,而是一个数值 2
    • 变量 b 被赋值为 2
    • 输出 a 的值:2
    • 输出 c 的值:3
  3. 图解

屏幕截图 2024-11-20 235738.png

结语

通过对变量提升和执行上下文的深入探讨,我们不仅能够写出更加高效和安全的代码,还能够更好地理解JavaScript这门语言的工作原理。希望本文能够帮助你在JavaScript的道路上更进一步,成为一名真正的高手。