作用域与闭包

全局作用域

全局作用域是最外层的作用域。

全局变量

// 全局变量
var globalVar = "全局变量";
let globalLet = "全局let";
const GLOBAL_CONST = "全局常量";

// 在任何地方都可以访问
function showGlobal() {
    console.log(globalVar);  // "全局变量"
}

// 全局变量成为window的属性(浏览器环境)
console.log(window.globalVar);  // "全局变量"
console.log(window.globalLet);  // undefined(let不会)

全局污染

// 不推荐:污染全局命名空间
var name = "东巴文";
var age = 1;

// 推荐:使用命名空间或模块
const App = {
    name: "东巴文",
    age: 1
};

// 或使用IIFE
(function() {
    var name = "东巴文";
    var age = 1;
})();

避免全局变量

// 使用模块
// module.js
export const name = "东巴文";
export const age = 1;

// 使用块级作用域
{
    let name = "东巴文";
    const age = 1;
}

// 使用函数作用域
function init() {
    var name = "东巴文";
    var age = 1;
}

函数作用域

函数内部创建的作用域。

基本概念

function outer() {
    var message = "函数作用域";
    
    function inner() {
        console.log(message);  // 可以访问外层变量
    }
    
    inner();
    console.log(message);  // "函数作用域"
}

outer();
// console.log(message);  // ReferenceError

var的函数作用域

function test() {
    if (true) {
        var x = 1;  // 函数作用域
    }
    console.log(x);  // 1(可以在if外访问)
}

test();

变量提升

function test() {
    console.log(x);  // undefined(提升但未赋值)
    var x = 1;
    console.log(x);  // 1
}

// 等价于
function test() {
    var x;           // 声明提升
    console.log(x);  // undefined
    x = 1;           // 赋值不提升
    console.log(x);  // 1
}

函数声明提升

function test() {
    console.log(foo());  // "foo"(函数声明提升)
    
    function foo() {
        return "foo";
    }
}

// 函数表达式不会提升
function test2() {
    // bar();  // TypeError
    var bar = function() {
        return "bar";
    };
}

块级作用域

ES6引入的letconst具有块级作用域。

基本概念

{
    let x = 1;
    const Y = 2;
    var z = 3;
}

// console.log(x);  // ReferenceError
// console.log(Y);  // ReferenceError
console.log(z);  // 3(var没有块级作用域)

块级作用域场景

// if语句
if (true) {
    let blockScoped = "块级作用域";
}
// console.log(blockScoped);  // ReferenceError

// for循环
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);  // 0 1 2
}
// console.log(i);  // ReferenceError

// for循环使用var的问题
for (var j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100);  // 3 3 3
}

暂时性死区

{
    // 暂时性死区开始
    // console.log(x);  // ReferenceError
    let x = 1;  // 暂时性死区结束
    console.log(x);  // 1
}

// typeof也会报错
{
    // typeof x;  // ReferenceError
    let x = 1;
}

let vs var

特性 var let 东巴文建议
作用域 函数 -
提升 是(但TDZ) -
重复声明 允许 不允许 用let
全局属性 -
重新赋值 可以 可以 -

作用域链

作用域链决定了变量的查找顺序。

查找规则

let global = "全局";

function outer() {
    let outer = "外层";
    
    function inner() {
        let inner = "内层";
        
        console.log(inner);  // 在当前作用域找到
        console.log(outer);  // 在外层作用域找到
        console.log(global); // 在全局作用域找到
    }
    
    inner();
}

outer();

作用域链图示

┌─────────────────────────────────────┐
│ 全局作用域                           │
│   global = "全局"                    │
│   ┌─────────────────────────────┐   │
│   │ outer函数作用域              │   │
│   │   outer = "外层"             │   │
│   │   ┌─────────────────────┐   │   │
│   │   │ inner函数作用域      │   │   │
│   │   │   inner = "内层"     │   │   │
│   │   └─────────────────────┘   │   │
│   └─────────────────────────────┘   │
└─────────────────────────────────────┘

变量遮蔽

let x = "全局";

function outer() {
    let x = "外层";  // 遮蔽全局x
    
    function inner() {
        let x = "内层";  // 遮蔽外层x
        console.log(x);  // "内层"
    }
    
    inner();
    console.log(x);  // "外层"
}

outer();
console.log(x);  // "全局"

词法作用域

JavaScript使用词法作用域(静态作用域)。

静态作用域

let x = 10;

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

function bar() {
    let x = 20;
    foo();  // 输出10,不是20
}

bar();  // 10

作用域在定义时确定

function createPrinter() {
    let message = "东巴文";
    
    return function() {
        console.log(message);  // 词法作用域:定义时确定
    };
}

const printer = createPrinter();
printer();  // "东巴文"(即使createPrinter已执行完毕)

动态作用域对比

// JavaScript是词法作用域
let name = "全局";

function greet() {
    console.log(name);  // 使用定义时的作用域
}

function run() {
    let name = "局部";
    greet();  // 输出"全局",不是"局部"
}

run();  // "全局"

// 如果是动态作用域(如Bash),会输出"局部"

闭包概念

闭包是函数和其词法环境的组合。

什么是闭包

function createCounter() {
    let count = 0;  // 私有变量
    
    return {
        increment() {
            count++;
            return count;
        },
        decrement() {
            count--;
            return count;
        },
        getCount() {
            return count;
        }
    };
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.getCount());   // 2
// count变量被"封闭"在闭包中

闭包的形成条件

// 1. 函数嵌套
// 2. 内部函数引用外部函数的变量
// 3. 内部函数被返回或传递到外部

function outer() {
    let outerVar = "外部变量";
    
    function inner() {
        console.log(outerVar);  // 引用外部变量
    }
    
    return inner;  // 返回内部函数
}

const closure = outer();
closure();  // "外部变量"

闭包的本质

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

const sayHello = createGreeter("你好");
const sayHi = createGreeter("嗨");

sayHello("东巴文");  // 你好, 东巴文!
sayHi("JavaScript");  // 嗨, JavaScript!

// greeting被保存在闭包中

闭包应用

闭包在实际开发中有很多应用场景。

数据私有化

function createUser(name) {
    let _name = name;  // 私有变量
    
    return {
        getName() {
            return _name;
        },
        setName(newName) {
            _name = newName;
        }
    };
}

const user = createUser("东巴文");
console.log(user.getName());  // "东巴文"
user.setName("JavaScript");
console.log(user.getName());  // "JavaScript"
// _name无法直接访问

函数工厂

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

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

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

回调与事件处理

function setupButtons() {
    const buttons = document.querySelectorAll("button");
    
    buttons.forEach((button, index) => {
        button.addEventListener("click", function() {
            console.log(`按钮${index}被点击`);
        });
    });
}

// 柯里化
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...moreArgs) {
            return curried.apply(this, args.concat(moreArgs));
        };
    };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3));  // 6

防抖与节流

// 防抖
function debounce(fn, delay) {
    let timer = null;
    
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// 节流
function throttle(fn, interval) {
    let lastTime = 0;
    
    return function(...args) {
        const now = Date.now();
        if (now - lastTime >= interval) {
            lastTime = now;
            fn.apply(this, args);
        }
    };
}

模块模式

const module = (function() {
    let privateVar = "私有变量";
    
    function privateMethod() {
        console.log("私有方法");
    }
    
    return {
        publicVar: "公共变量",
        publicMethod() {
            console.log(privateVar);
            privateMethod();
        }
    };
})();

module.publicMethod();  // 可以访问
// module.privateVar;  // 无法访问

闭包注意事项

闭包使用不当可能导致问题。

内存占用

// 闭包会保持对外部变量的引用
function createHeavyClosures() {
    const largeData = new Array(1000000).fill("data");
    
    return function() {
        console.log(largeData.length);  // largeData不会被回收
    };
}

// 解决:不需要时设为null
function createBetterClosure() {
    let largeData = new Array(1000000).fill("data");
    
    return function() {
        console.log(largeData.length);
        largeData = null;  // 允许垃圾回收
    };
}

循环中的闭包

// 问题:使用var
for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);  // 4 4 4
    }, 100);
}

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

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

// 解决方案3:使用forEach
[1, 2, 3].forEach(function(i) {
    setTimeout(function() {
        console.log(i);  // 1 2 3
    }, 100);
});

性能考虑

// 不推荐:每次调用都创建新闭包
function process(data) {
    const processItem = function(item) {
        // 处理逻辑
    };
    
    return data.map(processItem);
}

// 推荐:函数定义在外部
function processItem(item) {
    // 处理逻辑
}

function process(data) {
    return data.map(processItem);
}

立即执行函数IIFE

IIFE(Immediately Invoked Function Expression)是立即执行的函数表达式。

基本语法

// 标准形式
(function() {
    console.log("立即执行");
})();

// 另一种形式
(function() {
    console.log("立即执行");
}());

// 箭头函数形式
(() => {
    console.log("立即执行");
})();

带参数的IIFE

(function(name) {
    console.log(`你好,${name}!`);
})("东巴文");

// 返回值
const result = (function(a, b) {
    return a + b;
})(1, 2);

console.log(result);  // 3

IIFE的作用

// 创建独立作用域
(function() {
    var private = "私有变量";
    // 不会污染全局
})();

// 模块化
const module = (function() {
    let private = "私有";
    
    return {
        get() {
            return private;
        }
    };
})();

// 解决循环问题
for (var i = 1; i <= 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 100);
    })(i);
}

现代替代方案

// IIFE
(function() {
    let private = "私有";
})();

// 块级作用域替代
{
    let private = "私有";
}

// 模块替代
// module.js
const private = "私有";
export const get = () => private;

下一步

掌握了作用域与闭包后,让我们继续学习:

  1. 高阶函数 - 学习高阶函数
  2. this与执行上下文 - 深入理解this
  3. 数组基础 - 学习数组操作

东巴文(db-w.cn) - 让编程学习更简单

🔒 东巴文寄语:闭包是JavaScript最强大的特性之一,理解闭包对于编写高质量代码至关重要。掌握作用域链和闭包,让你的代码更加优雅和强大。在 db-w.cn,我们帮你深入理解每一个概念!