JavaScript中的八股文:作用域与执行上下文

146 阅读5分钟

引言

想象一下,你是一名小厨师,在一家大厨手下学习烹饪技巧。在这个过程中,你会遇到各种食材和烹饪步骤,这些食材和步骤就相当于JavaScript中的变量和函数,今天我们将通过这个厨房的故事来解释JavaScript的核心概念。

11.jpg

变量提升:食材的提前准备

什么是变量提升?

变量提升是JavaScript的一种行为,它把变量和函数的声明移动到它们所在作用域的顶部。这意味着,无论你在哪里声明变量或函数,JavaScript引擎都会认为它们在作用域的最开始就被声明了。

var声明的变量

假设你在厨房里准备做一道菜,需要一种特定的香料。虽然你还没有正式开始烹饪,但你已经把这种香料放在了工作台上。当你开始烹饪时,即使你还没有正式使用这种香料,你也知道它在那里。这就像var声明的变量一样。

console.log(name); // undefined
var name = 'wql';

在这个例子中,name变量在console.log之前并没有被赋值,但它已经被声明了。JavaScript引擎会把它提升到作用域的顶部,并初始化为undefined。这就像是你在准备食材时,已经知道某种香料存在,但还没有具体使用它。

函数声明

再假设厨房里有一个常用的烹饪方法,无论你在哪个步骤需要使用它,你都可以随时调用。这就像函数声明一样。

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

在这里,Hello函数在调用之前已经被完全提升,所以你可以放心地在任何地方调用它。

letconst声明

现在,假设厨房里有一种特别的食材,只有在你正式准备好了之后才能使用。如果在准备之前试图使用这种食材,你会被告知这种食材还不可用。这就像letconst声明的变量一样。

console.log(x); //  ReferenceError: Cannot access 'x' before initialization
let x = 10;

letconst声明的变量不会被提升到作用域的顶部,如果你在声明之前尝试访问它们,会抛出一个引用错误。这是因为这些变量存在一个“暂时性死区”(Temporal Dead Zone, TDZ)。

作用域:厨房的区域划分

什么是作用域?

作用域是指变量或函数可被访问的区域。JavaScript中有两种主要的作用域:全局作用域和局部作用域(也称为函数作用域)。

  • 全局作用域:在全局作用域中声明的变量在整个程序中都可以访问。
  • 局部作用域:在函数内部声明的变量只能在该函数内部访问。

作用域链

假设厨房里有不同的工作台,每个工作台都有自己的食材。如果在某个工作台上找不到需要的食材,你会去其他工作台继续寻找。这就像作用域链一样。

var globalVar = 'global';

function outerFunction() {
    var outerVar = 'outer';
    
    function innerFunction() {
        var innerVar = 'inner';
        console.log(globalVar, outerVar, innerVar); //  global outer inner
    }
    innerFunction();
}
outerFunction();

在这个例子中,innerFunction可以访问globalVarouterVarinnerVar,因为它们都在作用域链上。这就像是你在最内层的工作台上找不到食材时,会去上一层工作台继续寻找。

执行上下文:烹饪的步骤

什么是执行上下文?

执行上下文是JavaScript引擎为了管理代码执行而创建的一个抽象概念。每当进入一个新的作用域时,都会创建一个新的执行上下文。执行上下文包括以下三个主要部分:

  • 变量环境:存储当前作用域内的所有变量和函数声明。
  • 作用域链:用于确定变量的查找路径。
  • this绑定:确定当前执行上下文中this关键字的值。

执行上下文的生命周期

执行上下文的生命周期分为两个阶段:创建阶段和执行阶段。

  • 创建阶段:在这个阶段,JavaScript引擎会进行变量提升、初始化this值、设置作用域链等准备工作。
  • 执行阶段:在这个阶段,实际的代码开始执行,变量被赋值,函数被调用。

调用栈

调用栈是JavaScript引擎用来管理函数调用的一种数据结构。每当调用一个函数时,一个新的执行上下文会被压入调用栈;当函数执行完毕后,对应的执行上下文会被弹出调用栈。

function a() {
    console.log('a');
}
function b() {
    console.log('b');
    a();
}
function c() {
    console.log('c');
    b();
}
c();

在这个例子中调用栈的变化如下: c()被调用,c的执行上下文被压入调用栈;b()被调用,b的执行上下文被压入调用栈; a()被调用,a的执行上下文被压入调用栈; a执行完毕,a的执行上下文被弹出调用栈; b执行完毕,b的执行上下文被弹出调用栈; c执行完毕,c的执行上下文被弹出调用栈。

这就像你在厨房里烹饪时,从一个步骤到另一个步骤,每到一个新步骤都会记录下你的烹饪步骤,完成步骤后会返回上一个步骤继续烹饪。

总结

通过以上的故事和比喻,我们希望你能更好地理解JavaScript中的变量提升、作用域和执行上下文。变量提升使得var声明的变量和函数声明在作用域的顶部可见,而letconst声明的变量则存在暂时性死区。作用域决定了变量的可访问范围,而执行上下文则是JavaScript引擎管理代码执行的重要机制。

希望本文能够帮助你更加轻松地掌握这些概念,为你的JavaScript编程之旅增添更多的乐趣。如果你有任何疑问或需要进一步的帮助,请随时留言交流。祝你成为一名优秀的小厨师和程序员!