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 中的这些核心概念,进而提高编码技巧和解决问题的能力。掌握这些基础知识对于成为一名优秀的前端开发者来说至关重要。

相关推荐
Martin -Tang14 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发15 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html