当采取维基百科定义严格来说,将可变数据结构(例如本机数组)的引用作为参数的函数不是纯函数:
对于相同的参数,它的返回值是相同的(没有变化局部静态变量、非局部变量、可变引用参数或来自 I/O 设备的输入流)。
等价
尽管这明确表示“可变引用参数没有变化”,但我们可以说这是可以解释的,并且取决于"same" and “变化”。可能有不同的定义,因此我们进入意见领域。引自您提到的论文:
这些问题没有一个明显正确的答案。因此,确定性是一个参数化的属性:给出参数等价含义的定义,如果所有调用都使用相等的参数返回的结果与语言内部无法区分
The 功能纯度在同一篇论文中提出,使用以下等价定义:
如果两组对象引用产生相同的对象图,则认为它们是等效的
因此,根据该定义,以下两个数组被认为是等效的:
let a = [1];
let b = [1];
但如果不添加更多限制,这个概念就无法真正应用于 JavaScript。也不是 Java,这就是该论文的作者提到一种精简语言,称为 Joe-E 的原因:
对象具有身份:从概念上讲,它们有一个“地址”,我们可以使用以下方法来比较两个对象引用是否指向同一个“地址”:==
操作员。这种对象身份的概念可能会暴露出不确定性。
用 JavaScript 表示:
const compare = (array1, array2) => array1 === array2;
let arr = [1];
let a = compare(arr, arr);
let b = compare(arr, [1]);
console.log(a === b); // false
由于这两个调用返回不同的结果,即使参数具有相同的形状和内容,我们也应该得出结论(根据等价的定义)上述函数compare
不纯粹。在 Java 中,您可以影响==
接线员(Joe-E 禁止呼叫Object.hashCode
),因此要避免这种情况的发生,这在 JavaScript 中比较对象时通常是不可能的。
意想不到的副作用
另一个问题是 JavaScript 不是强类型的,因此函数无法确定它接收到的参数是否是其预期的参数。例如,以下函数看起来很纯粹:
const add = (a, b) => a + b;
但它可以以产生副作用的方式调用:
const add = (a, b) => a + b;
let i = 0;
let obj = { valueOf() { return i++ } };
let a = add(1, obj);
let b = add(1, obj);
console.log(a === b); // false
您问题中的函数也存在同样的问题:
const f = (arr) => arr.length;
const x = { get length() { return Math.random() } };
let a = f(x);
let b = f(x);
console.log(a === b) // false
在这两种情况下,函数都会无意中调用不纯函数并返回依赖于它的结果。虽然在第一个示例中,仍然很容易使用typeof
检查一下,这对于您的功能来说并不那么简单。我们可以想到instanceof
or Array.isArray
,甚至一些聪明的deepCompare
函数,但调用者仍然可以设置一个奇怪的对象的原型,设置其构造函数属性,用 getter 替换原始属性,将对象包装在代理中,...等等,甚至可以愚弄最聪明的相等检查器。
实用主义
正如 JavaScript 中存在太多“未解决的问题”一样,人们必须务实才能对“纯粹”有一个有用的定义,否则几乎没有任何东西可以被标记为纯粹。
例如,在实践中许多人会调用类似的函数Array#slice
纯粹的,尽管它存在上述问题(包括与特殊参数相关的问题)this
).
结论
在 JavaScript 中,当纯粹调用函数时,您通常必须就如何调用函数达成一致。参数应该是某种类型,并且没有可以调用但不纯的(隐藏)方法。
有人可能会说这违背了“纯粹”背后的理念,而“纯粹”应该only由函数定义本身决定,而不是由它最终被调用的方式决定。