作用域

5 阅读9分钟

JavaScript 作用域核心知识点详解

1. 作用域的基本概念

什么是作用域?

作用域是变量和函数的可访问范围,决定了代码中变量和函数的可见性。

作用域的类型

  • 词法作用域(静态作用域) :JavaScript 采用的作用域类型,在代码编写时确定
  • 动态作用域:在代码运行时确定(JavaScript 不使用)

2. 作用域的种类

2.1 全局作用域(Global Scope)

javascript

var globalVar = 'I am global';

function test() {
    console.log(globalVar); // 可以访问
}

// 以下也是全局作用域
window.globalVar2 = 'I am also global';

特点:

  • 在任何地方都可以访问
  • 在浏览器中是 window 对象
  • 生命周期与应用程序相同

2.2 函数作用域(Function Scope)

javascript

function outer() {
    var functionScoped = 'I am function scoped';
    
    function inner() {
        console.log(functionScoped); // 可以访问
    }
    inner();
}

console.log(functionScoped); // ReferenceError

特点:

  • 使用 varfunction 声明
  • 只在函数内部可访问
  • 每次函数调用都会创建新的作用域

2.3 块级作用域(Block Scope) - ES6+

javascript

{
    let blockScoped = 'I am block scoped';
    const alsoBlockScoped = 'Me too';
    console.log(blockScoped); // 可以访问
}

console.log(blockScoped); // ReferenceError

// 常见块级作用域
if (true) {
    let conditionScoped = 'only in if block';
}

for (let i = 0; i < 3; i++) {
    // 每次循环都有独立的 i
}

特点:

  • 使用 letconst 声明
  • 在 {} 内部可访问
  • 避免变量污染和意外覆盖

3. 作用域链(Scope Chain)

作用域链的形成

javascript

var global = 'global';

function outer() {
    var outerVar = 'outer';
    
    function inner() {
        var innerVar = 'inner';
        console.log(innerVar);    // 当前作用域
        console.log(outerVar);    // 外层作用域
        console.log(global);      // 全局作用域
    }
    
    inner();
}

outer();

作用域链查找规则:

  1. 在当前作用域查找变量
  2. 如果找不到,向上一层作用域查找
  3. 重复步骤2直到全局作用域
  4. 如果全局作用域也找不到,抛出 ReferenceError

作用域链的创建时机

javascript

function createScopeChain() {
    var parentVar = 'parent';
    
    // 函数定义时作用域链就已确定
    return function child() {
        console.log(parentVar); // 可以访问父级变量
    };
}

const childFunc = createScopeChain();
childFunc(); // "parent"

4. 变量提升(Hoisting)

var 的变量提升

javascript

console.log(a); // undefined
var a = 10;
console.log(a); // 10

// 实际执行顺序:
// var a;          // 声明提升
// console.log(a); // undefined
// a = 10;         // 赋值
// console.log(a); // 10

函数声明提升

javascript

foo(); // "Hello" - 可以调用

function foo() {
    console.log("Hello");
}

// 函数表达式不会整体提升
bar(); // TypeError: bar is not a function
var bar = function() {
    console.log("World");
};

let/const 的暂时性死区(TDZ)

javascript

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

// 暂时性死区:从块开始到变量声明之间的区域
{
    console.log(b); // ReferenceError
    let b = 20;
}

5. 闭包(Closure)

闭包的定义

函数能够记住并访问其词法作用域,即使函数在其它地方执行。

闭包的形成

javascript

function createCounter() {
    let count = 0; // 私有变量
    
    // 返回的函数形成了闭包
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

闭包的实际应用

javascript

// 1. 模块模式
const calculator = (function() {
    let memory = 0;
    
    return {
        add: function(x) { memory += x; },
        subtract: function(x) { memory -= x; },
        getMemory: function() { return memory; }
    };
})();

// 2. 事件处理
function setupButtons() {
    for (let i = 0; i < 3; i++) {
        document.getElementById(`btn-${i}`).addEventListener('click', function() {
            console.log(`Button ${i} clicked`);
        });
    }
}

// 3. 函数柯里化
function multiply(a) {
    return function(b) {
        return a * b;
    };
}

const double = multiply(2);
console.log(double(5)); // 10

6. 执行上下文与作用域的关系

执行上下文的创建阶段

javascript

function example(a) {
    var b = 2;
    let c = 3;
    
    function inner() {
        console.log(a, b, c);
    }
    
    return inner;
}

// 创建阶段:
// 1. 创建变量对象(VO)
// 2. 建立作用域链
// 3. 确定this指向

7. ES6+ 作用域新特性

块级作用域的实际应用

javascript

// 避免循环中的闭包问题
for (let i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(i); // 0, 1, 2
    }, 100);
}

// 替代IIFE
{
    let temp = 'temporary value';
    // 使用temp...
}
// temp 在这里不可访问

// 在switch语句中
switch (condition) {
    case 1: {
        let message = 'case 1';
        break;
    }
    case 2: {
        let message = 'case 2'; // 不会冲突
        break;
    }
}

const 与不可变性

javascript

const PI = 3.14159;
// PI = 3.14; // TypeError: Assignment to constant variable

const obj = { name: 'John' };
obj.name = 'Jane'; // 允许 - 修改属性
// obj = {};     // 不允许 - 重新赋值

const arr = [1, 2, 3];
arr.push(4);    // 允许
// arr = [];    // 不允许

8. 作用域的最佳实践

1. 避免全局变量污染

javascript

// 不好
var globalData = 'data';

// 好
(function() {
    var localData = 'data';
    // 使用模块模式或ES6模块
})();

// 更好 - 使用模块
export const data = 'data';

2. 合理使用块级作用域

javascript

// 使用 let/const 替代 var
for (let i = 0; i < 10; i++) {
    // i 只在循环内有效
}

// 使用块限制变量作用域
{
    const temp = calculateSomething();
    useTemp(temp);
}
// temp 自动回收

3. 管理闭包内存

javascript

function createHeavyClosure() {
    const heavyData = new Array(1000000);
    
    return function() {
        // 使用 heavyData...
    };
}

const closure = createHeavyClosure();
// 使用完后手动释放
closure = null;

4. 使用严格模式

javascript

'use strict';

function strictFunction() {
    undeclaredVar = 10; // ReferenceError
    delete variable;    // SyntaxError
}

9. 常见作用域陷阱

陷阱1:循环中的闭包

javascript

// 问题
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// 解决方案
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 0, 1, 2
}

陷阱2:变量提升混淆

javascript

var name = 'global';

function test() {
    console.log(name); // undefined,不是 'global'
    var name = 'local';
}

test();

陷阱3:意外的全局变量

javascript

function createGlobal() {
    undeclared = 'I become global!'; // 没有 var/let/const
}

createGlobal();
console.log(undeclared); // 'I become global!'

10. 总结

概念关键点示例
全局作用域整个程序可见var global = 'value'
函数作用域varfunctionfunction foo() { var local = 1 }
块级作用域letconst{ let block = 1 }
作用域链从内向外查找inner → outer → global
变量提升var 和函数声明提升console.log(a); var a = 1
暂时性死区let/const 声明前不可访问console.log(b); let b = 2
闭包函数记住词法作用域function outer() { return function inner() {} }

理解作用域是掌握 JavaScript 的核心,它影响着变量生命周期、内存管理和代码结构设计。

基础作用域题目

题目 1:变量提升

javascript

console.log(a);
var a = 10;
console.log(a);

答案:

undefined
10

知识点解析:

  • 变量提升:var 声明的变量会被提升到当前作用域的顶部
  • 创建阶段:a 被声明并初始化为 undefined
  • 执行阶段:第一个 console.log 时 a 还未赋值,第二个 console.log 时已赋值为 10

题目 2:函数提升

foo();

function foo() {
    console.log('foo called');
}

bar();

var bar = function() {
    console.log('bar called');
};

答案:

foo called
TypeError: bar is not a function

知识点解析:

  • 函数声明整体提升:function foo(){} 整个函数被提升
  • 函数表达式只提升变量声明:var bar 被提升,但赋值不提升
  • bar() 调用时 bar 是 undefined,不是函数

题目 3:作用域链查找

var x = 1;

function outer() {
    var x = 2;
    
    function inner() {
        console.log(x);
    }
    
    inner();
}

outer();

答案:  2
知识点解析:

  • 作用域链:inner → outer → 全局
  • 变量查找从内向外,找到最近的 x 就停止
  • inner 找到 outer 中的 x = 2

题目 4:块级作用域

function test() {
    if (true) {
        var a = 1;
        let b = 2;
        const c = 3;
    }
    
    console.log(a);
    console.log(b);
    console.log(c);
}

test();

答案:

1
ReferenceError: b is not defined

知识点解析:

  • var 没有块级作用域,只有函数作用域
  • let 和 const 有块级作用域,只在 {} 内有效
  • b 和 c 在 if 块外不可访问

闭包相关题目

题目 5:经典闭包

function createCounter() {
    let count = 0;
    
    return function() {
        count++;
        console.log(count);
    };
}

const counter1 = createCounter();
const counter2 = createCounter();

counter1();
counter1();
counter2();
counter1();

答案:

1
2
1
3

知识点解析:

  • 每次调用 createCounter() 都会创建新的词法环境
  • counter1 和 counter2 有各自独立的 count
  • 闭包保持对外部变量的引用

题目 6:循环中的闭包问题

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

答案:  3 3 3
知识点解析:

  • var i 在全局/函数作用域,循环结束后 i = 3
  • 所有回调函数共享同一个 i
  • 事件循环执行时 i 已经是最终值

题目 7:循环闭包解决方案

// 方案1:使用IIFE
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 100);
    })(i);
}

// 方案2:使用let
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

答案:  0 1 2(两种方案都输出)
知识点解析:

  • IIFE:每次循环创建新作用域,捕获当前 i 的值
  • let:每次循环创建新的块级作用域,每个 i 都是独立的

题目 8:模块模式

function createModule() {
    let privateVar = 0;
    
    return {
        getValue: function() {
            return privateVar;
        },
        setValue: function(val) {
            privateVar = val;
        },
        increment: function() {
            privateVar++;
        }
    };
}

const module = createModule();
console.log(module.getValue());
module.increment();
console.log(module.getValue());
module.setValue(10);
console.log(module.getValue());

答案:  0 1 10
知识点解析:

  • 模块模式利用闭包创建私有变量
  • privateVar 只能在模块内部访问
  • 对外暴露的接口可以操作私有变量

作用域链查找题目

题目 9:多层嵌套作用域

var a = 1;

function level1() {
    var a = 2;
    
    function level2() {
        var a = 3;
        
        function level3() {
            console.log(a);
        }
        
        level3();
    }
    
    level2();
}

level1();

答案:  3
知识点解析:

  • 作用域链:level3 → level2 → level1 → 全局
  • 变量查找遵循"最近原则"
  • level3 找到 level2 中的 a = 3

题目 10:作用域链中断

var a = 1;

function test() {
    console.log(a);
    var a = 2;
}

test();

答案:  undefined
知识点解析:

  • 变量提升:函数内的 var a 被提升
  • 创建阶段:a 被声明并初始化为 undefined
  • 第一个 console.log 时,局部变量 a 已存在但未赋值

题目 11:全局变量污染

function func1() {
    x = 10; // 没有使用 var/let/const
}

function func2() {
    console.log(x);
}

func1();
func2();

答案:  10
知识点解析:

  • 未声明的变量赋值会创建全局变量
  • 在严格模式下会报错
  • 应该避免这种隐式全局变量

题目 12:严格模式的影响

'use strict';

function test() {
    x = 10; // 这里会发生什么?
    console.log(x);
}

test();

答案:  ReferenceError: x is not defined
知识点解析:

  • 严格模式禁止隐式创建全局变量
  • 未声明的变量赋值会抛出错误
  • 提高代码质量和安全性

综合题目

题目 13:作用域链 + this

var name = 'Global';

var obj = {
    name: 'Object',
    getName: function() {
        return function() {
            return this.name;
        };
    },
    getNameArrow: function() {
        return () => {
            return this.name;
        };
    }
};

console.log(obj.getName()());
console.log(obj.getNameArrow()());

答案:

Global
Object

知识点解析:

  • 普通函数:this 取决于调用方式,独立调用指向全局
  • 箭头函数:this 继承自外层作用域,指向 obj

题目 14:闭包内存泄漏

function createHeavyObject() {
    const heavyData = new Array(1000000).fill('heavy data');
    
    return function() {
        console.log('Closure still holds reference to heavyData');
    };
}

let closure = createHeavyObject();
// 如何释放 heavyData 的内存?

答案:  设置 closure = null
知识点解析:

  • 闭包会保持对外部变量的引用
  • 即使函数不再使用,引用的变量也不会被垃圾回收
  • 手动解除引用可以释放内存

题目 15:作用域链性能

function createNestedFunctions(depth) {
    let value = 0;
    
    function createLevel(currentDepth) {
        if (currentDepth >= depth) {
            return function() {
                return value;
            };
        }
        
        return createLevel(currentDepth + 1);
    }
    
    return createLevel(0);
}

const deepClosure = createNestedFunctions(1000);
console.log(deepClosure());

答案:  0
知识点解析:

  • 深层嵌套的作用域链会影响性能
  • 变量查找需要沿着作用域链向上搜索
  • 现代 JS 引擎会优化这种访问

题目 16:块级作用域与循环

// 使用 var
console.log('Using var:');
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
}

// 使用 let
console.log('Using let:');
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0);
}

答案:

Using var:
3
3
3
Using let:
0
1
2

知识点解析:

  • var:函数作用域,循环结束后 i=3
  • let:块级作用域,每次循环创建新的 i

题目 17:函数参数作用域

var x = 1;

function test(x) {
    console.log(x);
    x = 2;
    console.log(x);
}

test(10);
console.log(x);

答案:

10
2
1

知识点解析:

  • 函数参数相当于在函数作用域内声明变量
  • 修改参数不会影响外部变量
  • 参数作用域优先级高于外部作用