JavaScript 作用域与闭包:深入理解词法作用域

27 阅读6分钟

学好JS,离全栈又近了一步,欢迎使用我的小程序👇👇👇👇

small.png


一、作用域基础

1.1 什么是作用域?

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

// 全局作用域
var globalVar = "我是全局变量";

function testScope() {
    // 函数作用域
    var functionVar = "我是函数内变量";
    console.log(globalVar); // 可以访问全局变量
    console.log(functionVar); // 可以访问函数内变量
}

testScope();
console.log(globalVar); // ✅ 可以访问
console.log(functionVar); // ❌ ReferenceError: functionVar is not defined

1.2 JavaScript 的作用域类型

1)全局作用域

  • 在函数外部声明的变量
  • 在整个程序中都可见
var global = "全局变量";
let globalLet = "全局let";
const globalConst = "全局const";

// 没有使用 var/let/const 声明的变量也会成为全局变量
function createGlobal() {
    accidentalGlobal = "意外创建的全局变量";
}
createGlobal();
console.log(accidentalGlobal); // ✅ 可以访问

2)函数作用域

  • 使用 var 声明的变量具有函数作用域
  • 在函数内部声明的变量只能在函数内部访问
function functionScope() {
    var a = 10;
    if (true) {
        var b = 20; // 使用var,仍然是函数作用域
        console.log(a); // ✅ 10
    }
    console.log(b); // ✅ 20(var没有块级作用域)
}
functionScope();
console.log(a); // ❌ ReferenceError
console.log(b); // ❌ ReferenceError

3)块级作用域(ES6+)

  • 使用 letconst 声明的变量具有块级作用域
  • 块是 {} 包围的代码区域
function blockScope() {
    if (true) {
        let blockLet = "块级let";
        const blockConst = "块级const";
        var functionVar = "函数var";
        
        console.log(blockLet); // ✅
        console.log(blockConst); // ✅
        console.log(functionVar); // ✅
    }
    
    console.log(functionVar); // ✅ var没有块级作用域
    console.log(blockLet); // ❌ ReferenceError
    console.log(blockConst); // ❌ ReferenceError
}
blockScope();

二、词法作用域(静态作用域)

2.1 什么是词法作用域?

词法作用域(Lexical Scope)意味着作用域在代码书写时就已经确定,而不是在运行时确定。

var name = "全局";

function outer() {
    var name = "outer";
    
    function inner() {
        console.log(name); // 输出什么?
    }
    
    return inner;
}

var innerFunc = outer();
innerFunc(); // 输出:"outer"

2.2 作用域链

当访问一个变量时,JavaScript 引擎会从当前作用域开始查找,然后逐级向上,直到全局作用域。

var global = "全局";

function level1() {
    var level1Var = "第一层";
    
    function level2() {
        var level2Var = "第二层";
        
        function level3() {
            var level3Var = "第三层";
            
            console.log(level3Var); // ✅ 当前作用域
            console.log(level2Var); // ✅ 父作用域
            console.log(level1Var); // ✅ 祖父作用域
            console.log(global);    // ✅ 曾祖父作用域
            console.log(notExist);  // ❌ ReferenceError
        }
        
        level3();
    }
    
    level2();
}

level1();

2.3 词法作用域 vs 动态作用域

// JavaScript 是词法作用域(静态作用域)
var value = "全局value";

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

function bar() {
    var value = "bar的value";
    foo(); // foo在定义时已经确定了作用域链
}

bar(); // 输出:"全局value"

// 如果是动态作用域,会输出:"bar的value"

三、闭包

3.1 什么是闭包?

闭包是一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

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

// 每次调用createCounter都会创建新的闭包
const counter2 = createCounter();
console.log(counter2()); // 1(独立的环境)

3.2 闭包的工作原理

function outer() {
    const secret = "我是秘密";
    
    return function inner() {
        console.log(secret); // 可以访问outer的变量
    };
}

const innerFunc = outer();
innerFunc(); // "我是秘密"

// 即使outer执行完毕,inner仍然可以访问secret
// 因为inner函数的作用域链保留了对外部环境的引用

3.3 闭包的常见应用场景

1)数据封装和私有变量

function createBankAccount(initialBalance) {
    let balance = initialBalance; // 私有变量
    
    return {
        deposit: function(amount) {
            balance += amount;
            return balance;
        },
        withdraw: function(amount) {
            if (amount <= balance) {
                balance -= amount;
                return balance;
            }
            return "余额不足";
        },
        getBalance: function() {
            return balance;
        }
    };
}

const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
// console.log(balance); // ❌ ReferenceError: balance is not defined

2)函数工厂

function createMultiplier(factor) {
    return function(x) {
        return x * factor;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

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

3)事件处理和回调

function setupButtons() {
    for (var i = 1; i <= 3; i++) {
        // 使用闭包保存每个按钮的索引
        (function(index) {
            document.getElementById(`btn-${index}`).addEventListener('click', function() {
                console.log(`按钮 ${index} 被点击`);
            });
        })(i);
    }
}

// ES6+ 更简洁的写法(使用let的块级作用域)
function setupButtonsES6() {
    for (let i = 1; i <= 3; i++) {
        document.getElementById(`btn-${i}`).addEventListener('click', function() {
            console.log(`按钮 ${i} 被点击`); // let为每个迭代创建新的块级作用域
        });
    }
}

4)模块模式

const calculator = (function() {
    let memory = 0;
    
    function add(a, b) {
        return a + b;
    }
    
    function subtract(a, b) {
        return a - b;
    }
    
    function store(value) {
        memory = value;
    }
    
    function recall() {
        return memory;
    }
    
    // 只暴露公共接口
    return {
        add,
        subtract,
        store,
        recall
    };
})();

console.log(calculator.add(5, 3)); // 8
calculator.store(100);
console.log(calculator.recall()); // 100
// console.log(memory); // ❌ 无法直接访问私有变量

四、闭包的注意事项

4.1 内存泄漏

// 不当使用闭包可能导致内存泄漏
function createHeavyObject() {
    const heavyData = new Array(1000000).fill('大数据');
    
    return function() {
        // 即使只需要一小部分数据,整个heavyData都被保留
        return heavyData[0];
    };
}

// 解决方法:使用后释放引用
function createOptimizedObject() {
    const heavyData = new Array(1000000).fill('大数据');
    const neededData = heavyData[0]; // 只提取需要的数据
    
    // 允许垃圾回收heavyData
    return function() {
        return neededData;
    };
}

4.2 循环中的闭包陷阱

// 常见问题
function problem() {
    for (var i = 0; i < 5; i++) {
        setTimeout(function() {
            console.log(i); // 全部输出5
        }, 100);
    }
}

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

// 解决方案2:使用let(推荐)
function solution2() {
    for (let i = 0; i < 5; i++) {
        setTimeout(function() {
            console.log(i); // 0,1,2,3,4
        }, 100);
    }
}

4.3 性能考虑

// 大量使用闭包可能影响性能
function createManyClosures() {
    const closures = [];
    
    for (let i = 0; i < 10000; i++) {
        closures.push((function(value) {
            return function() {
                return value;
            };
        })(i));
    }
    
    return closures;
}

// 在性能敏感的场景中,考虑替代方案

五、实际应用示例

5.1 防抖(Debounce)

function debounce(func, delay) {
    let timeoutId;
    
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 使用
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(query) {
    console.log(`搜索: ${query}`);
}, 300);

searchInput.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

5.2 节流(Throttle)

function throttle(func, limit) {
    let inThrottle;
    
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// 使用
window.addEventListener('scroll', throttle(function() {
    console.log('滚动事件处理');
}, 100));

5.3 缓存函数(Memoization)

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log('从缓存获取');
            return cache.get(key);
        }
        
        console.log('计算新值');
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// 使用
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 计算
console.log(fibonacci(10)); // 从缓存获取

总结

  1. 词法作用域是JavaScript的基础特性,作用域在代码书写时确定
  2. 闭包是函数和其周围状态(词法环境)的组合,允许函数访问创建时的作用域
  3. 闭包的实际应用广泛,包括:
    • 数据封装和私有变量
    • 函数工厂和柯里化
    • 事件处理和回调
    • 模块化编程
  4. 注意闭包的内存管理和性能影响
  5. 合理使用闭包可以写出更安全、更模块化的代码

理解作用域和闭包是掌握JavaScript高级编程的关键,它们是许多设计模式和最佳实践的基础。