深入理解作用域【JavaScript】

一、作用域的内部原理

JavaScript 的作用域机制是理解变量如何被访问和存储的重要概念。下面详细介绍作用域的内部原理,包括编译、执行、查询、嵌套和异常处理这五个步骤。

1. 编译

在 JavaScript 的执行过程中,首要的步骤是编译。尽管JavaScript是解释性语言,但现代 JavaScript 引擎(如 V8)在执行代码前会先进行编译。编译阶段主要完成以下几项工作:

  • 词法分析:将源代码分解成语法单元(tokens)。
  • 语法分析:将 tokens 结构化为抽象语法树(AST)。
  • 作用域分析:在这一步,编译器会确定每个变量和函数的作用域位置,并建立作用域链。这里会生成一个环境记录(environment record),用于存储作用域内的变量信息。

编译阶段的结果为代码的执行做好准备,包括确定变量的可访问性和生命周期。

通过一个完整的实例来说明 JavaScript 的编译过程。假设我们有以下 JavaScript 代码:

let x = 5;
function add(a, b) {
    return a + b;
}

let result = add(x, 3);
console.log(result);  // 输出结果:8

 1.1 编译阶段 

1.1.1 词法分析(Lexical Analysis)

词法分析
在这一步,源代码被分解成语法单元(tokens)。每个元素(关键字、标识符、常量等)被识别并分类。

示例代码的 tokens

  • let
  • x
  • =
  • 5
  • ;
  • function
  • add
  • (
  • a
  • ,
  • b
  • )
  • {
  • return
  • a
  • +
  • b
  • }
  • ;
  • let
  • result
  • =
  • add
  • (
  • x
  • ,
  • 3
  • )
  • ;
  • console
  • .
  • log
  • (
  • result
  • )
  • ;

1.1.2 语法分析(Syntax Analysis)

语法分析
在这一步,tokens 被组织成一个抽象语法树(AST)。AST 是代码的层次化表示,能更清晰地反映出程序的结构。

示例代码的 AST 简化表示

Program
├── VariableDeclaration (x)
│   └── Literal (5)
├── FunctionDeclaration (add)
│   ├── Identifier (a)
│   ├── Identifier (b)
│   └── BlockStatement
│       └── ReturnStatement
│           └── BinaryExpression (+)
│               ├── Identifier (a)
│               └── Identifier (b)
└── VariableDeclaration (result)
    └── CallExpression (add)
        ├── Identifier (x)
        └── Literal (3)
└── ExpressionStatement (console.log)
    └── Identifier (result)

1.1.3 作用域分析(Scope Analysis)

作用域分析
在这一步,编译器会确定变量和函数的作用域,并建立作用域链。编译器会生成环境记录(environment record),记录每个变量的作用域信息。

示例代码的环境记录

Global Environment Record
├── x: 5
├── add: Function
│   ├── Parameters:  
│   │   ├── a
│   │   └── b
│   └── Scope: add's local scope
└── result: add(x, 3)

作用域链

  • 全局作用域包含 x 和 add
  • add 函数的作用域包含参数 a 和 b
  • result 在全局作用域中定义。

2. 执行

编译完成后,进入执行阶段。在此阶段,代码按顺序执行。JavaScript 引擎会根据生成的环境记录,处理变量的声明和赋值。在执行过程中会遵循以下原则:

  • 作用域链:当执行上下文被创建时,会创建一个作用域链,包含当前执行上下文的环境记录和其父级的环境记录,这样就可以实现层级结构,确定变量的作用范围。
  • 变量提升:在执行上下文被创建时,变量和函数的声明会被提升到作用域的顶部,而赋值在执行到对应声明的那一行时才会被应用。

let x = 5;
function add(a, b) {
    return a + b;
}

let result = add(x, 3);
console.log(result);  // 输出结果:8

2.1 代码执行阶段

编译完成后,JavaScript 引擎进入执行阶段。以下是代码的执行过程:

2.1 变量声明

  • let x = 5;:全局作用域中定义变量 x,并赋值为 5
  • let result;:全局作用域中定义变量 result

2.2 函数定义

  • function add(a, b) { return a + b; }:定义函数 add,并在全局作用域中注册。

2.3 函数调用

  • result = add(x, 3);:调用 add 函数,传递参数 x 和 3
  • 在 add 函数内部,a 被赋值为 5b 被赋值为 3
  • 执行 return a + b;,结果为 8
  • result 被赋值为 8

2.4 输出结果

  • console.log(result);:在控制台输出 result,即 8

3. 查询

在执行某行代码时,JavaScript 引擎需要查找变量。查询过程如下:

  • 引擎首先检查当前执行上下文的环境记录是否包含该变量。
  • 如果找不到,查找上一级的环境记录,依此类推,直到找到该变量或到达全局上下文。
  • 如果最终仍未找到,则会抛出 ReferenceError

这种查找机制也解释了“光照作用域”的概念,即局部变量优先于全局变量

在 JavaScript 中,LHS(Left-Hand Side)查询和 RHS(Right-Hand Side)查询是两种不同的变量查找方式。

3.1 LHS 查询

LHS 查询发生在变量被赋值的时候。换句话说,当引擎在执行代码时需要查找一个变量的位置以便对其进行赋值操作,这时的查找就是 LHS 查询。

let a = 2;

在这个例子中,引擎需要找到变量 a 以便对其赋值 2。这个查找过程就是 LHS 查询。

LHS 查询的目的是找到变量容器本身,以便对其进行赋值

3.2 RHS 查询

RHS 查询发生在引擎需要获取变量的值时。换句话说,当引擎在执行代码时需要获取变量的值,这时的查找就是 RHS 查询。

console.log(a);

在这个例子中,引擎需要找到变量 a 的值以便将其传递给 console.log 函数。这个查找过程就是 RHS 查询。

RHS 查询的目的是获取变量的当前值

3.3 区别总结

  • LHS 查询:目的是找到变量容器本身,以便对其进行赋值
  • RHS 查询:目的是获取变量的当前值

3.4 示例说明

考虑以下代码:

function foo(a) {
    console.log(a);
}

foo(2);

在这个代码片段中,发生了以下操作:

  1. 函数调用 foo(2)

    • 这里发生了 RHS 查询,查找 foo 函数的定义。
    • 然后是 LHS 查询,查找 a 以便对 a 赋值 2
  2. console.log(a)

    • 这里发生了 RHS 查询,查找 console.log 函数的定义。
    • 然后再次发生 RHS 查询,查找 a 的值,以便将其传递给 console.log 函数。

4. 嵌套(重要!)

JavaScript 支持嵌套的函数定义。每当一个函数被调用时,都会创建一个新的执行上下文。嵌套函数会形成新的作用域链,从而影响变量的可访问性。

  • 闭包:嵌套函数可以访问外部函数的变量,这形成了“闭包”。闭包允许内部函数保持对其外部环境的引用,即使外部函数已经结束执行,内部函数仍然有权限访问外部函数的变量。

这种嵌套和闭包的机制使得 JavaScript 函数可以有效地封装状态,形成私有变量。

 4.1 作用域变量的查找机制:

在 JavaScript 中,作用域变量的查找机制遵循一种递归式的查找过程,称为“作用域链查找”。当在当前作用域中无法找到某个变量时,引擎会在外层嵌套的作用域中继续查找,直到找到该变量,或者是抵达最外层的作用域(全局作用域)为止。如果在全局作用域中仍然无法找到该变量,引擎会抛出一个 ReferenceError(引用错误)

4.1.1 详细过程

1、当前作用域(Local Scope)

  • 当引擎遇到一个变量时,首先会在当前的执行上下文中查找该变量。
  • 如果找到,则会停止查找并使用该变量的值。

2、外部作用域(Outer Scope)

  • 如果在当前作用域中未找到该变量,引擎会进入外层嵌套的作用域继续查找。
  • 这个过程会一直持续,直到找到该变量或者到达全局作用域。

3、全局作用域(Global Scope)

  • 如果在外层作用域中仍然未找到该变量,引擎最终会到达全局作用域进行查找。
  • 如果此时仍未找到,引擎会抛出一个 ReferenceError,表示该变量未定义。

4.1.2 作用域链

作用域链是一个链式结构,每个执行上下文都有一个指向外部作用域的引用。这个链式结构使得引擎能够按照特定的顺序查找变量。

  • 创建作用域链

    • 当一个函数被调用时,引擎会创建一个新的执行上下文,并将其推入执行栈中。
    • 这个新的执行上下文会包含一个指向外部作用域的指针,这个指针指向创建该函数的执行上下文(即父级作用域)。
  • 查找过程

    • 引擎会按照作用域链的顺序,从当前作用域开始,逐级向上查找变量。
    • 直到找到该变量或到达作用域链的顶部(全局作用域)。

4.2 示例说明

考虑以下代码:

var globalVar = '全局变量';

function outerFunction() {
    var outerVar = '外部变量';

    function innerFunction() {
        var innerVar = '内部变量';
        console.log(innerVar); // 输出:内部变量
        console.log(outerVar); // 输出:外部变量
        console.log(globalVar); // 输出:全局变量
        console.log(nonExistentVar); // 抛出错误:ReferenceError: nonExistentVar is not defined
    }

    innerFunction();
}

outerFunction();

在这个例子中:

  1. innerFunction 在当前作用域中查找 innerVar,并找到它。
  2. innerFunction 在当前作用域中找不到 outerVar,因此沿着作用域链查找,在外部作用域 outerFunction 中找到 outerVar
  3. innerFunction 在当前作用域和外部作用域中均找不到 globalVar,继续沿着作用域链查找,在全局作用域中找到 globalVar
  4. innerFunction 在所有作用域中均找不到 nonExistentVar,因此抛出 ReferenceError

4.3 总结

作用域变量的查找机制是一个递归式的过程,通过作用域链逐级向上查找变量,直到找到该变量或到达全局作用域。

5. 异常

JavaScript 的异常处理机制可以影响作用域的访问。使用 try...catch 语句块时,会创建一个新的执行上下文。在这个过程中,异常会影响变量的可访问性和作用范围:

  • 在 try 块中,如果发生异常,控制权转移到 catch 块。
  • 如果错误发生在一个嵌套作用域内,catch 块仍然可以访问外部作用域的变量。
  • 对于未捕获的异常,将会将所有当前的上下文信息清理,并转移到全局作用域。

5.1 示例代码

// 全局作用域
var globalVar = '全局变量';

function outerFunction() {
    // 外部函数作用域
    var outerVar = '外部变量';

    try {
        // try 块作用域
        var tryVar = 'try 块变量';

        function innerFunction() {
            // 内部函数作用域
            var innerVar = '内部变量';
            throw new Error('抛出一个错误'); // 抛出一个异常
        }

        innerFunction();
    } catch (error) {
        // catch 块作用域
        console.log('捕获的错误:', error.message);
        console.log('try 块变量:', tryVar); // 访问 try 块中的变量
        console.log('外部变量:', outerVar); // 访问外部作用域中的变量
        console.log('全局变量:', globalVar); // 访问全局作用域中的变量
        console.log('内部变量:', innerVar); // 尝试访问内部作用域中的变量,将抛出 ReferenceError
    }
}

outerFunction();

5.2 代码分析

全局作用域

  • 定义了一个全局变量 globalVar

外部函数 outerFunction

  • 定义了一个局部变量 outerVar
  • 使用 try...catch 语句块来处理可能发生的异常。

try 块作用域

  • 在 try 块中定义了一个局部变量 tryVar
  • 定义了一个内部函数 innerFunction

内部函数 innerFunction

  • 定义了一个局部变量 innerVar
  • 抛出一个错误 throw new Error('抛出一个错误')

catch 块作用域

  • 捕获 try 块中抛出的异常,并输出错误信息。
  • 访问 try 块中的变量 tryVar,输出 “try 块变量:”。
  • 访问外部作用域中的变量 outerVar,输出 “外部变量:”。
  • 访问全局作用域中的变量 globalVar,输出 “全局变量:”。
  • 尝试访问 innerFunction 中的变量 innerVar,由于 innerVar 在 catch 块的作用域链之外,抛出 ReferenceError

二、遮蔽效应

1. 遮蔽效应

(或者称为“变量遮蔽”)是指在 JavaScript 中,当一个变量在内层作用域中被声明时,它会遮蔽(或隐藏)同名的外层作用域中的变量。这意味着内层作用域中的同名变量会覆盖外层作用域中的变量,外层变量在该内层作用域中将不可见。

1.1 举例说明

下面是一个示例,展示了遮蔽效应的行为。

// 外层作用域
var name = 'LuQian';

function greet() {
    // 内层作用域
    var name = 'Bob'; // 这个声明将遮蔽外层的 name 变量
    console.log('Hello, ' + name); // 输出: "Hello, Bob"
}

greet(); // 调用 greet 函数
console.log('Global name: ' + name); // 输出: "Global name: LuQian"
1.2 代码分析

外层作用域

  • 在全局范围(外层作用域)中,声明了一个变量 name,值为 'Alice'

内层作用域

  • 在 greet 函数内,声明了一个同名变量 name,值为 'Bob'。这个变量遮蔽了外层作用域中的 name

输出

  • 当调用 greet 函数时,console.log('Hello, ' + name) 访问的是 greet 函数内的 name 变量,输出为 Hello, Bob
  • 当控制流返回到全局作用域,console.log('Global name: ' + name) 显示的是外层作用域的 name 变量,输出为 Global name: Alice

2. 注意事项

2.1 遮蔽只在当前作用域有效
  • 变量遮蔽只是在声明变量的那一层作用域有效。如果内层作用域不声明同名变量,则会查找外层作用域的变量。
2.2 遮蔽的层级
  • 如果在内层作用域再次声明一个同名变量,则会遮蔽它的外层同名变量。例如,如果在 greet 函数内部再有一个嵌套函数,并且在嵌套函数内再次声明 name,则嵌套函数将覆盖 greet 函数内的 name 变量。
2.3 示例代码(更复杂的遮蔽例子)
var name = 'LuQian';

function outerFunction() {
    var name = 'Bob';

    function innerFunction() {
        var name = 'Charlie'; // 遮蔽了 outerFunction 的 name
        console.log('Inner name: ' + name); // 输出: "Inner name: Charlie"
    }

    innerFunction();
    console.log('Outer name: ' + name); // 输出: "Outer name: Bob"
}

outerFunction();
console.log('Global name: ' + name); // 输出: "Global name: LuQian"

3. 总结

遮蔽效应是 JavaScript 中作用域链和变量查找的一部分。理解这一概念可以帮助开发者更清晰地控制变量的访问以及避免不必要的错误。在编写函数和作用域相关代码时,务必注意同名变量的作用域,以确保代码的可读性和逻辑的准确性。

三、变量声明提升

1. 变量声明提升

在 JavaScript 中,变量声明提升(Hoisting)是指在代码执行之前,JavaScript 引擎会将所有变量声明和函数声明提升到其所在作用域的顶部。这意味着无论变量或函数在何处声明,它们都会被移动到作用域的开始处,但变量的初始化并不会被提升。

2. 变量声明提升的详细说明

2.1 变量声明提升

  • 使用 var 声明的变量会被提升到其所在函数的顶部,但其初始化值不会被提升。
  • 使用 let 和 const 声明的变量也会被提升,但它们会被初始化为 undefined 状态,直到它们被实际声明为止。这个状态称为“暂时性死区”(Temporal Dead Zone, TDZ)。

2.2 函数声明提升

  • 函数声明也会被提升到其所在作用域的顶部。这意味着可以在函数声明之前调用该函数。

2.3 示例代码

以下是几个示例,展示了不同类型的变量声明提升行为。

// 示例 1: 变量声明提升 (var)
console.log(x); // 输出: undefined
var x = 10;
console.log(x); // 输出: 10

// 示例 2: 变量声明提升 (let)
// console.log(y); // 抛出 ReferenceError,因为 y 处于暂时性死区
let y = 20;
console.log(y); // 输出: 20

// 示例 3: 变量声明提升 (const)
// console.log(z); // 抛出 ReferenceError,因为 z 处于暂时性死区
const z = 30;
console.log(z); // 输出: 30

// 示例 4: 函数声明提升
foo(); // 输出: "Hello, I am foo!"
function foo() {
    console.log("Hello, I am foo!");
}

// 示例 5: 函数表达式不提升
// bar(); // 抛出 TypeError,因为 bar 是 undefined
var bar = function() {
    console.log("Hello, I am bar!");
};
bar(); // 输出: "Hello, I am bar!"

2.4 代码分析

示例 1: 变量声明提升 (var)

  • var x = 10; 被解释为 var x; 和 x = 10;
  • var x; 被提升到作用域顶部,因此 console.log(x) 输出 undefined
  • 赋值操作 x = 10; 在原位置执行,因此 console.log(x) 输出 10

示例 2: 变量声明提升 (let)

  • let y = 20; 被提升到作用域顶部,但在实际声明之前访问 y 会导致 ReferenceError,因为 let 和 const 存在暂时性死区。

示例 3: 变量声明提升 (const)

  • const z = 30; 同样被提升到作用域顶部,但在实际声明之前访问 z 会导致 ReferenceError,因为 const 存在暂时性死区。

示例 4: 函数声明提升

  • 函数声明 function foo() {...} 被提升到作用域顶部,因此可以在声明之前调用 foo()

示例 5: 函数表达式不提升

  • 函数表达式 var bar = function() {...}; 的 var bar 被提升,但 bar 初始化为 undefined,因此在赋值之前调用 bar() 会抛出 TypeError。赋值操作完成后,bar() 可以正常调用。

3. 总结

变量声明提升是 JavaScript 中一个重要的概念,理解它有助于更好地组织代码,避免常见的错误。变量声明提升使得变量和函数声明在作用域内提前可用,但需要注意 let 和 const 的暂时性死区,以避免在实际声明之前访问变量导致的错误。

四、作用域链和自由变量

在 JavaScript 中,作用域链和自由变量是理解变量查找和函数执行上下文的重要概念。它们共同决定了变量在不同作用域中的可见性和访问方式。

1.  作用域链(Scope Chain)

作用域链是在 JavaScript 引擎执行代码时,用于查找变量和函数的机制。作用域链由当前执行上下文的变量对象(Variable Object)和所有父级执行上下文的变量对象组成。

  • 当前执行上下文:在函数执行时,JavaScript 引擎会创建一个执行上下文(Execution Context),其中包括函数的参数、局部变量和函数的内部函数。
  • 变量对象:每个执行上下文都有一个与之关联的变量对象,其中存储了该上下文中的所有变量和函数声明。
  • 作用域链的构成:当前执行上下文的变量对象位于作用域链的顶部,其后是所有父级执行上下文的变量对象,依次排列,直到全局作用域的变量对象。

2. 自由变量(Free Variables)

自由变量是指在函数内部使用,但在函数内部未定义的变量。换句话说,自由变量是在函数外部定义但在函数内部引用的变量。

  • 查找自由变量:当函数内部引用一个自由变量时,JavaScript 引擎会沿着作用域链向上查找,直到找到该变量为止。如果在全局作用域中仍未找到该变量,则会抛出 ReferenceError

3. 示例代码

下面的示例代码展示了作用域链和自由变量的使用:

// 全局作用域
var globalVar = 'Global';

function outerFunction() {
    // 外部函数作用域
    var outerVar = 'Outer';

    function innerFunction() {
        // 内部函数作用域
        var innerVar = 'Inner';

        // 访问当前作用域的变量
        console.log('innerVar:', innerVar); // 输出: "innerVar: Inner"

        // 访问外部作用域的变量(自由变量)
        console.log('outerVar:', outerVar); // 输出: "outerVar: Outer"

        // 访问全局作用域的变量(自由变量)
        console.log('globalVar:', globalVar); // 输出: "globalVar: Global"
    }

    innerFunction();
}

outerFunction();

4. 代码分析

全局作用域

  • 定义了全局变量 globalVar,值为 'Global'

外部函数 outerFunction

  • 定义了局部变量 outerVar,值为 'Outer'
  • 定义了内部函数 innerFunction

内部函数 innerFunction

  • 定义了局部变量 innerVar,值为 'Inner'
  • 访问当前作用域的变量 innerVar,输出 "innerVar: Inner"
  • 访问外部作用域的变量 outerVar(自由变量),输出 "outerVar: Outer"
  • 访问全局作用域的变量 globalVar(自由变量),输出 "globalVar: Global"

5. 详细过程

5.1 作用域链

  • 当 innerFunction 执行时,其作用域链由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成。具体顺序为:innerFunction 的变量对象 -> outerFunction 的变量对象 -> 全局变量对象。

5.2 自由变量查找

  • 当 innerFunction 内部访问 outerVar 时,JavaScript 引擎首先在 innerFunction 的变量对象中查找,未找到。接着在 outerFunction 的变量对象中查找,找到并输出 "outerVar: Outer"
  • 当 innerFunction 内部访问 globalVar 时,JavaScript 引擎首先在 innerFunction 的变量对象中查找,未找到。接着在 outerFunction 的变量对象中查找,未找到。最后在全局变量对象中查找,找到并输出 "globalVar: Global"

6. 总结

作用域链和自由变量是 JavaScript 中重要的概念,用于管理变量的可见性和访问方式。理解作用域链和自由变量有助于编写更清晰、更高效的代码,并避免常见的变量查找错误。

五、 执行的上下文环境、执行环境和执行流、执行环境栈

在 JavaScript 中,理解和掌握执行上下文环境、执行环境和执行流、执行环境栈是深入理解语言运行机制的关键。这些概念共同构成了 JavaScript 代码执行的核心机制。

1. 执行上下文环境(Execution Context)

执行上下文环境是 JavaScript 代码执行的上下文,定义了代码执行时的作用域、变量、对象、函数以及 this 的绑定。每个执行上下文环境包含以下三个组成部分:

  • 变量对象(Variable Object, VO):存储了当前上下文中的所有变量、函数声明和函数参数。
  • 作用域链(Scope Chain):由当前执行上下文的变量对象和所有父级执行上下文的变量对象组成,用于查找变量和函数。
  • this 绑定:确定当前执行上下文中的 this 值。

执行上下文可以分为以下几种类型:

  • 全局执行上下文:JavaScript 代码首次执行时创建的全局执行上下文。它是作用域链的最外层,包含全局变量和全局对象(如 window 或 global)。
  • 函数执行上下文:每当调用一个函数时,会创建一个新的函数执行上下文。函数执行上下文包含函数的局部变量、参数和内部函数。
  • eval 执行上下文:在 eval 函数内部执行的代码所创建的执行上下文。

2. 执行环境和执行流

  • 执行环境(Execution Environment):执行环境是指代码在特定上下文中的执行状态。它包括当前的执行上下文、作用域链、this 绑定等。
  • 执行流(Execution Flow):执行流是指代码在执行过程中的控制流程。它包括顺序执行、函数调用、条件分支、循环、异常处理等。

3. 执行环境栈(Execution Context Stack)

执行环境栈(也称为调用栈,Call Stack)是 JavaScript 引擎用于管理执行上下文的数据结构。执行环境栈遵循后进先出(LIFO)的原则。每当调用一个函数时,引擎会将该函数的执行上下文推入栈顶;当函数执行完毕后,引擎会将该执行上下文从栈顶移除。

  • 栈底:全局执行上下文始终位于栈底。
  • 栈顶:当前正在执行的执行上下文位于栈顶。

4. 示例代码

下面的示例代码展示了执行上下文环境、执行环境和执行流、执行环境栈的运行机制:

// 全局执行上下文
var globalVar = 'Global';

function outerFunction(outerParam) {
    // 外部函数执行上下文
    var outerVar = 'Outer';

    function innerFunction(innerParam) {
        // 内部函数执行上下文
        var innerVar = 'Inner';

        console.log('innerVar:', innerVar); // 输出: "innerVar: Inner"
        console.log('outerVar:', outerVar); // 输出: "outerVar: Outer"
        console.log('globalVar:', globalVar); // 输出: "globalVar: Global"
        console.log('outerParam:', outerParam); // 输出: "outerParam: OuterParam"
        console.log('innerParam:', innerParam); // 输出: "innerParam: InnerParam"
    }

    innerFunction('InnerParam');
}

outerFunction('OuterParam');

5. 代码分析

全局执行上下文

  • 定义了全局变量 globalVar,值为 'Global'
  • 定义了函数 outerFunction

调用 outerFunction

  • 创建 outerFunction 的执行上下文,推入执行环境栈。
  • 定义局部变量 outerVar,值为 'Outer'
  • 定义函数 innerFunction

调用 innerFunction

  • 创建 innerFunction 的执行上下文,推入执行环境栈。
  • 定义局部变量 innerVar,值为 'Inner'
  • 访问当前作用域的变量 innerVar,输出 "innerVar: Inner"
  • 访问外部作用域的变量 outerVar(自由变量),输出 "outerVar: Outer"
  • 访问全局作用域的变量 globalVar(自由变量),输出 "globalVar: Global"
  • 访问外部函数参数 outerParam(自由变量),输出 "outerParam: OuterParam"
  • 访问当前函数参数 innerParam,输出 "innerParam: InnerParam"

innerFunction 执行完毕

  • innerFunction 的执行上下文从执行环境栈中移除。
  • 返回到 outerFunction 的执行上下文。

outerFunction 执行完毕

  • outerFunction 的执行上下文从执行环境栈中移除。
  • 返回到全局执行上下文。

6. 执行环境栈的变化

初始状态

  • 栈底:全局执行上下文。

调用 outerFunction

  • 栈顶:outerFunction 的执行上下文。
  • 栈底:全局执行上下文。

调用 innerFunction

  • 栈顶:innerFunction 的执行上下文。
  • 中间:outerFunction 的执行上下文。
  • 栈底:全局执行上下文。

innerFunction 执行完毕

  • 栈顶:outerFunction 的执行上下文。
  • 栈底:全局执行上下文。

outerFunction 执行完毕

  • 栈顶:全局执行上下文。

7. 总结

执行上下文环境、执行环境和执行流、执行环境栈是 JavaScript 引擎在执行代码时的核心机制。理解这些概念有助于更好地理解代码的执行顺序、作用域链、变量查找和函数调用过程。掌握这些知识可以帮助开发者编写更高效、更清晰的代码,并避免常见的执行上下文相关错误。

六、 作用域的类型

在 JavaScript 中,作用域主要分为以下几种类型:

  • 全局作用域
  • 函数作用域
  • 块作用域
  • 隐式作用域(或称为闭包)

1. 全局作用域

全局作用域是最高的作用域,它里的变量和函数可以在代码的任何位置访问。全局变量是由任何函数、对象、或者块中都可以访问的变量。

var globalVar = 'I am global';

function testGlobal() {
    console.log(globalVar); // 输出: "I am global"
}

testGlobal();
console.log(globalVar); // 输出: "I am global"

在上面的示例中,globalVar 是一个全局变量,可以在函数和全局代码中访问。

2. 函数作用域

函数作用域是指在函数内部定义的变量,只能在该函数内部访问。每当创建一个函数时,JavaScript 会为该函数创建一个新的作用域。

function testFunctionScope() {
    var functionVar = 'I am local to this function';
    console.log(functionVar); // 输出: "I am local to this function"
}

testFunctionScope();
console.log(functionVar); // 抛出 ReferenceError: functionVar is not defined

在这个例子中,functionVar 是被定义在 testFunctionScope 函数内部的,外部无法访问。

3. 块作用域

块作用域是在 ES6 引入的一个新特性,使用 let 和 const 关键字声明的变量具有块级作用域。这意味着在诸如 ifforwhile 等控制结构或代码块中声明的变量只在该块内部有效。

if (true) {
    let blockVar = 'I am block scoped';
    console.log(blockVar); // 输出: "I am block scoped"
}

console.log(blockVar); // 抛出 ReferenceError: blockVar is not defined

在这个例子中,blockVar 只在 if 语句的块内部有效,外部无法访问。

4. 作用域链

作用域链是 JavaScript 用于查找变量的一种机制。它是一系列的作用域,从最内层到最外层的作用域形成一个链条。当 JavaScript 引擎要查找一个变量时,它首先在当前作用域中查找,如果未找到,则查找外层作用域,直到全局作用域。

var globalVar = 'Global';

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

    function inner() {
        var innerVar = 'Inner';
        console.log(outerVar); // 输出: "Outer"
        console.log(globalVar); // 输出: "Global"
    }

    inner();
}

outer();

在上述代码中,inner 函数可以访问其外部函数 outer 的变量 outerVar,以及全局变量 globalVar。这就是作用域链的工作原理。

5. 闭包

闭包是 JavaScript 中的一个重要概念,它是指一个函数可以“记住”并访问其外部作用域的变量,即使该函数是在其外部执行的。闭包允许你将数据封装在一个特定范围中,防止外部直接访问。

function makeCounter() {
    let count = 0; // count 变量在这里被创建
    return function() {
        count++; // 可以访问外部变量 count
        return count;
    };
}

const counter = makeCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3

在这个示例中,返回的函数在其调用时仍然能够访问 makeCounter 函数中的 count 变量,这就是闭包的作用。

6. 作用域的重要性与应用

掌握作用域非常重要,因为它直接影响到变量的生命周期、内存管理和代码的可读性与可维护性。

  1. 避免变量冲突:通过使用局部作用域和块作用域,可以减少全局变量的使用,从而降低变量命名冲突的风险。

  2. 封装和信息隐藏:通过闭包技术,可以封装变量,使外界无法直接访问,从而增强数据的隐私性。

  3. 增加可维护性:好的作用域管理可以使代码逻辑更清晰,从而增加可维护性。

7. 总结

JavaScript 的作用域是一个非常重要且复杂的主题,它由全局作用域、函数作用域和块作用域组成。通过作用域链和闭包的概念,能够有效管理变量的可访问性和生命周期。掌握作用域的机制对于编写高效、可维护的 JavaScript 代码至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值