惰性求值
惰性求值(Lazy Evaluation)是一种计算策略,它将表达式的求值延迟到真正需要结果的时候。与立即求值(Eager Evaluation)相对,惰性求值可以显著提高性能,特别是在处理大数据集或无限序列时。
什么是惰性求值?
惰性求值的核心思想是:不要在定义表达式时立即计算,而是在真正需要结果时才计算。这种策略有几个重要的优势:
- 避免不必要的计算:如果最终结果不需要某个中间值,那就不计算它
- 处理无限数据结构:可以表示和操作无限序列,只计算需要的部分
- 内存效率:不需要一次性将所有中间结果存储在内存中
- 组合优化:多个操作可以融合执行,减少中间数据结构
立即求值 vs 惰性求值
让我们通过一个例子来理解两种策略的区别:
// 立即求值(Eager Evaluation)
const eagerResult = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.map(x => {
console.log(`map: ${x}`);
return x * 2;
}) // 立即遍历整个数组,创建新数组 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
.filter(x => {
console.log(`filter: ${x}`);
return x > 10;
}); // 立即遍历整个数组,创建新数组 [12, 14, 16, 18, 20]
// 控制台输出:
// map: 1, map: 2, ..., map: 10
// filter: 2, filter: 4, ..., filter: 20
// 惰性求值(Lazy Evaluation)
// 只有在需要结果时才开始计算
// 而且可能只计算需要的部分
在立即求值中,每个操作都会遍历整个数组并创建新的中间数组。如果有三个链式操作,就会创建两个中间数组,遍历数组三次。
而在惰性求值中,数据像流水线一样流动:第一个元素经过所有操作后,才处理第二个元素。这意味着:
- 不需要创建中间数组
- 如果只需要前 N 个结果,可以提前停止
什么时候需要惰性求值?
惰性求值并非总是更好的选择。它最适合以下场景:
// 场景 1:大数据集,只需要部分结果
// 例如:在一百万条记录中找出前 10 个符合条件的
const findFirst10 = bigArray
.filter(isValid)
.map(transform)
.slice(0, 10);
// 场景 2:无限序列
// 例如:生成斐波那契数列的前 N 个数
const fibNumbers = fibonacciSequence().take(10);
// 场景 3:昂贵的计算
// 例如:只在需要时才发起网络请求或执行复杂计算
const expensiveResult = lazyCompute(() => heavyOperation());
// 场景 4:条件分支
// 例如:根据条件决定是否执行某些操作
const result = condition
? lazySequence.map(expensiveOperation).take(5)
: defaultValue;
对于小数据集或需要全部结果的场景,立即求值通常更简单直接。
JavaScript 中的惰性求值
JavaScript 默认是立即求值的语言,但它提供了几种机制来实现惰性求值:
生成器函数(Generator Functions)
生成器是 JavaScript 中实现惰性求值的核心工具。根据 MDN 文档,生成器函数可以"暂停执行并在之后恢复",这使我们能够按需产生值。
// 基本生成器
function* numberGenerator() {
console.log('开始生成');
yield 1;
console.log('生成第二个');
yield 2;
console.log('生成第三个');
yield 3;
console.log('结束');
}
const gen = numberGenerator();
// 此时没有任何输出,生成器还没有开始执行
console.log(gen.next()); // 开始生成 → { value: 1, done: false }
console.log(gen.next()); // 生成第二个 → { value: 2, done: false }
console.log(gen.next()); // 生成第三个 → { value: 3, done: false }
console.log(gen.next()); // 结束 → { value: undefined, done: true }
生成器的关键特性:
- 惰性执行:只有在调用
next()时才执行到下一个yield - 可暂停:执行到
yield时暂停,保存当前状态 - 可恢复:调用
next()时从暂停处继续执行
迭代器协议(Iterator Protocol)
JavaScript 的迭代器协议定义了一种标准方式来产生值序列。根据 MDN 文档,一个对象只要实现了 next() 方法,就是迭代器。
// 手动实现迭代器
const createCounter = (max) => {
let count = 0;
return {
next() {
if (count < max) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
}
};
};
// 可迭代对象(实现 [Symbol.iterator])
const createIterableCounter = (max) => ({
[Symbol.iterator]() {
let count = 0;
return {
next() {
if (count < max) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
}
};
}
});
// 可以在 for...of 中使用
for (const num of createIterableCounter(5)) {
console.log(num); // 0, 1, 2, 3, 4
}
// 可以使用展开运算符
console.log([...createIterableCounter(3)]); // [0, 1, 2]
Iterator Helpers(ES2023+)
ES2023 引入了 Iterator Helpers,为惰性求值提供了原生支持:
// 使用原生 Iterator Helpers(现代浏览器支持)
const lazyMap = function* (iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
};
const lazyFilter = function* (iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
};
// 使用
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = [...lazyFilter(lazyMap(numbers, x => x * 2), x => x > 10)];
console.log(result); // [12, 14, 16, 18, 20]
实现惰性序列
让我们实现一个完整的惰性序列类,支持常见的函数式操作:
class LazySequence {
constructor(generator) {
this.generator = generator;
}
// 从现有可迭代对象创建
static of(iterable) {
return new LazySequence(function* () {
yield* iterable;
});
}
// 创建数字范围
static range(start, end, step = 1) {
return new LazySequence(function* () {
for (let i = start; i < end; i += step) {
yield i;
}
});
}
// 无限重复一个值
static repeat(value) {
return new LazySequence(function* () {
while (true) {
yield value;
}
});
}
// 无限重复调用函数
static repeatedly(fn) {
return new LazySequence(function* () {
while (true) {
yield fn();
}
});
}
// 从种子值迭代生成
static iterate(fn, seed) {
return new LazySequence(function* () {
let value = seed;
while (true) {
yield value;
value = fn(value);
}
});
}
// ========== 变换操作 ==========
// 映射:对每个元素应用函数
map(fn) {
const self = this;
return new LazySequence(function* () {
for (const item of self.generator()) {
yield fn(item);
}
});
}
// 过滤:只保留满足条件的元素
filter(predicate) {
const self = this;
return new LazySequence(function* () {
for (const item of self.generator()) {
if (predicate(item)) {
yield item;
}
}
});
}
// 扁平化映射
flatMap(fn) {
const self = this;
return new LazySequence(function* () {
for (const item of self.generator()) {
yield* fn(item);
}
});
}
// ========== 截取操作 ==========
// 取前 n 个元素
take(n) {
const self = this;
return new LazySequence(function* () {
let count = 0;
for (const item of self.generator()) {
if (count >= n) break;
yield item;
count++;
}
});
}
// 持续取元素直到条件不满足
takeWhile(predicate) {
const self = this;
return new LazySequence(function* () {
for (const item of self.generator()) {
if (!predicate(item)) break;
yield item;
}
});
}
// 跳过前 n 个元素
drop(n) {
const self = this;
return new LazySequence(function* () {
let count = 0;
for (const item of self.generator()) {
if (count >= n) {
yield item;
}
count++;
}
});
}
// 跳过元素直到条件不满足
dropWhile(predicate) {
const self = this;
return new LazySequence(function* () {
let dropping = true;
for (const item of self.generator()) {
if (dropping && predicate(item)) {
continue;
}
dropping = false;
yield item;
}
});
}
// ========== 组合操作 ==========
// 连接另一个序列
concat(other) {
const self = this;
return new LazySequence(function* () {
yield* self.generator();
yield* other.generator();
});
}
// 与另一个序列配对
zip(other) {
const self = this;
return new LazySequence(function* () {
const gen1 = self.generator();
const gen2 = other.generator();
while (true) {
const r1 = gen1.next();
const r2 = gen2.next();
if (r1.done || r2.done) break;
yield [r1.value, r2.value];
}
});
}
// ========== 强制求值 ==========
// 转换为数组
toArray() {
return [...this.generator()];
}
// 归约
reduce(fn, initial) {
let acc = initial;
for (const item of this.generator()) {
acc = fn(acc, item);
}
return acc;
}
// 查找第一个满足条件的元素
find(predicate) {
for (const item of this.generator()) {
if (predicate(item)) {
return item;
}
}
return undefined;
}
// 检查是否存在满足条件的元素
some(predicate) {
for (const item of this.generator()) {
if (predicate(item)) {
return true;
}
}
return false;
}
// 检查是否所有元素都满足条件
every(predicate) {
for (const item of this.generator()) {
if (!predicate(item)) {
return false;
}
}
return true;
}
// 遍历执行副作用
forEach(fn) {
for (const item of this.generator()) {
fn(item);
}
}
// 计算长度(需要遍历整个序列)
length() {
let count = 0;
for (const _ of this.generator()) {
count++;
}
return count;
}
// 支持迭代协议
*[Symbol.iterator]() {
yield* this.generator();
}
}
无限序列
惰性求值的一个重要应用是处理无限序列。因为只有在需要时才计算,我们可以安全地表示无限的数据结构。
数学序列
// 自然数序列
const naturals = LazySequence.iterate(x => x + 1, 0);
console.log(naturals.take(5).toArray()); // [0, 1, 2, 3, 4]
// 斐波那契数列
const fibonacci = new LazySequence(function* () {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
});
// 获取前 10 个斐波那契数
console.log(fibonacci.take(10).toArray());
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// 获取第一个大于 1000 的斐波那契数
const firstLarge = fibonacci.find(x => x > 1000);
console.log(firstLarge); // 1597
// 偶数序列
const evens = LazySequence.iterate(x => x + 2, 0);
console.log(evens.take(5).toArray()); // [0, 2, 4, 6, 8]
// 2 的幂次序列
const powersOf2 = LazySequence.iterate(x => x * 2, 1);
console.log(powersOf2.take(8).toArray()); // [1, 2, 4, 8, 16, 32, 64, 128]
素数序列
// 使用埃拉托斯特尼筛法的惰性版本
function* primes() {
const sieve = new Map();
let n = 2;
while (true) {
if (!sieve.has(n)) {
// n 是素数
yield n;
// 标记 n^2, n^2 + n, n^2 + 2n, ... 为非素数
sieve.set(n * n, [n]);
} else {
// n 是合数,更新筛子
for (const p of sieve.get(n)) {
const next = n + p;
if (!sieve.has(next)) {
sieve.set(next, []);
}
sieve.get(next).push(p);
}
sieve.delete(n);
}
n++;
}
}
const primeSequence = new LazySequence(primes);
console.log(primeSequence.take(20).toArray());
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
// 判断一个数是否是素数
const isPrime = (n) => {
const gen = primes();
let p = gen.next().value;
while (p * p <= n) {
if (n % p === 0) return false;
p = gen.next().value;
}
return n > 1;
};
字符串生成
// 生成所有可能的字符串组合
function* generateStrings(chars, maxLength) {
for (let len = 1; len <= maxLength; len++) {
yield* generateOfLength(chars, '', len);
}
}
function* generateOfLength(chars, prefix, remaining) {
if (remaining === 0) {
yield prefix;
return;
}
for (const char of chars) {
yield* generateOfLength(chars, prefix + char, remaining - 1);
}
}
const abcStrings = new LazySequence(() => generateStrings('abc', 3));
console.log(abcStrings.take(15).toArray());
// ['a', 'b', 'c', 'aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc', 'aaa', 'aab', 'aac']
实际应用场景
处理大型数据集
当处理无法一次性加载到内存的数据时,惰性求值非常有用:
// 模拟大数据集处理
function* readLargeFile(filename) {
// 实际应用中,这里可能是逐行读取文件
let line = 0;
const maxLines = 1000000;
while (line < maxLines) {
// 模拟每行数据
yield {
id: line,
value: Math.random(),
timestamp: new Date()
};
line++;
}
}
// 处理日志:找出前 10 个异常
const processLog = (filename) => {
return LazySequence.of(readLargeFile(filename))
.filter(line => line.value < 0.1) // 假设小于 0.1 是异常
.map(line => ({
...line,
severity: 'ERROR'
}))
.take(10)
.toArray();
};
// 这样即使文件有百万行,也只会处理到找到 10 个异常为止
流式处理 CSV
// 惰性处理 CSV 文件
function* parseCSV(content) {
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
yield {
lineNumber: i,
data: lines[i].split(',')
};
}
}
const processCSV = (content) => {
const lines = LazySequence.of(parseCSV(content));
const headers = lines.take(1).toArray()[0].data;
return lines
.drop(1) // 跳过表头
.filter(row => row.data.length === headers.length) // 过滤无效行
.map(row => {
return headers.reduce((obj, header, i) => {
obj[header] = row.data[i];
return obj;
}, {});
});
};
// 使用
const csvContent = `name,age,city
张三,25,北京
李四,30,上海
王五,28,广州`;
const records = processCSV(csvContent).toArray();
console.log(records);
// [
// { name: '张三', age: '25', city: '北京' },
// { name: '李四', age: '30', city: '上海' },
// { name: '王五', age: '28', city: '广州' }
// ]
搜索算法
// 广度优先搜索(BFS)的惰性实现
function* bfs(start, getNeighbors) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
if (!visited.has(node)) {
visited.add(node);
yield node;
for (const neighbor of getNeighbors(node)) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}
}
// 使用:在迷宫中找最短路径
const maze = [
[0, 0, 1, 0, 0],
[1, 0, 1, 0, 1],
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 1, 0]
];
const getNeighbors = ([x, y]) => {
const directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
return directions
.map(([dx, dy]) => [x + dx, y + dy])
.filter(([nx, ny]) =>
nx >= 0 && nx < maze[0].length &&
ny >= 0 && ny < maze.length &&
maze[ny][nx] === 0
);
};
const path = LazySequence.of(bfs([0, 0], getNeighbors))
.take(20)
.toArray();
console.log(path);
分页数据加载
// 惰性分页数据加载
function* paginatedFetch(baseUrl, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
for (const item of data.items) {
yield item;
}
page++;
hasMore = data.hasMore;
}
}
}
// 使用:只加载需要的数据
async function searchProducts(query, maxResults = 20) {
const results = LazySequence.of(paginatedFetch('/api/products'))
.filter(product => product.name.includes(query))
.take(maxResults);
const products = [];
for await (const product of results.generator()) {
products.push(product);
}
return products;
}
性能优化
避免中间数组
惰性求值最大的性能优势在于避免创建中间数组:
// 性能对比
const bigArray = Array.from({ length: 1000000 }, (_, i) => i);
// 立即求值:创建多个中间数组
console.time('eager');
const eager = bigArray
.map(x => x * 2)
.filter(x => x % 3 === 0)
.slice(0, 10);
console.timeEnd('eager');
// 惰性求值:不创建中间数组
console.time('lazy');
const lazy = LazySequence.of(bigArray)
.map(x => x * 2)
.filter(x => x % 3 === 0)
.take(10)
.toArray();
console.timeEnd('lazy');
// 惰性求值通常更快,因为:
// 1. 不创建中间数组
// 2. 找到足够的元素后就停止
融合操作
惰性求值允许将多个操作融合为一次遍历:
// 立即求值:三次遍历
const result1 = bigArray
.map(x => x * 2) // 第一次遍历
.filter(x => x > 0) // 第二次遍历
.map(x => x + 1); // 第三次遍历
// 惰性求值:一次遍历
// 每个元素依次通过 map -> filter -> map
const result2 = LazySequence.of(bigArray)
.map(x => x * 2)
.filter(x => x > 0)
.map(x => x + 1)
.toArray();
记忆化与惰性求值
记忆化与惰性求值结合,可以实现延迟计算并缓存结果:
// 记忆化的惰性属性
function lazyProperty(obj, name, initializer) {
let value;
let initialized = false;
Object.defineProperty(obj, name, {
get() {
if (!initialized) {
console.log(`初始化 ${name}...`);
value = initializer();
initialized = true;
}
return value;
},
enumerable: true,
configurable: true
});
}
// 使用
const config = {};
lazyProperty(config, 'database', () => {
// 昂贵的初始化
return { host: 'localhost', port: 5432, connected: true };
});
// 第一次访问时才初始化
console.log(config.database); // 初始化 database... → { host: ..., ... }
console.log(config.database); // 直接返回缓存值
// 创建懒加载对象
function createLazyObject(computations) {
const obj = {};
for (const [key, fn] of Object.entries(computations)) {
lazyProperty(obj, key, fn);
}
return obj;
}
// 使用
const analytics = createLazyObject({
totalUsers: () => {
console.log('计算用户总数...');
return 10000;
},
activeUsers: () => {
console.log('计算活跃用户数...');
return 5000;
},
revenue: () => {
console.log('计算收入...');
return 100000;
}
});
// 只有访问的属性才会被计算
console.log(analytics.activeUsers); // 只计算 activeUsers
console.log(analytics.activeUsers); // 返回缓存值
与其他概念的关系
与柯里化的结合
惰性求值与柯里化结合,可以创建更灵活的数据处理管道:
// 柯里化的惰性操作
const takeWhile = (predicate) => (sequence) => sequence.takeWhile(predicate);
const dropWhile = (predicate) => (sequence) => sequence.dropWhile(predicate);
const take = (n) => (sequence) => sequence.take(n);
// 组合使用
const processStream = (sequence) =>
sequence
.filter(x => x > 0)
.map(x => x * 2)
.take(100);
// 更灵活的管道构建
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const pipeline = pipe(
sequence => sequence.filter(x => x > 0),
sequence => sequence.map(x => x * 2),
take(10)
);
const result = pipeline(LazySequence.range(0, 100));
与函数组合的结合
惰性序列与函数组合是天然的搭档:
// 构建可复用的处理管道
const removeEmpty = sequence => sequence.filter(x => x !== null && x !== undefined);
const trimStrings = sequence => sequence.map(x => typeof x === 'string' ? x.trim() : x);
const unique = sequence => {
const seen = new Set();
return new LazySequence(function* () {
for (const item of sequence.generator()) {
if (!seen.has(item)) {
seen.add(item);
yield item;
}
}
});
};
// 组合使用
const cleanData = pipe(removeEmpty, trimStrings, unique);
const raw = [null, ' hello ', 'world', 'world', undefined, 'test'];
const cleaned = cleanData(LazySequence.of(raw)).toArray();
console.log(cleaned); // ['hello', 'world', 'test']
注意事项
副作用的时机
惰性求值中,副作用的执行时机可能不确定:
// 警告:惰性求值中的副作用
let counter = 0;
const sequence = LazySequence.of([1, 2, 3])
.map(x => {
counter++; // 副作用
return x * 2;
});
console.log(counter); // 0(还没有求值)
const result = sequence.take(2).toArray();
console.log(counter); // 2(只执行了两次)
// 建议:避免在惰性操作中使用副作用
// 如果必须使用,确保理解执行时机
多次求值
惰性序列可以多次迭代,但每次都会重新计算:
const sequence = LazySequence.of([1, 2, 3])
.map(x => {
console.log(`计算 ${x}`);
return x * 2;
});
// 第一次求值
sequence.toArray();
// 计算 1, 计算 2, 计算 3
// 第二次求值会重新计算
sequence.toArray();
// 计算 1, 计算 2, 计算 3(再次计算)
// 如果需要缓存结果,使用 toArray() 后存起来
const cached = sequence.toArray();
内存考虑
虽然惰性求值避免了中间数组,但生成器本身也需要内存:
// 对于小数据集,惰性求值可能更慢
const small = [1, 2, 3];
// 立即求值可能更快,因为数组操作已经优化
// 对于大数据集,惰性求值优势明显
const large = LazySequence.range(0, 10000000);
large.take(10).toArray(); // 只计算 10 个
最佳实践
1. 在合适的场景使用
// 适合惰性求值的场景:
// - 大数据集,只需要部分结果
// - 无限序列
// - 昂贵的计算,可能不需要执行
// - 流式数据处理
// 不适合惰性求值的场景:
// - 小数据集
// - 需要多次访问结果(每次都会重新计算)
// - 简单的一次性操作
2. 明确终止条件
// 好的做法:明确限制结果数量
const result = infiniteSequence
.take(100)
.toArray();
// 危险:没有限制,可能无限循环
// const badResult = infiniteSequence.toArray();
3. 避免在惰性操作中使用外部状态
// 不好:依赖外部状态
let threshold = 10;
const result = sequence.filter(x => x > threshold);
threshold = 20; // 修改外部状态
// result 的行为可能不符合预期
// 好:使用纯函数
const filterGreaterThan = (threshold) => (sequence) =>
sequence.filter(x => x > threshold);
const result = filterGreaterThan(10)(sequence);
4. 结合 TypeScript 使用
interface LazySequence<T> {
map<U>(fn: (value: T) => U): LazySequence<U>;
filter(predicate: (value: T) => boolean): LazySequence<T>;
take(n: number): LazySequence<T>;
toArray(): T[];
reduce<U>(fn: (acc: U, value: T) => U, initial: U): U;
}
// 类型安全的惰性序列
const numbers: LazySequence<number> = LazySequence.range(0, 100);
const strings: LazySequence<string> = numbers.map(x => x.toString());
const result: string[] = strings.take(10).toArray();
小结
惰性求值是函数式编程中的重要技术:
核心概念:
- 延迟计算,只在需要时执行
- 避免不必要的计算和中间数据结构
- 可以处理无限序列
实现方式:
- JavaScript 生成器函数(
function*) - 迭代器协议(
[Symbol.iterator]) - 自定义惰性序列类
主要优势:
- 内存效率:不需要存储所有中间结果
- 计算效率:避免不必要的计算
- 表达能力:可以表示无限数据结构
适用场景:
- 大数据集处理
- 无限序列(斐波那契、素数等)
- 流式数据处理
- 条件分支中的延迟计算
注意事项:
- 副作用的执行时机不确定
- 多次迭代会重复计算
- 小数据集可能不如立即求值高效
惰性求值与函数组合、柯里化等技术结合,可以构建强大而优雅的数据处理管道。理解惰性求值,是深入掌握函数式编程的重要一步。