20分钟教会你 “变量提升、调用栈和作用域链”

JS 核心概念详解:变量提升、调用栈与作用域链

引言

JavaScript 作为一门灵活且强大的脚本语言,在 Web 开发中占据着不可替代的地位。然而,要想真正掌握这门语言,深入了解其内部机制是非常必要的。本文将聚焦于 JavaScript 中的三个关键概念------变量提升、调用栈以及作用域链,并通过具体的代码示例来详细解释这些概念。

变量提升

在 JavaScript 中,所有的变量和函数声明都会被 "提升" 到它们所在作用域的顶部。这意味着无论你在代码中的哪个位置声明变量或函数,JavaScript 引擎都会认为它们是在当前作用域的开始处被声明的。但是,只有声明会被提升,而赋值操作则不会。对于函数声明,情况稍有不同,整个函数体都会被提升至作用域顶部。

代码示例

javascript 复制代码
console.log(name); // 输出: undefined
var name = 'John Doe';

// 解释:虽然 name是在 console.log之后才被赋值,但由于变量提升,
// 实际上 JavaScript引擎在执行这段代码前已经处理了 var name的声明,
// 因此 name的初始值为 undefined


sayHello(); // 输出: Hello, World!

function sayHello() {
    console.log('Hello, World!');
}

// 解释:因为函数声明 sayHello()也被提升了,所以在调用 sayHello()时,
// 函数体已经存在于内存中,可以正常执行

核心知识点:

  • 变量声明与初始化的区别: 变量声明会提升到其作用域的顶部,但初始化(赋值)不会。
  • 函数声明与函数表达式: 函数声明也会被完全提升(即声明和定义都会被提升),而函数表达式只有声明部分会被提升。
  • let 和 const 的暂时性死区: 使用 letconst 声明的变量虽然也会被提升,但在它们的实际声明点之前是不可访问的,这段区域被称为"暂时性死区"。

注意事项:

  • 避免在变量声明前使用变量,即使使用 var 声明也是如此,因为这可能会导致意外的结果。
  • 使用 letconst 时要特别注意暂时性死区,不要尝试在声明之前读取或写入这些变量。

调用栈

调用栈是 JavaScript 中用于跟踪程序执行过程中的函数调用顺序的数据结构。每当一个函数被调用时,一个新的栈帧就会被创建并压入调用栈的顶部。当函数执行完毕后,相应的栈帧会被弹出。

代码示例

javascript 复制代码
function a() {
    b();
}
function b() {
    c();
}
function c() {
    console.log('Function c is called.');
}
a(); // 调用栈: a -> b -> c

在这个例子中,调用栈依次为abc。当c函数执行完毕后,它从调用栈中弹出,然后是b,最后是a

核心知识点:

  • 同步执行: 调用栈是一个后进先出的数据结构,确保了代码的同步执行。
  • 错误处理: 当函数中抛出未捕获的异常时,调用栈可以帮助调试者追踪错误发生的位置。
  • 堆栈溢出: 如果递归调用过深或者循环调用不当,可能导致调用栈溢出,进而引发程序崩溃。

注意事项:

  • 编写递归函数时应考虑最大递归深度,避免造成调用栈溢出。
  • 使用异步编程模式(如回调函数、Promises、async/await)可以减少调用栈的压力。

作用域链

作用域链决定了变量如何在不同的作用域之间进行查找。每个执行上下文都有一个与之关联的作用域链,它由一系列的变量对象组成。当尝试访问一个变量时,JavaScript 引擎会从当前执行上下文的作用域链开始逐层向上查找,直到找到该变量或到达全局作用域。

代码示例

javascript 复制代码
var global = 'I am global';

function outerFunction() {
    var outer = 'I am outer';
    
    function innerFunction() {
        var inner = 'I am inner';
        console.log(global); // I am global
        console.log(outer);  // I am outer
        console.log(inner);  // I am inner
    }
    innerFunction();
}
outerFunction();

在这个例子中,innerFunction可以访问globalouterinner。这是因为它的作用域链包括了自身的局部变量对象、outerFunction的局部变量对象以及全局变量对象。

核心知识点:

  • 词法作用域: JavaScript 中的作用域是在编译阶段确定的,而不是在运行时。这意味着函数内的变量查找遵循的是函数被定义时的环境,而非调用时的环境。
  • 闭包: 当一个函数可以记住并访问其自身作用域之外的变量时,就形成了闭包。闭包允许函数访问其创建时的作用域,即使这个函数在其父作用域之外被调用。

注意事项:

  • 尽量减少不必要的变量访问,特别是那些需要经过长作用域链才能访问的变量。
  • 注意内存泄漏的风险,尤其是在使用闭包时,长时间保持对大对象的引用可能会阻碍垃圾回收器释放内存。

结合实例

这里提供一个综合运用上述概念的例子,包括变量提升、调用栈和作用域链:

javascript 复制代码
var globalVar = 'global';

function outer() {
    var outerVar = 'outer';

    function inner() {
        var innerVar = 'inner';
        console.log(globalVar); // 输出: global
        console.log(outerVar);  // 输出: outer
        console.log(innerVar);  // 输出: inner
    }
    inner();
}
outer();

// 下面的代码将展示变量提升的不同行为
console.log(x); // 输出: undefined
var x = 1;

console.log(y); // 抛出 ReferenceError: Cannot access 'y' before initialization(初始化前无法访问 'y')
let y = 2;

function hoistedFunc() {
    console.log('这是一个提升的函数声明。');
}
hoistedFunc();

notHoistedFunc(); // 抛出 TypeError: notHoistedFunc is not a function
const notHoistedFunc = function() {
    console.log('这是一个函数表达式。');
};

分析

1. 变量提升
  • 全局作用域

    • var globalVarvar x的声明会被提升到全局作用域的顶部,但它们的初始化不会。
    • let yconst notHoistedFunc的声明也会被提升,但它们的初始化不会,且在声明之前访问它们会抛出错误(这是由于暂时性死区的存在)。
    • function hoistedFunc()作为一个函数声明,会被完全提升到其作用域的顶部,包括其定义。
  • outer 函数作用域

    • var outerVar的声明会被提升到outer函数作用域的顶部,但其初始化不会。
    • function inner()作为一个函数声明,会被完全提升到outer函数作用域的顶部,包括其定义。
  • inner 函数作用域

    • var innerVar的声明会被提升到inner函数作用域的顶部,但其初始化不会。
2. 调用栈
  • 创建outer函数的执行上下文,并将其推入调用栈。
  • outer函数内部,调用inner()
  • 创建inner函数的执行上下文,并将其推入调用栈。
  • 执行inner函数中的代码。
  • inner函数执行完毕,其执行上下文从调用栈中弹出。
  • outer函数执行完毕,其执行上下文从调用栈中弹出。
3. 作用域链
  • inner 函数

    • inner函数可以访问其自身作用域内的变量innerVar
    • inner函数可以访问其外层作用域outer函数中的变量outerVar
    • inner函数可以访问全局作用域中的变量globalVar
  • outer 函数

    • outer函数可以访问其自身作用域内的变量outerVar
    • outer函数可以访问全局作用域中的变量globalVar
  • 全局作用域

    • 全局作用域可以访问其自身的变量globalVarxy

总结

  • 变量提升:所有变量和函数声明都会被提升到它们所在作用域的顶部,但仅限于声明部分。
  • 调用栈:用于跟踪函数调用的顺序,确保每个函数都能正确执行并返回。
  • 作用域链:决定变量如何在嵌套作用域中被查找,保证了代码的逻辑性和可预测性。

通过本文的介绍,希望能帮助读者更深刻地理解 JavaScript 中的这些核心概念,进而提高编码技巧和解决问题的能力。掌握这些基础知识对于成为一名优秀的前端开发者来说至关重要。

相关推荐
虾球xz20 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇25 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒29 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员44 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express