作用域链的定义

作用域链是由当前执行环境和所有父级执行环境的变量对象组成的链式结构。它保证了变量和函数的有序访问。

作用域链的创建过程

当函数被调用时,会创建一个执行环境(execution context),每个执行环境都有一个关联的变量对象(variable object),

作用域链就是由这些变量对象组成的。

具体过程:
  • 在函数定义时,会创建一个包含全局变量对象的作用域链,并存储在函数的内部属性[[Scope]]中。

  • 当函数被调用时,会创建一个新的执行环境,然后复制函数的[[Scope]]属性来构建作用域链。

  • 然后,会创建一个活动对象(activation object,即函数本身的变量对象)并将其推入作用域链的前端。

变量查找

当在函数中访问一个变量时,会从作用域链的前端(当前函数的活动对象)开始查找,如果没有找到,就会沿着作用域链向后查找,

直到全局变量对象。如果全局变量对象中也没有,则会抛出ReferenceError。

作用域链是JavaScript中非常重要的概念,它决定了变量和函数的查找机制。让我详细解析作用域链的工作原理。

1. 什么是作用域链?

作用域链是由多个执行上下文的变量对象组成的链式结构,用于标识符解析。

var globalVar = "全局变量";

function outer() {
    var outerVar = "外部变量";
    
    function inner() {
        var innerVar = "内部变量";
        // 这里可以访问 innerVar, outerVar, globalVar
        console.log(innerVar, outerVar, globalVar);
    }
    
    inner();
}

outer();
2. 作用域链的构建过程
2.1 全局执行上下文
// 全局作用域链
globalExecutionContext = {
    VO: globalObject, // 全局变量对象
    Scope: [globalObject] // 作用域链
}
 2.2 函数执行上下文
function test(a, b) {
    var c = 30;
    console.log(a, b, c);
}

test(10, 20);

// test函数的作用域链构建过程:
testExecutionContext = {
    AO: { // 活动对象
        arguments: {0: 10, 1: 20, length: 2},
        a: 10,
        b: 20,
        c: 30
    },
    Scope: [test.AO, global.VO] // 作用域链 = 当前AO + [[Scope]]
}

// VO是 Variable Object(变量对象),指的是全局变量对象,即存储全局变量和函数的对象
// 活动对象(AO - Active Object),包含了参数和局部变量
3. [[Scope]]属性

每个函数在创建时都会有一个内部的[[Scope]]属性,保存着父级的作用域链。

var x = 10;

function createFunction() {
    var y = 20;
    
    function inner() {
        var z = 30;
        console.log(x, y, z); // 10, 20, 30
    }
    
    // inner函数的[[Scope]] = [createFunction's AO, global.VO]
    return inner;
}

const myFunc = createFunction();
myFunc();
4. 详细的作用域链分析
// 全局执行上下文
globalContext = {
    VO: {
        a: 1,
        outer: function
    },
    Scope: [globalContext.VO]
}

function outer() {
    var b = 2;
    
    function inner() {
        var c = 3;
        console.log(a + b + c); // 6
    }
    
    return inner;
}

// outer函数创建时的[[Scope]]
outer.[[Scope]] = [globalContext.VO]

// outer函数执行时的上下文
outerContext = {
    AO: {
        arguments: {},
        b: 2,
        inner: function
    },
    Scope: [outerContext.AO, globalContext.VO]
}

// inner函数创建时的[[Scope]]
inner.[[Scope]] = [outerContext.AO, globalContext.VO]

// inner函数执行时的上下文
innerContext = {
    AO: {
        arguments: {},
        c: 3
    },
    Scope: [innerContext.AO, outerContext.AO, globalContext.VO]
}
5. 作用域链与闭包

闭包的本质就是函数保持对其创建时作用域链的引用。

function createCounter() {
    let count = 0;
    
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();

// 三个闭包函数的作用域链:
// [increment.AO, createCounter.AO, global.VO]
// [decrement.AO, createCounter.AO, global.VO]  
// [getCount.AO, createCounter.AO, global.VO]

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
6. 作用域链的延长
6.1 with语句(不推荐使用)
var obj = {a: 1, b: 2};

with (obj) {
    console.log(a); // 1 - 先在obj中查找
    console.log(b); // 2 - 先在obj中查找
    var c = 3;     // 不会添加到obj中
}

console.log(obj.c); // undefined
console.log(c);     // 3 - 在全局作用域
6.2 try-catch语句
try {
    throw new Error("测试错误");
} catch (e) {
    // catch块会创建一个新的变量对象,包含错误对象e
    console.log(e.message); // "测试错误"
    var catchVar = "在catch块中声明";
}

console.log(catchVar); // "在catch块中声明" - 变量提升到函数/全局作用域
7. 作用域链与this的区别
var obj = {
    value: "对象属性",
    getValue: function() {
        console.log(this.value);     // "对象属性" - this指向obj
        console.log(value);          // ReferenceError - 作用域链中找不到value
    }
};

var value = "全局变量";

obj.getValue();
8. 实际案例分析
8.1 循环中的闭包问题
// 问题代码
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 全部输出 3
    }, 100);
}

// 解决方案1:使用IIFE创建新的作用域
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j); // 0, 1, 2
        }, 100);
    })(i);
}

// 解决方案2:使用let块级作用域
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 0, 1, 2
    }, 100);
}
 8.2 模块模式
const MyModule = (function() {
    let privateVar = 0;
    
    function privateFunction() {
        return privateVar;
    }
    
    return {
        publicMethod: function() {
            privateVar++;
            return privateFunction();
        },
        getValue: function() {
            return privateVar;
        }
    };
})();

// 作用域链分析:
// publicMethod.[[Scope]] = [MyModule匿名函数的AO, global.VO]
// 其中MyModule匿名函数的AO包含 privateVar 和 privateFunction

console.log(MyModule.publicMethod()); // 1
console.log(MyModule.getValue());     // 1
console.log(MyModule.privateVar);     // undefined - 无法直接访问
9. 性能考虑
9.1 标识符查找性能
function inefficient() {
    // 多次访问全局变量 - 查找需要遍历整个作用域链
    for (var i = 0; i < 1000; i++) {
        console.log(document); // 全局变量,查找较慢
    }
}

function efficient() {
    // 缓存局部引用 - 只需一次查找
    var doc = document;
    for (var i = 0; i < 1000; i++) {
        console.log(doc); // 局部变量,查找快速
    }
}
9.2 避免过深的作用域嵌套
// 不推荐 - 作用域嵌套过深
function deeplyNested() {
    var a = 1;
    
    function level1() {
        var b = 2;
        
        function level2() {
            var c = 3;
            
            function level3() {
                // 需要遍历4层作用域才能找到a
                return a + b + c;
            }
            
            return level3();
        }
        
        return level2();
    }
    
    return level1();
}
10. 现代JavaScript中的作用域链
10.1 块级作用域的影响
{
    let blockScoped = "块级作用域";
    const constant = "常量";
    
    {
        // 新的块级作用域,但仍能访问外层块级变量
        console.log(blockScoped); // "块级作用域"
        console.log(constant);    // "常量"
    }
}

console.log(blockScoped); // ReferenceError
 10.2 模块作用域
// module.js
let moduleVar = "模块变量";

export function getModuleVar() {
    return moduleVar;
}

// main.js
import { getModuleVar } from './module.js';

console.log(getModuleVar()); // "模块变量"
console.log(moduleVar);      // ReferenceError - 模块作用域隔离
名词解释
一、Variable Object(VO 变量对象)

变量对象(VO)是执行上下文的一个组成部分,它存储了在当前执行环境中声明的变量、函数声明和函数参数。在JavaScript中,变量对象的行为会根据执行上下文的类型而有所不同:

  1. 全局执行上下文:在全局环境中,变量对象就是全局对象(在浏览器中window对象,在Node.js中global对象)。

  2. 函数执行上下文:在函数环境中,变量对象被称为活动对象(AO - Active Object),它会在进入函数执行上下文时被激活,并且会包含函数的参数对象(arguments object)。

  1. VO与AO的关系

  • VO是一个抽象概念,在进入执行上下文但代码尚未执行时就已经创建。

  • AO是VO在函数执行上下文中的具体实现,它包含了函数参数。

作用域链的构建过程正是当前执行上下文的活动对象(AO)加上所有父级执行上下文的变量对象(VO)按顺序组成的链式结构。

二、Active Object (AO 活动对象)

1. 概念定义:AO 是 Variable Object (VO) 在函数执行上下文中的一个特殊实例,它是在函数被调用时创建的。

2. 特点与功能

  • 只存在于函数执行上下文中。

  • 包含函数的参数对象(arguments object)。

  • 存储函数内部声明的变量和函数。

  • 在函数执行上下文的创建阶段被初始化。

3. 创建过程

  • 第一步:建立 arguments 对象。

  • 第二步:创建函数声明。

  • 第三步:创建变量声明。

4. 与VO的关系

  • VO是一个抽象概念,在不同执行上下文中有不同的具体实现。

  • 在全局执行上下文中,VO就是全局对象。

  • 在函数执行上下文中,VO被激活为AO,增加了arguments对象。

总结

作用域链的核心要点:

  1. 链式结构:由当前执行环境的变量对象和所有父级变量对象组成。

  2. 静态确定:在函数创建时确定,与调用位置无关(词法作用域)。

  3. 查找机制:从当前作用域开始,逐级向上查找标识符。

  4. 闭包基础:函数保持对其创建时作用域链的引用。

  5. 性能影响:作用域链越长,变量查找越慢。

理解作用域链对于掌握JavaScript的闭包、模块化、内存管理等高级概念至关重要。

VO与AO完整代码示例

下面是一个详细展示JavaScript中变量对象(VO)和活动对象(AO)工作原理的完整示例:

示例代码
// 全局变量
var globalVar = 100;

// 全局函数声明
function outerFunction(a, b) {
    // 局部变量
    var localVar = 200;
    
    // 内部函数
    function innerFunction(c) {
        var innerVar = 300;
        return a + b + c + localVar + innerVar + globalVar;
    }
    
    // 调用内部函数
    return innerFunction(50);
}

// 全局执行
var result = outerFunction(10, 20);
console.log(result); // 输出: 680
执行上下文与变量对象的详细解析
1. 全局执行上下文 (Global Execution Context)

当JavaScript引擎开始执行代码时,首先创建全局执行上下文:

// 全局变量对象 (VO)
globalVO = {
    globalVar: undefined, // 变量声明提升,初始值为undefined
    outerFunction: function(a, b) { /* 函数体 */ }, // 函数声明提升
    result: undefined // 变量声明提升
};

// 全局执行上下文
globalExecutionContext = {
    VO: globalVO, // 全局上下文使用VO
    this: window, // 在浏览器中,全局this指向window
    Scope: [globalVO] // 作用域链只有全局VO
};
2. 执行代码阶段 - 全局上下文

代码执行过程中,全局变量对象被填充实际值:

globalVO = {
    globalVar: 100, // 赋值完成
    outerFunction: function(a, b) { /* 函数体 */ },
    result: undefined // 尚未赋值
};
3. 进入outerFunction执行上下文

当调用 outerFunction(10, 20) 时,创建函数执行上下文:

// outerFunction的活动对象 (AO) - 创建阶段
outerFunctionAO = {
    arguments: {0: 10, 1: 20, length: 2}, // 参数对象
    a: 10, // 形式参数
    b: 20, // 形式参数
    localVar: undefined, // 变量声明提升
    innerFunction: function(c) { /* 函数体 */ } // 函数声明提升
};

// outerFunction执行上下文
outerFunctionExecutionContext = {
    AO: outerFunctionAO, // 函数上下文使用AO
    this: window, // 非严格模式下,this指向全局对象
    Scope: [outerFunctionAO, globalVO] // 作用域链 = 当前AO + 父级VO
};
4. 执行outerFunction代码阶段

执行outerFunction内部代码时,AO被填充实际值:

// outerFunction的活动对象 (AO) - 执行阶段
outerFunctionAO = {
    arguments: {0: 10, 1: 20, length: 2},
    a: 10,
    b: 20,
    localVar: 200, // 赋值完成
    innerFunction: function(c) { /* 函数体 */ }
};
 5. 进入innerFunction执行上下文

当调用 innerFunction(50) 时,创建内部函数执行上下文:

// innerFunction的活动对象 (AO) - 创建阶段
innerFunctionAO = {
    arguments: {0: 50, length: 1}, // 参数对象
    c: 50, // 形式参数
    innerVar: undefined // 变量声明提升
};

// innerFunction执行上下文
innerFunctionExecutionContext = {
    AO: innerFunctionAO,
    this: window, // 非严格模式下
    Scope: [innerFunctionAO, outerFunctionAO, globalVO] // 完整作用域链
};
6. 执行innerFunction代码阶段

执行innerFunction内部代码时,AO被填充实际值:

// innerFunction的活动对象 (AO) - 执行阶段
innerFunctionAO = {
    arguments: {0: 50, length: 1},
    c: 50,
    innerVar: 300 // 赋值完成
};
7. 作用域链查找过程

在 innerFunction 中,当访问变量时,按照作用域链顺序查找:

  1. 先在 innerFunctionAO 中查找:找到 c 和 innerVar 。

  2. 接着在 outerFunctionAO 中查找:找到 a 、 b 和 localVar 。

  3. 最后在 globalVO 中查找:找到 globalVar 。

所以 a + b + c + localVar + innerVar + globalVar 等于 10 + 20 + 50 + 200 + 300 + 100 = 680 。

8. 返回并更新全局变量

执行完成后result 变量在全局VO中被赋值:

globalVO = {
    globalVar: 100,
    outerFunction: function(a, b) { /* 函数体 */ },
    result: 680 // 赋值完成
};
9.VO与AO的核心区别

1. VO (Variable Object):

  • 通用概念,存在于所有执行上下文中。

  • 在全局执行上下文中直接使用。

  • 无法直接访问。

2. AO (Active Object):

  • VO在函数执行上下文中的特殊形式。

  • 包含函数参数对象(arguments)。

  • 当进入函数执行上下文时,VO被激活为AO。

彩蛋

完整的作用域链深度解析示例链接,请移步Cnb: Cnb中的完整示例