从延迟处理解析JavaScript惰性编程

作者:函数茶馆

时间:2026-10-26

浏览:309

本文深入解析JavaScript中的延迟处理与惰性编程思想。从闭包、柯里化到纯函数,阐述延迟处理的理论基础;对比Haskell的thunk机制,探讨JavaScript中Promise与Generator在异步控制上的差异;通过无限序列与串行请求实例,展示如何利用Generator实现精确的惰性求值与异步流程控制。

文章目录

前文回顾

回顾历史脉络,JavaScript 的设计基因中蕴含着函数式编程的核心理念。

从柯里化概念出发,系统梳理 JavaScript 中关键的高阶函数应用。

从纯函数概念切入,深入探讨函子(Monad)这一深刻的函数式编程结构。

我们沿着闭包起源、柯里化及各类高阶函数的脉络,进一步讲解纯函数及其组合与简化演算的逻辑;

核心收获包括:

  • 闭包的诞生源于 lambda 表达式仅支持单参数设计的限制,该概念最早出现于 1930 年;
  • 柯里化与闭包具有同源性,柯里化的思想构成了高阶函数设计的重要指导原则;
  • 函数式编程中的函数运算可类比数学推导,无副作用的纯函数与函数组合使得代码更具可读性;

本文将重点展开“延迟处理”这一主题,直接进入核心内容。

延迟处理

仔细研读前文可以发现,尽管未专门设立“延迟处理”章节,但该思想贯穿始终。

首先,闭包体现了延迟处理:函数在声明时即确定了其作用域关系。例如以下代码:

function addA(A){
    return function(B){
        return B+A
    }
}
let count = addA(7)
console.log(count(8)) // 15

调用 addA(7) 函数时,它不会立即执行运算,而是返回一个新函数及一个包含引用变量值的“闭包”。当后续需要计算时,再从闭包中获取值进行运算。

其次,柯里化与闭包同源,由 add(1,2,3) 柯里化为 add(1)(2)(3)() 的过程中,在判定最终参数为空之前,始终返回一个待执行的函数,不进行实际的运算处理。

function addCurry() {
    let arr = [...arguments]
    let fn = function () {
        if(arguments.length === 0) {
	    return arr.reduce((a, b) => a + b) // 当参数为空时才执行求和计算;
        } else {
            arr.push(...arguments)
            return fn
        }
    }
    return fn
}

此外,在纯函数中,HTTP、IO 或 DOM 操作等业务场景难以避免副作用。为此,采用“盒子”机制包裹不纯函数,通过链式声明调用关系,直到最后执行 monad.value() 时才暴露副作用,从而最大程度限制并延迟了副作用的影响。

因此,“延迟处理”思想已深植于函数式编程的各个核心要点中。

从专栏整体视角来看,至此文章已至中段。除了围绕“闭包”这一核心,另一个核心点“异步”也将逐渐展开。

延迟处理是连接 JavaScript 闭包与异步两大核心概念的重要桥梁。

惰性求值

“延迟处理”在函数式编程语言中还有一个更官方、学术的名称,即“惰性求值”。

🌰我们不妨通过一段代码进行简要示例:

// 示例代码 1

const myFunction = function(a, b, c) {
  let result1 = longCalculation1(a,b);
  let result2 = longCalculation2(b,c);
  let result3 = longCalculation3(a,c);
  if (result1 < 10) {
    return result1;
  } else if (result2 < 100) {
    return result2;
  } else {
    return result3;
  }
}

这是一个求值函数,result1、result2、result3 依次经过一段长运算,随后进行条件判断,最终 return 结果;

该代码的不合理之处在于,每次调用 myFunction() 都要执行 3 个 longCalculation 计算,非常耗时,而实际结果可能只需其中一个运算结果。

针对此问题,优化策略为:需要用到哪个计算,才执行哪个计算。(即惰性求值)

// 示例代码 2

const myFunction = function(a, b, c) {
  let result1 = longCalculation1(a,b);
  if (result1 < 10) {
    return result1;
  } else {
    let result2 = longCalculation2(b,c);
    if (result2 < 100) {
     return result2;
    } else {
      let result3 = longCalculation3(a,c);
      return result3;
    }
  }
}

优化后的写法逻辑更合理,但 if...else... 嵌套结构使得代码可读性下降。

由于 JavaScript 并非惰性求值语言,它与 C 语言等主流语言一样属于【及早求值】。而 Haskell 等纯函数式语言支持惰性求值,用 Haskell 实现上述函数为:

myFunction :: Int -> Int -> Int -> Int
myFunction a b c =
  let result1 = longCalculation1 a b
      result2 = longCalculation2 b c
      result3 = longCalculation3 a c
  in if result1 < 10
       then result1
       else if result2 < 100
         then result2
         else result3

表面上看,这似乎与 JavaScript 示例代码 1 相似,但实际上它实现了 JavaScript 示例代码 2 的效果;

在 GHC 编译器中,result1、result2 和 result3 被存储为 “thunk”,编译器知晓何时需要计算结果,否则不会提前计算。这体现了强大的优化能力。

在《Haskell 函数式编程入门》中,对 thunk 的解释如下:

thunk 意为形实替换程序(有时也称为延迟计算,suspended computation)。它指在计算过程中,某些函数的参数或结果通过一段程序来代表,这段程序被称为 thunk。可以简单地将 thunk 视为一个未完全求值的表达式与获取该结果所需环境变量组成的函数。该表达式与环境变量形成了一个无参数的闭包(parameterless closure),因此 thunk 中包含求得表达式所需的所有信息,仅在不需要时不执行求值。

虽然 JavaScript 语言设计本身不支持惰性求值,但这并不意味着无法利用惰性思想进行编程。

从惰性编程角度思考问题,可以消除代码中不必要的计算,并帮助重构程序,使其更直接地面向业务问题。

惰性编程

什么是惰性编程?

惰性编程是一种将对函数或请求的处理延迟到真正需要结果时进行的通用概念。

许多应用程序都采用了这种概念,有的表现明显,有的则较为隐蔽。

例如 JavaScript 的前身 Scheme 中就包含简单的惰性编程,它有两个特殊结构,delay 和 force。delay 接收一个代码块,不会立即执行,而是将代码和参数作为一个 promise 存储起来。而 force promise 则会运行这段代码,产生一个返回值;

这里提到 promise?在 JS 中也有 Promise,它是 JS 实现惰性的关键吗?

我们不妨通过代码来测试一下:

const st=()=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log("done promise")
            resolve(true)
        },1000)
    })
}
let a = st()
console.log(a)

前文回顾

可以看到,Promise 并不是惰性的,它一旦执行,状态就转为 Pending,无法暂停。我们无法判断 Promise 是刚开始执行、快执行完还是处于其他具体阶段;内部的异步任务一旦启动便无法中途取消。这些问题也是面试中常考的 Promise 的缺点。

幸运的是,后来 Generator 函数的出现,将 JavaScript 异步编程带入了一个全新的阶段。

ES6 引入的 Generator,为 JavaScript 赋予了惰性的能力! 👏

Generator

Thunk

Generator 就像是 Haskell 中的 thunk,赋值时不进行计算,而是将其包装成一个 <suspended> 暂停等待,等你调用 next() 的时候,再进行计算;

function* gen(x){
 const y = yield x + 6;
 return y;
}
const g = gen(1);
g.next() // { value: 7, done: false }
g.next() // { value: undefined, done: true }

调用 Generator 函数后,该函数并不立即执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象。下一步,必须调用遍历器对象的 next 方法,使指针移向下一个状态。

在异步场景下同样适用,将上述 promise 的测试代码改造为:

function * st1(){
    setTimeout(()=>{
        console.log("done promise")
    },1000)
    yield("done promise")
}
let aThunk = st1()
console.log(aThunk)

前文回顾

只有执行 aThunk.next() 时,异步操作才开始执行。

迭代生成器

Promise 不能随用随停,而 Generator 可以。我们通过 Generator 生成的序列值是可以迭代的,迭代过程可被操作,例如在循环中迭代生成器:

//基本的生成器函数产生序列值。
function* gen(){
    yield 'first';
    yield 'second';
    yield 'third';
}
//创建生成器。
var generator = gen();
//循环直到序列结束。
while(true) {
    //获取序列中的下一项。
    let item = generator.next();
    //下一个值等于 'third' 吗
    if(item.value === 'third') {
        break;
    }
    console.log('while', item.value);
}

当 item.value === 'third',break 跳出循环,迭代结束。

循环+请求

综合循环和异步的问题,抛出一个经典的面试题:

如何依次请求一个 api 数组中的接口,需保证一个请求结束后才开始另一个请求?

代码实现如下:

async function* generateSequence(items) {
  for (const i of items) {
    await new Promise(resolve => setTimeout(resolve, i));
    yield i;
  }
}
(async () => {
  let generator = generateSequence(['3000','8000','1000','4000']);
  for await (let value of generator) {
    console.log(value);
  }
})();

这里用 setTimeout 模拟了异步请求,代码可复制到控制台中自行运行测试。

无限序列

在函数式编程语言中有一个特殊的数据结构 —— 无限列表,Generator 也可以帮助 JS 实现这一结构:

🌰比如生成一个无限增长的 id 序列:

function* idMaker(){
    let index = 0;
    while(true)
        yield index++;
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
// 0
console.log(gen.next().value);
// 1
console.log(gen.next().value);
// 2
// ...

🌰比如实现一个循环交替的无限序列:

//一个通用生成器将无限迭代
//提供的参数,产生每个项。
function* alternate(...seq) {
    while (true) {
        for (let item of seq) {
            yield item;
        }
    }
}
//使用新值创建新的生成器实例
//来迭代每个项。
let alternator = alternator('one', 'two', 'three');
//从无限序列中获取前10个项。
for (let i = 0; i < 6; i++) {
    console.log(`"${alternator.next().value}"`);
}
// "one"
// "two"
// "three"
// "one"
// "two"
// "three"

由于 while 循环永远不会退出,for 循环将自我重复。也就是说,参数值会交替出现。

无限序列具有现实意义,许多数字组合都是无限的,例如素数、斐波那契数、奇数等;

结语

看到这里,大家是否感觉 Generator 和之前讲过的某些内容相似?

纯函数的衍生 compose 组合函数,将一个个函数组装、拼接形成链条;Generator 自定义生成序列,依次执行。二者有异曲同工之妙。前者侧重函数封装、后者侧重异步处理,但二者都体现了“延迟处理”的思想。

JavaScript 也能借助 闭包、柯里化、组合函数、Generator 实现惰性编程,减少不必要的计算、精确控制序列的执行、实现无限列表等。。。

JavaScript 确实展现了强大的灵活性与兼容性。

以上就是从延迟处理解析JavaScript惰性编程的详细内容,更多关于JavaScript 延迟处理惰性编程的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章