✨从延迟处理讲起,JavaScript 也能惰性编程?

简介: 我们从闭包起源开始、再到百变柯里化等一票高阶函数,再讲到纯函数、纯函数的组合以及简化演算;

image.png

前文回顾



# ✨从历史讲起,JavaScript 基因里写着函数式编程

# ✨从柯里化讲起,一网打尽 JavaScript 重要的高阶函数

# ✨从纯函数讲起,一窥最深刻的函子 Monad


我们从闭包起源开始、再到百变柯里化等一票高阶函数,再讲到纯函数、纯函数的组合以及简化演算;


学到了:

  1. 闭包的设计就是因为 lambda 表达式只能接受一个参数的设计导致的,诞生 1930 ;
  2. 柯里化是闭包的孪生子,柯里化思想是高阶函数的重要指导;
  3. 原来编程函数也可以和数学函数一样运算推导,无副作用的纯函数、函数组合,代码更易读;


本篇将展开“延迟处理”这一话题,闲言少叙,冲了~


延迟处理



认真读前面几篇,虽然没有专门讲“延迟处理”,但实际上处处都体现着“延迟处理”。

首先闭包是延迟处理:函数在声明的时候,确定了上下作用域关系。比如以下代码:

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 中就有简单的惰性编程,它有两个特殊的结构,delayforce,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 实现惰性编程,减少不必要的计算、精确控制序列的执行、实现无限列表等。。。

不愧是你,真胶水语言,啥都能干!


后文会重点讲:JS 异步核心、响应式事件流、RxJS等,敬请期待~



相关文章
|
2月前
|
存储 JavaScript 前端开发
JavaScript编程实现tab选项卡切换的效果+1
JavaScript编程实现tab选项卡切换的效果+1
|
3月前
|
JavaScript 前端开发 编译器
解锁JavaScript模块化编程新纪元:从CommonJS的基石到ES Modules的飞跃,探索代码组织的艺术与科学
【8月更文挑战第27天】随着Web应用复杂度的提升,JavaScript模块化编程变得至关重要,它能有效降低代码耦合度并提高项目可维护性及扩展性。从CommonJS到ES Modules,模块化标准经历了显著的发展。CommonJS最初专为服务器端设计,通过`require()`同步加载模块。而ES Modules作为官方标准,支持异步加载,更适合浏览器环境,并且能够进行静态分析以优化性能。这两种标准各有特色,但ES Modules凭借其更广泛的跨平台兼容性和现代语法逐渐成为主流。这一演进不仅标志着JavaScript模块化的成熟,也反映了整个JavaScript生态系统的不断完善。
50 3
|
11天前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包:解锁编程潜能,释放你的创造力
【10月更文挑战第25天】本文深入探讨了JavaScript中的闭包,包括其基本概念、创建方法和实践应用。闭包允许函数访问其定义时的作用域链,常用于数据封装、函数柯里化和模块化编程。文章还提供了闭包的最佳实践,帮助读者更好地理解和使用这一强大特性。
11 2
|
2月前
|
JavaScript 前端开发
JavaScript编程实现tab选项卡切换的效果
JavaScript编程实现tab选项卡切换的效果
|
2月前
|
JavaScript 前端开发
用JavaScript编程控制网页上checkbox选择状态:全选、全部取消、反选
用JavaScript编程控制网页上checkbox选择状态:全选、全部取消、反选
|
2月前
|
JavaScript 前端开发 安全
JavaScript编程实现字符和字符串翻转
JavaScript编程实现字符和字符串翻转
|
2月前
|
JavaScript 前端开发
用JavaScript编程定义二维数组并初始化,然后输出元素值
用JavaScript编程定义二维数组并初始化,然后输出元素值
|
3月前
|
JavaScript 前端开发 安全
揭秘TypeScript的魔力:它是如何华丽变身为JavaScript的超能英雄,让您的代码飞入全新的编程维度!
【8月更文挑战第22天】在Web开发领域,JavaScript是最主流的编程语言之一。但随着应用规模的增长,其类型安全和模块化的不足逐渐显现。为解决这些问题,微软推出了TypeScript,这是JavaScript的一个超集,通过添加静态类型检查来提升开发效率。TypeScript兼容所有JavaScript代码,并引入类型注解功能。
38 2
|
3月前
|
JavaScript 前端开发 Oracle
|
3月前
|
JavaScript 前端开发 开发者
震撼揭秘!JS模块化进化史:从混沌到秩序,一场代码世界的华丽蜕变,你怎能错过这场编程盛宴?
【8月更文挑战第23天】在 Web 前端开发领域,JavaScript 模块化已成为处理日益复杂的 Web 应用程序的关键技术。通过将代码分解成独立且可重用的模块,开发者能够更有效地组织和管理代码,避免命名冲突和依赖混乱。从最早的全局函数模式到 IIFE,再到 CommonJS 和 AMD,最终进化到了 ES6 的原生模块支持以及 UMD 的跨环境兼容性。本文通过具体示例介绍了这些模块化规范的发展历程及其在实际开发中的应用。
48 0
下一篇
无影云桌面