您的位置:首页 > 科技 > IT业 > 网页模板下载不了_站长统计幸福宝宝官方_手机百度账号登录个人中心_上海广告公司排名

网页模板下载不了_站长统计幸福宝宝官方_手机百度账号登录个人中心_上海广告公司排名

2024/11/19 19:05:17 来源:https://blog.csdn.net/frgod/article/details/142986647  浏览:    关键词:网页模板下载不了_站长统计幸福宝宝官方_手机百度账号登录个人中心_上海广告公司排名
网页模板下载不了_站长统计幸福宝宝官方_手机百度账号登录个人中心_上海广告公司排名

文章目录

  • 4.1 纯函数
    • 4.1.1. 引用透明 Referential transparency
    • 4.1.2. JS 函数中的副作用 Side effects
    • 4.1.3. 纯函数的优势 Advantages of pure functions
  • 4.2. 非纯函数 Impure functions ✔️
    • 4.2.1. 避开非纯函数 Avoiding impure functions
      • 1 避免使用状态 Avoiding the usage of state
      • 2 注入非纯函数 Injecting impure functions
    • 4.2.2. 纯函数的判定 Is your function pure?

(接上篇内容)

4.1 纯函数

4.1.1. 引用透明 Referential transparency

(详见本专栏 【第 012 篇】 )

4.1.2. JS 函数中的副作用 Side effects

(详见本专栏 【第 012 篇】 及 【第 013 篇】)

4.1.3. 纯函数的优势 Advantages of pure functions

(详见本专栏 【第 014 篇】)

4.2. 非纯函数 Impure functions ✔️

如果完全避开各种副作用,那么程序将只能使用硬编码输入,无法显示计算结果;同样,大多数网页都将黯然失色:既无法调用任何 Web 服务,也无法更新 DOM——只有静态页面;而且 Node 代码在服务器端也将毫无意义——因为不执行任何 I/O 操作。

减少副作用是函数式编程一个很好的目标,但过犹不及!接下来,就让我们来考察一下如何避免非纯函数的使用;如果实在避无可避,又该怎样找到最佳方法来圈定或限制其作用范围。

4.2.1. 避开非纯函数 Avoiding impure functions

前面章节介绍了使用非纯函数的一些较常见的原因;本节就来看看,在完全消灭非纯函数不现实的情况下,怎样减少它们在代码中的数量。主要方法有两个:

  • 避免使用状态;
  • 使用一种更通用的注入模式(injection)将非纯函数的行为限制在可控范围内;

1 避免使用状态 Avoiding the usage of state

关于全局状态的使用——获取值也好、设置值也罢,解决方案都是众所周知的。其要点提炼如下:

  • 将全局状态所需的任何内容作为参数传给函数;
  • 如果函数需要更新状态,不宜直接更新,而应该生成一个新状态来作返回值;
  • 应该由调用方接收返回的状态并更新该状态。

这些也是 Redux 中的 reducer 的设计理念。reducer 的签名为 (previousState, action) => newState,表示接收一个状态值 state 和一个 action 作参数,并返回一个新的状态值 newState。更具体地说,reducer 不应该简单地更改 previousState 参数;它必须保持不变(第十章《确保纯度——不变性》将详细介绍)。

最早版本的 isOldEnough 函数用到了一个全局变量 limitYear,改造起来也很简单:将其视为函数参数即可。这样原函数就变成了纯函数,其结果只取决于传入的参数。再进一步,可以将当前年份传入,让函数来处理具体计算,而不是交由调用者亲自计算,代码如下:

const isOldEnough3 = (currentYear, birthYear) => birthYear <= currentYear-18;

很明显,该方案必须更改所有的函数调用,将 limitYear 作为参数传入(也可以使用科里化来解决,第七章《函数转换——科里化与部分应用》会详述)。给 limitYear 赋初值后,和之前一样,其职责依然游离于函数之外,但这样处理已经成功避开了一个陷阱。

不妨在 roundFix 函数上小试牛刀。回忆一下,该函数根据上一次计算的舍入误差的正负,来对累计舍入误差进行修正。虽然无法绕开中间状态,但却可以将当前舍入计算与累计修正计算剥离开。原代码如下:

const roundFix1 = (function() {let accum = 0;return n => {let nRounded = accum > 0 ? Math.ceil(n) : Math.floor(n);accum += n - nRounded;return nRounded;};
})();

新版实现将传入两个参数:

const roundFix2 = (a, n) => {let r = a > 0 ? Math.ceil(n) : Math.floor(n);a += n - r;return {a, r};
};

该函数具体怎么使用呢?其用法是:先初始化累计误差,然后传入该函数,再由调用方完成状态更新:

let accum = 0;// ...some other code...let {a, r} = roundFix2(accum, 3.1415);
accum = a;
console.log(accum, r); // 0.1415 3

注意以下几点:

  • accum 现在是全局状态的一部分;
  • 根据 roundFix2() 的定义,每次调用该函数都会用到当前 accum 的最新的值;
  • 负责更新全局状态的是调用者自己,而不是 roundFix2() 函数本身。

提示

注意第 5 行解构赋值的用法,可以将函数返回一个以上的结果并轻松赋给不同的变量。更多详情,参考 MDN 官方文档。

改造后的新 roundFix2() 函数就是百分百的纯函数了,测试起来也会很轻松。若要对程序其他部分隐藏 accum,可以继续使用闭包,但这样会由于函数自身的调用而再次引入不纯的代码。

2 注入非纯函数 Injecting impure functions

如果一个函数因为调用了另一个不纯的函数而变得不纯,其中一种解题思路,是将所需函数直接注入原函数。该方案提供了更大的代码灵活性,应对后续变动更轻松,进行单元测试也更简单。

再来回顾一下用于生成随机文件名的示例函数。原函数的问题在于,使用了 getRandomLetter() 函数来生成文件名(第 5 行):

const getRandomFileName = (fileExtension = "") => {const NAME_LENGTH = 12;let namePart = new Array(NAME_LENGTH);for (let i = 0; i < NAME_LENGTH; i++) {namePart[i] = getRandomLetter();}return namePart.join("") + fileExtension;
};

解决该问题的一个思路是用一个外部注入的函数来替换非纯函数;此时需要为之前的 getRandomFileName 函数引入一个新的参数 randomLetterFunc

const getRandomFileName2 = (fileExtension = "", randomLetterFunc) => {const NAME_LENGTH = 12;let namePart = new Array(NAME_LENGTH);for (let i = 0; i < NAME_LENGTH; i++) {namePart[i] = randomLetterFunc();}return namePart.join("") + fileExtension;
};

这样就从原函数中移除了原来的非纯函数行为。虽然引入的是一个给定的伪随机函数,但如果返回的一个是固定的、已知的值,那么后续就可以轻松对该函数进行单元测试;后续的示例还将进一步演示具体做法。函数的用法已然改变,此时需要这么写:

let fn = getRandomFileName2(".pdf", getRandomLetter);

如果看上去不太习惯,也可以处理成给 randomLetterFunc 参数提供一个默认值,如下所示(注意第 3 行):

const getRandomFileName2 = (fileExtension = "",randomLetterFunc = getRandomLetter
) => {...
};

当然,这个问题也可以通过部分传参(partial application)来解决,后续第 7 章《函数变换——柯里化及部分传参技术》会进一步介绍。

其实这样做并没有完全避免使用非纯函数。正常情况下,我们还是会通过传入的随机字母生成逻辑来调用 getRandomFileName(),因此改造后的函数本质上仍然是非纯函数。但是,出于测试方面的考虑,如果传入的是一个返回给定值而非随机值的函数,就可以假定它是个纯函数来进行测试,操作起来就会方便很多。

而对于这一切的始作俑者、非纯函数 getRandomLetter() 又该如何处置呢?可以使用同样的技巧,引入一个参数来生成随机数,从而得到下面的函数新版本(留意第 1 行):

const getRandomLetter = (getRandomInt = Math.random) => {const min = "A".charCodeAt();const max = "Z".charCodeAt();return String.fromCharCode(Math.floor(getRandomInt() * (1 + max - min)) + min);
};

对于常规调用,getRandomFileName() 中的 getRandomLetter() 在调用时不带任何参数,也就是说 getRandomLetter() 将按照默认的随机逻辑进行计算;但若要测试该函数是否按预期运行,则可以人为注入一个伪随机函数来进行测试,让该函数返回测试预设的结果,从而充分测试原函数。

这一思路至关重要,在解决其他类似的问题时也有相当广泛的应用。例如,可以提供某个注入函数(injected functions),避免在原函数中直接访问 DOM。这样在后续测试时,要验证被测试函数是否确实完成了它要实现的功能,将会变得非常简单,无需真正与 DOM 进行交互(当然,测试那些与 DOM 相关的注入函数还需要其他的一些方法,但不是此处的重点)。类似的操作也适用于需要更新 DOM、生成新元素以及进行各种 DOM 操作的其他函数——只需引入一些中介函数即可。

4.2.2. 纯函数的判定 Is your function pure?

让我们通过考虑一个重要的问题来结束本小节的学习:您如何确保一个函数确实是纯函数?为演示该问题的难点,不妨再来看看第 1 章中介绍过了一个简单求和的函数 sum3()。简便起见,这里改写为箭头函数的形式。问题来了:该函数是纯函数吗?看起来确实像:

const sum3 = (x, y, z) => x + y + z;

让我们考察考察:这个 sum3 函数除了访问其自身的参数外,不涉及外部任何内容,也没有中途修改参数的迹象(暂且不论能不能改(要是能改,情况又如何?));既不执行任何 I/O 操作,也不与之前提到的任何非纯函数或非纯方法打交道。这么一来,判定为纯函数会有什么问题吗?

问题的关键在于你的前提条件。举个例子,谁说 sum3 的参数就一定是数字?您可能会做出让步:好吧,就当它们也可以是字符串吧;可即便如此,函数也仍然是纯函数啊,不是吗?但是(用这种语气说明肯定不对),如果是下面这种情况,答案还会是纯函数吗:

let x = {};
x.valueOf = Math.random;let y = 1;
let z = 2;console.log(sum3(x, y, z)); // 3.2034400919849431
console.log(sum3(x, y, z)); // 3.8537045249277906
console.log(sum3(x, y, z)); // 3.0833258308458734

高手支招

注意,上述代码将一个新函数赋给了 x.valueOf 方法。这是对“函数是一等对象”这一知识点的灵活应用。更多相关介绍,详见本书第三章第 3.1.3 节《不必要的错误(An unnecessary mistake)》。

由此可见,sum3() 是不是纯函数,其实取决于传给它的参数是什么;在 JavaScript 中,是完全可以让一个纯函数以非纯函数的方式来运行的!您可能会自我安慰说,肯定不会遇到这样传参的情况,但这些边缘情况(edge cases)往往就是 Bug 的藏身之所;但也不必放弃使用纯函数的想法。尽管 JavaScript 无法让您完全确信写出的代码 始终是纯函数形式 的,但至少只要通过添加一些类型检查(比如使用第 1 章介绍转译工具时提过的 TypeScript)来有效防止上述情况的发生。

通过前面几节的介绍,想必您以及了解了纯函数和非纯函数各自的特点。在本章的最后一节,让我们再来看看如何测试所有这些函数。

(本节完)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com