Day-8-作用域与闭包

Day 8: 作用域与闭包

🎯 学习目标

  • 理解 作用域与闭包 的核心概念和工作原理
  • 掌握相关语法和最佳实践
  • 学会在实际项目中应用 作用域与闭包

💡 核心概念

作用域(Scope)决定了变量的可访问范围,而闭包(Closure)是JavaScript中最强大的特性之一。理解这两个概念对于编写高质量的JavaScript代码至关重要。

关键概念

1. 作用域(Scope)

作用域是变量和函数的可访问范围。JavaScript有三种作用域:

  1. 全局作用域:在任何函数外声明的变量
let globalVar = "全局变量";
function test() {
    console.log(globalVar);  // 可以访问
}
  1. 函数作用域:在函数内声明的变量,只在函数内可访问
function test() {
    let localVar = "局部变量";
    console.log(localVar);  // 可以访问
}
console.log(localVar);  // 错误:无法访问
  1. 块级作用域:let/const在{}块内声明的变量
if (true) {
    let blockVar = "块级变量";
    console.log(blockVar);  // 可以访问
}
console.log(blockVar);  // 错误:无法访问

2. 变量提升(Hoisting)

var声明的变量会被提升到函数顶部,但赋值不会提升:

console.log(myVar);  // undefined(不是错误)
var myVar = 10;

// 实际执行顺序
var myVar;           // 声明提升
console.log(myVar);  // undefined
myVar = 10;          // 赋值留在原地

let/const不会被提升:

console.log(myLet);  // ReferenceError
let myLet = 10;

最佳实践:始终在作用域顶部声明变量,避免混淆。

3. 闭包(Closure)

闭包是函数和其词法环境的组合。简单来说,内部函数可以访问外部函数的变量,即使外部函数已经返回。

闭包示例

function createCounter() {
    let count = 0;  // 私有变量

    return function() {
        count++;    // 访问外部变量
        return count;
    };
}

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

闭包的用途

  1. 数据私有化(模拟私有方法)
  2. 函数工厂(创建特定功能的函数)
  3. 维持状态(变量在函数调用间保持)

🎮 示例代码

示例1:作用域链

// 全局变量
let globalVar = "全局";

function outerFunction() {
    // 外层函数变量
    let outerVar = "外层";

    function innerFunction() {
        // 内层函数变量
        let innerVar = "内层";

        console.log(innerVar);  // ✅ 可以访问
        console.log(outerVar);  // ✅ 可以访问
        console.log(globalVar); // ✅ 可以访问
    }

    innerFunction();

    console.log(outerVar);  // ✅ 可以访问
    // console.log(innerVar); // ❌ 错误:无法访问
}

outerFunction();
// console.log(outerVar); // ❌ 错误:无法访问
console.log(globalVar);   // ✅ 可以访问

/*
输出:
内层
外层
全局
外层
全局
*/

示例2:闭包实现数据私有化

function createPerson(name) {
    // 私有变量
    let age = 0;

    return {
        getName: function() {
            return name;
        },
        getAge: function() {
            return age;
        },
        setAge: function(newAge) {
            if (newAge > 0 && newAge < 150) {
                age = newAge;
            } else {
                console.log("无效的年龄");
            }
        },
        incrementAge: function() {
            age++;
        }
    };
}

const person = createPerson("张三");

console.log(person.getName());  // 张三
console.log(person.getAge());   // 0

person.setAge(25);
console.log(person.getAge());   // 25

person.incrementAge();
console.log(person.getAge());   // 26

// 无法直接访问age
console.log(person.age);        // undefined
// person.age = -10;            // 不会影响内部age

/*
这样实现了数据私有化,外部代码只能通过提供的方法访问和修改数据。
*/

示例3:闭包实现函数工厂

// 函数工厂:创建特定功能的函数
function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

// 创建不同的乘法函数
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);

console.log(double(5));    // 10
console.log(triple(5));    // 15
console.log(quadruple(5)); // 20

// 实际应用:折扣计算器
function createDiscountCalculator(discountRate) {
    return function(price) {
        return price * (1 - discountRate);
    };
}

const studentDiscount = createDiscountCalculator(0.1);  // 9折
const vipDiscount = createDiscountCalculator(0.2);      // 8折

console.log(studentDiscount(100));  // 90
console.log(vipDiscount(100));      // 80

示例4:闭包陷阱(循环问题)

// ❌ 错误示例:常见陷阱
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);  // 都输出 3
    }, 100);
}
// 原因:var没有块级作用域,所有函数共享同一个i

// ✅ 解决方法1:使用let
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);  // 输出 0, 1, 2
    }, 100);
}

// ✅ 解决方法2:使用闭包
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);  // 输出 0, 1, 2
        }, 100);
    })(i);
}

// ✅ 解决方法3:使用IIFE返回函数
for (var i = 0; i < 3; i++) {
    setTimeout(
        (function(j) {
            return function() {
                console.log(j);
            };
        })(i),
        100
    );
}

✍️ 练习任务

练习 1: 练习1:作用域测试

分析以下代码的输出:

let x = 10;

function test1() {
    let x = 20;
    console.log(x);
}

function test2() {
    console.log(x);
    let x = 30;
}

test1();
test2();
console.log(x);

问题

  1. test1() 输出什么?
  2. test2() 会报错吗?为什么?
  3. 最后的 console.log(x) 输出什么?

要求:解释每个输出的原因,说明作用域规则。

提示: 注意变量提升和暂时性死区(TDZ)

练习 2: 练习2:实现计数器

使用闭包实现一个计数器模块,要求:

  1. 提供以下方法:

    • increment(): 计数+1
    • decrement(): 计数-1
    • getCount(): 获取当前计数
    • reset(): 重置为0
  2. 计数值不能为负数

  3. 提供多个独立的计数器实例

示例用法

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

counter1.increment();
counter1.increment();
console.log(counter1.getCount());  // 2

counter2.increment();
console.log(counter2.getCount());  // 1

提示: 使用闭包保护count变量,返回对象包含操作方法

练习 3: 练习3:缓存函数结果

使用闭包实现一个memoize函数,缓存函数的计算结果:

function memoize(fn) {
    // 你的代码
}

// 慢速函数
function slowFunction(n) {
    console.log("计算中...");
    return n * n;
}

const memoized = memoize(slowFunction);

console.log(memoized(5));  // 输出:计算中... 25
console.log(memoized(5));  // 直接输出:25(不计算)
console.log(memoized(10)); // 输出:计算中... 100
console.log(memoized(5));  // 直接输出:25

要求

  • 第一次调用时执行并缓存结果
  • 相同参数直接返回缓存结果
  • 使用对象存储缓存

提示: 用参数作为key,结果作为value存储在对象中

🎓 今日挑战

今日挑战:实现事件管理器

使用闭包实现一个完整的事件管理器(EventEmitter):

功能要求

  1. on(event, callback):注册事件监听器
emitter.on('click', function(data) {
    console.log('点击事件:', data);
});
  1. emit(event, data):触发事件
emitter.emit('click', {x: 100, y: 200});
  1. off(event, callback):移除事件监听器
emitter.off('click', handler);
  1. once(event, callback):只触发一次的监听器
emitter.once('init', function() {
    console.log('只执行一次');
});

技术要求

  • 使用闭包保护events对象
  • 支持同一事件的多个监听器
  • once监听器触发后自动移除
  • 移除不存在的监听器不报错

测试代码

const emitter = createEventEmitter();

let count = 0;
emitter.on('test', () => count++);
emitter.on('test', () => count++);
emitter.once('test', () => count += 10);

emitter.emit('test');  // count = 12 (2 + 10)
emitter.emit('test');  // count = 14 (2,once已移除)

扩展功能(可选):

  • 支持通配符监听所有事件
  • 添加removeAllListeners(event)方法
  • 添加listenerCount(event)方法

提示: 使用对象存储事件,key是事件名,value是回调数组

💡 常见问题

Q1: 什么时候使用闭包?

A: 闭包的典型使用场景:

1. 数据私有化

function createBankAccount() {
    let balance = 0;
    return {
        deposit: (amount) => balance += amount,
        getBalance: () => balance
    };
}

2. 函数工厂

function createGreeter(greeting) {
    return function(name) {
        console.log(`${greeting}, ${name}!`);
    };
}

3. 维持状态

function createCounter() {
    let count = 0;
    return () => ++count;
}

什么时候避免

  • 不需要保持状态时,不要用闭包
  • 注意内存占用,闭包会一直保持作用域

Q2: 闭包会导致内存泄漏吗?

A: 闭包本身不会导致内存泄漏,但不小心使用会占用过多内存。

问题示例

function createElements() {
    let largeData = new Array(1000000);
    
    document.getElementById('btn')
        .addEventListener('click', function() {
            console.log('clicked');
            // largeData被闭包引用,不会被回收
        });
}

解决方案

function createElements() {
    let largeData = new Array(1000000);
    
    function handleClick() {
        console.log('clicked');
        // 不引用largeData
    }
    
    document.getElementById('btn')
        .addEventListener('click', handleClick);
    
    // 使用完后可以释放
    largeData = null;
}

最佳实践

  • 闭包中只引用需要的数据
  • 不再使用时设置为null
  • 移除事件监听器:removeEventListener

Q3: let、const、var有什么区别?

A: 三大区别

1. 作用域

  • var: 函数作用域
  • let/const: 块级作用域
if (true) {
    var a = 1;
    let b = 2;
}
console.log(a);  // 1
console.log(b);  // ReferenceError

2. 变量提升

  • var: 提升并初始化为undefined
  • let/const: 提升但不初始化(TDZ)
console.log(a);  // undefined
var a = 1;

console.log(b);  // ReferenceError
let b = 2;

3. 重复声明

  • var: 允许重复声明
  • let/const: 不允许
var a = 1;
var a = 2;  // OK

let b = 1;
let b = 2;  // SyntaxError

选择建议

  • 默认使用const(不会被重新赋值)
  • 需要重新赋值时使用let
  • 避免使用var

Q4: 如何调试闭包问题?

A: 调试闭包的技巧

1. 使用console.log查看闭包变量

function outer() {
    let count = 0;
    return function() {
        console.log('count:', count);  // 查看值
        count++;
    };
}

2. 浏览器开发者工具

  • 在闭包函数内设置断点
  • 查看Scope面板的Closure部分
  • 可以看到所有闭包变量

3. 检查意外的闭包

// 检查函数长度
function create() {
    let data = 'large data';
    return function() {
        console.log('hello');
        // data被意外引用
    };
}

// 解决:明确不引用
function create() {
    let data = 'large data';
    let result = function() {
        console.log('hello');
    };
    data = null;  // 释放引用
    return result;
}

4. 内存分析

  • 使用Chrome Memory面板
  • 拍摄堆快照
  • 查看分离的DOM树和闭包

学习时间: 2026-03-09 22:22
难度: ⭐⭐⭐⭐☆
预计用时: 2-3 小时
关键词: 作用域与闭包