JavaScript闭包与作用域
JavaScript中的闭包和作用域是该语言最强大也是最容易被误解的特性之一。本文将深入探讨这两个概念,帮助你更好地理解和应用它们。
作用域基础
什么是作用域?
作用域是指程序中定义变量的区域,它决定了变量的可访问性和生命周期。JavaScript中主要有以下几种作用域:
- 全局作用域:在代码的任何地方都能访问的变量
- 函数作用域:在函数内部定义的变量
- 块级作用域:使用
let
和const
声明的变量所在的代码块(ES6引入)
// 全局作用域
const globalVar = "我是全局变量";
function exampleFunction() {
// 函数作用域
const functionVar = "我是函数作用域变量";
console.log(globalVar); // 可以访问全局变量
console.log(functionVar); // 可以访问函数作用域变量
if (true) {
// 块级作用域
const blockVar = "我是块级作用域变量";
console.log(blockVar); // 可以访问块级作用域变量
}
// console.log(blockVar); // 错误:blockVar未定义
}
exampleFunction();
console.log(globalVar); // 可以访问全局变量
// console.log(functionVar); // 错误:functionVar未定义
作用域链
当JavaScript引擎查找变量时,它会从最内层的作用域开始查找,如果找不到,就会向外层作用域继续查找,直到找到该变量或到达全局作用域。这个查找过程形成了作用域链。
const global = "全局变量";
function outer() {
const outerVar = "外层函数变量";
function inner() {
const innerVar = "内层函数变量";
console.log(innerVar); // 内层函数变量
console.log(outerVar); // 外层函数变量
console.log(global); // 全局变量
}
inner();
console.log(outerVar); // 外层函数变量
// console.log(innerVar); // 错误:innerVar未定义
}
outer();
console.log(global); // 全局变量
// console.log(outerVar); // 错误:outerVar未定义
变量提升
在JavaScript中,使用var
声明的变量和函数声明会被"提升"到其所在作用域的顶部。
console.log(hoistedVar); // undefined(不会报错)
var hoistedVar = "我被提升了";
hoistedFunction(); // "我是被提升的函数"
function hoistedFunction() {
console.log("我是被提升的函数");
}
// console.log(notHoisted); // 错误:notHoisted未定义
let notHoisted = "我没有被提升";
// notHoistedFunction(); // 错误:notHoistedFunction不是一个函数
const notHoistedFunction = function() {
console.log("我没有被提升");
};
var、let和const的区别
- var:函数作用域,会被提升,可以重复声明,可以在声明前使用(值为undefined)
- let:块级作用域,不会被提升,不能重复声明,不能在声明前使用(暂时性死区)
- const:与let类似,但声明时必须初始化,且不能重新赋值(但对象和数组的内容可以修改)
// var示例
var x = 1;
var x = 2; // 允许重复声明
console.log(x); // 2
// let示例
let y = 1;
// let y = 2; // 错误:不能重复声明
y = 2; // 可以重新赋值
console.log(y); // 2
// const示例
const z = 1;
// z = 2; // 错误:不能重新赋值
// 但const声明的对象内容可以修改
const obj = { name: "张三" };
obj.name = "李四"; // 允许
console.log(obj.name); // "李四"
// obj = { name: "王五" }; // 错误:不能重新赋值
暂时性死区(Temporal Dead Zone)
使用let
和const
声明的变量从块的开始到声明处这段区域被称为"暂时性死区",在这个区域内,变量不能被访问。
{
// 从这里开始是age的暂时性死区
// console.log(age); // 错误:不能在声明前访问'age'
let age = 25; // 暂时性死区结束
console.log(age); // 25
}
闭包详解
什么是闭包?
闭包是指一个函数能够访问其词法作用域之外的变量的能力。更具体地说,当一个函数在其父函数作用域之外被调用时,它仍然能够访问其父函数作用域中的变量。
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
函数返回了一个内部函数,这个内部函数形成了一个闭包,它可以访问和修改createCounter
函数中的count
变量,即使createCounter
函数已经执行完毕。
闭包的实际应用
1. 数据封装和私有变量
function createPerson(name) {
// 私有变量
let _age = 0;
return {
getName: function() {
return name;
},
getAge: function() {
return _age;
},
setAge: function(age) {
if (age > 0) {
_age = age;
}
},
incrementAge: function() {
_age++;
}
};
}
const person = createPerson("张三");
console.log(person.getName()); // "张三"
person.setAge(25);
console.log(person.getAge()); // 25
person.incrementAge();
console.log(person.getAge()); // 26
// console.log(person._age); // undefined,无法直接访问私有变量
2. 函数工厂和柯里化
// 函数工厂
function multiply(x) {
return function(y) {
return x * y;
};
}
const multiplyByTwo = multiply(2);
const multiplyByTen = multiply(10);
console.log(multiplyByTwo(3)); // 2 * 3 = 6
console.log(multiplyByTen(5)); // 10 * 5 = 50
// 柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
3. 事件处理和回调
function setupButton(buttonId, message) {
const button = document.getElementById(buttonId);
button.addEventListener("click", function() {
// 闭包捕获了message变量
console.log(message);
});
}
setupButton("button1", "按钮1被点击了");
setupButton("button2", "按钮2被点击了");
4. 模块模式
const calculator = (function() {
// 私有变量和函数
let result = 0;
function add(x) {
result += x;
}
function subtract(x) {
result -= x;
}
// 公共API
return {
add: function(x) {
add(x);
return this;
},
subtract: function(x) {
subtract(x);
return this;
},
getResult: function() {
return result;
},
reset: function() {
result = 0;
return this;
}
};
})();
calculator.add(5).subtract(2).add(10);
console.log(calculator.getResult()); // 13
calculator.reset();
console.log(calculator.getResult()); // 0
闭包的注意事项
1. 内存管理
闭包可能导致内存泄漏,因为它们会保持对外部变量的引用,阻止垃圾回收机制回收这些变量。
function createLargeArray() {
// 一个大数组,占用大量内存
const largeArray = new Array(1000000).fill("大量数据");
return function() {
// 引用了外部的largeArray
console.log(largeArray.length);
};
}
let printArrayLength = createLargeArray(); // largeArray在内存中
printArrayLength(); // 1000000
// 当不再需要时,应该解除引用
printArrayLength = null; // 现在largeArray可以被垃圾回收
2. 循环中的闭包
在循环中创建闭包时,需要注意变量捕获问题。
// 错误的方式
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const fns1 = createFunctions();
fns1[0](); // 3
fns1[1](); // 3
fns1[2](); // 3
// 正确的方式 - 使用IIFE
function createFunctionsWithIIFE() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push((function(value) {
return function() {
console.log(value);
};
})(i));
}
return functions;
}
const fns2 = createFunctionsWithIIFE();
fns2[0](); // 0
fns2[1](); // 1
fns2[2](); // 2
// 正确的方式 - 使用let
function createFunctionsWithLet() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const fns3 = createFunctionsWithLet();
fns3[0](); // 0
fns3[1](); // 1
fns3[2](); // 2
作用域和闭包的高级概念
执行上下文
每当JavaScript代码执行时,都会创建一个执行上下文。执行上下文包含三个主要部分:
- 变量对象:包含函数的参数、内部变量和函数声明
- 作用域链:当前上下文和所有父级上下文的变量对象列表
- this值:函数调用时的上下文对象
function outer() {
console.log(this); // 取决于调用方式
const outerVar = "外部变量";
function inner() {
console.log(this); // 取决于调用方式
console.log(outerVar); // 通过作用域链访问
}
inner(); // 常规函数调用,this指向全局对象(非严格模式)或undefined(严格模式)
}
outer(); // 常规函数调用
const obj = { method: outer };
obj.method(); // 方法调用,this指向obj
词法作用域与动态作用域
JavaScript使用词法作用域(也称为静态作用域),这意味着函数的作用域在函数定义时确定,而不是在函数调用时确定。
const x = "全局";
function first() {
const x = "first函数";
second(); // 调用second函数
}
function second() {
console.log(x); // 输出什么?
}
first(); // 输出:"全局"(因为词法作用域)
如果JavaScript使用动态作用域,上面的代码会输出"first函数",但由于JavaScript使用词法作用域,所以输出"全局"。
this关键字与箭头函数
在JavaScript中,this
关键字的值取决于函数的调用方式,而不是函数的定义位置。
const person = {
name: "张三",
regularFunction: function() {
console.log(this.name); // "张三"
function innerFunction() {
console.log(this.name); // undefined(在浏览器中是全局对象的name属性)
}
innerFunction();
},
arrowFunction: function() {
console.log(this.name); // "张三"
const inner = () => {
console.log(this.name); // "张三"(箭头函数继承外层函数的this)
};
inner();
}
};
person.regularFunction();
person.arrowFunction();
箭头函数不绑定自己的this
,而是继承外层作用域的this
值,这使得它们非常适合在需要保持外层this
上下文的场景中使用。
模块作用域
ES6引入了模块系统,每个模块都有自己的作用域。模块中的变量、函数和类默认是私有的,除非显式导出。
// math.js
const PI = 3.14159;
export function calculateCircleArea(radius) {
return PI * radius * radius;
}
export function calculateCircleCircumference(radius) {
return 2 * PI * radius;
}
// app.js
import { calculateCircleArea, calculateCircleCircumference } from './math.js';
console.log(calculateCircleArea(5)); // 78.53975
console.log(calculateCircleCircumference(5)); // 31.4159
// console.log(PI); // 错误:PI是math.js模块的私有变量
闭包与异步编程
闭包在异步编程中非常有用,特别是在处理回调函数和Promise时。
function fetchData(url) {
const data = { timestamp: Date.now() };
return new Promise((resolve) => {
setTimeout(() => {
// 闭包捕获了url和data变量
resolve({
url: url,
result: `来自${url}的数据`,
timestamp: data.timestamp
});
}, 1000);
});
}
fetchData('https://example.com/api')
.then(response => {
console.log(response);
// {url: "https://example.com/api", result: "来自https://example.com/api的数据", timestamp: 1234567890}
});
闭包与高阶函数
高阶函数是指接受函数作为参数或返回函数的函数。闭包经常与高阶函数一起使用,创建更强大的抽象。
// 函数组合
function compose(...functions) {
return function(x) {
return functions.reduceRight((acc, fn) => fn(acc), x);
};
}
function addOne(x) { return x + 1; }
function double(x) { return x * 2; }
function square(x) { return x * x; }
const calculate = compose(square, double, addOne);
console.log(calculate(3)); // square(double(addOne(3))) = square(double(4)) = square(8) = 64
// 函数记忆(缓存)
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] === undefined) {
cache[key] = fn.apply(this, args);
}
return cache[key];
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoizedFibonacci = memoize(function(n) {
if (n <= 1) return n;
return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});
console.time('普通fibonacci');
console.log(fibonacci(35)); // 9227465,计算很慢
console.timeEnd('普通fibonacci');
console.time('记忆化fibonacci');
console.log(memoizedFibonacci(35)); // 9227465,计算很快
console.timeEnd('记忆化fibonacci');
实际应用示例
1. 实现一个简单的状态管理器
function createStore(initialState = {}) {
let state = initialState;
const listeners = [];
function getState() {
// 返回状态的副本,防止直接修改
return JSON.parse(JSON.stringify(state));
}
function setState(newState) {
state = { ...state, ...newState };
// 通知所有监听器
listeners.forEach(listener => listener(state));
}
function subscribe(listener) {
listeners.push(listener);
// 返回取消订阅的函数
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
};
}
return {
getState,
setState,
subscribe
};
}
// 使用示例
const store = createStore({ count: 0, user: null });
// 订阅状态变化
const unsubscribe = store.subscribe(state => {
console.log('状态已更新:', state);
});
// 修改状态
store.setState({ count: 1 });
// 输出: 状态已更新: { count: 1, user: null }
store.setState({ user: { name: '张三', age: 30 } });
// 输出: 状态已更新: { count: 1, user: { name: '张三', age: 30 } }
// 取消订阅
unsubscribe();
// 再次修改状态,不会触发已取消的订阅
store.setState({ count: 2 });
2. 实现防抖和节流函数
// 防抖函数 - 延迟执行,重置计时器
function debounce(fn, delay) {
let timer = null;
return function(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
// 节流函数 - 按时间间隔执行
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const context = this;
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(context, args);
lastTime = now;
}
};
}
// 使用示例
const expensiveCalculation = () => {
console.log('执行昂贵计算...');
// 假设这是一个耗时的操作
};
// 防抖版本 - 只有在用户停止输入500ms后才执行
const debouncedCalculation = debounce(expensiveCalculation, 500);
// 节流版本 - 最多每1000ms执行一次
const throttledCalculation = throttle(expensiveCalculation, 1000);
// 在实际应用中
// window.addEventListener('resize', debouncedCalculation);
// document.getElementById('search').addEventListener('input', debouncedCalculation);
// window.addEventListener('scroll', throttledCalculation);
3. 实现一个简单的观察者模式
class EventEmitter {
constructor() {
this.events = {};
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
return () => this.off(eventName, callback);
}
// 取消订阅
off(eventName, callback) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName]
.filter(cb => cb !== callback);
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
// 触发一次后自动取消订阅
once(eventName, callback) {
const onceWrapper = (...args) => {
callback.apply(this, args);
this.off(eventName, onceWrapper);
};
return this.on(eventName, onceWrapper);
}
// 触发事件
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(callback => {
callback.apply(this, args);
});
}
}
// 使用示例
const emitter = new EventEmitter();
// 订阅事件
const unsubscribe = emitter.on('userLogin', user => {
console.log(`用户 ${user.name} 已登录`);
});
// 订阅一次性事件
emitter.once('firstVisit', () => {
console.log('欢迎首次访问!');
});
// 触发事件
emitter.emit('userLogin', { name: '张三', id: 1 });
// 输出: 用户 张三 已登录
emitter.emit('firstVisit');
// 输出: 欢迎首次访问!
// 再次触发一次性事件,不会有输出
emitter.emit('firstVisit');
// 取消订阅
unsubscribe();
// 再次触发事件,不会有输出
emitter.emit('userLogin', { name: '李四', id: 2 });
4. 实现一个简单的单例模式
function Singleton(className) {
let instance;
return function(...args) {
if (!instance) {
instance = new className(...args);
}
return instance;
};
}
// 使用示例
class Database {
constructor(host, port) {
this.host = host;
this.port = port;
this.connected = false;
console.log(`创建数据库连接: ${host}:${port}`);
}
connect() {
if (this.connected) {
console.log('已经连接到数据库');
return;
}
console.log(`连接到数据库: ${this.host}:${this.port}`);
this.connected = true;
}
query(sql) {
if (!this.connected) {
throw new Error('请先连接数据库');
}
console.log(`执行查询: ${sql}`);
return `查询结果: ${sql}`;
}
}
// 创建单例构造函数
const SingletonDatabase = Singleton(Database);
// 创建实例
const db1 = new SingletonDatabase('localhost', 3306);
db1.connect();
// 输出:
// 创建数据库连接: localhost:3306
// 连接到数据库: localhost:3306
// 尝试创建另一个实例
const db2 = new SingletonDatabase('127.0.0.1', 5432);
// 不会创建新实例,不会输出创建信息
console.log(db1 === db2); // true,它们是同一个实例
db2.query('SELECT * FROM users');
// 输出: 执行查询: SELECT * FROM users
总结
JavaScript中的作用域和闭包是该语言最基础也是最强大的特性之一。理解这些概念对于编写高效、可维护的JavaScript代码至关重要。
主要要点:
- 作用域决定了变量的可访问性和生命周期,JavaScript中有全局作用域、函数作用域和块级作用域。
- 作用域链使得内部作用域可以访问外部作用域的变量。
- 闭包是函数能够访问其词法作用域之外变量的能力,即使函数在其定义的作用域之外执行。
- 闭包的主要应用包括数据封装、函数工厂、回调函数和模块模式等。
- 使用闭包时需要注意内存管理和变量捕获问题。
- JavaScript使用词法作用域,函数的作用域在定义时确定,而不是在调用时。
- this关键字的值取决于函数的调用方式,而箭头函数继承外层作用域的this值。
- ES6引入的模块系统为每个模块提供了独立的作用域。
通过深入理解作用域和闭包,你可以更好地利用JavaScript的特性,编写更清晰、更高效的代码。