时间:2020/08/14
背景
同上一篇解读v8对JSArray的实现的文章一样,本篇内容也是基于一个小知识点的困惑而翻阅源码得到的理解。
之前在学习Monad与FP的时候对一些概念不甚了解,后来查阅资料发现FP(functional programming,函数式编程)中Monad的概念居然可以解释我上面提到的困惑(下文论述),受到启发,写了这篇内容。
本文实质上是由js中的“同值相等”概念,追溯到论证v8底层源码对该理论的逻辑实现。通过对v8单元测试用例断言函数的解析,串联起部分函数式编程中的概念(单位元)对该函数方法的解释与支持。同时也通过这些实际使用场景,将函数式编程甚至是本文中出现的部分抽象概念具象化。
js中的相等性判断
前置知识点,回顾js中对于相等性和一些特殊值的判定方法:
- js中的三种相等性判定
- 非严格相等(==)
将会对操作数执行类型转换。
- 严格相等(===)
不会对操作数执行类型转换,只对值作比较。
- Object.is()(es6新特性)
同三等运算符,但是对+0和-0、NaN值将会做特殊处理(下文论述)。我们再来回顾一下js中对NaN值的判断是如何做的。
2. js中NaN值判断方法
-
- window.isNaN(x: any): Boolean
- Number.isNaN(x: Number): Boolean
- 利用NaN是js中唯一不等于自身的值(NaN !== NaN为true)
- Object.is(),同值相等
MDN对Object.is()的解释是这样的:
在es6中,Oject.is()将有以下两种特殊场景:
- Object.is(+0, -0) // false
- Object.is(NaN, NaN) // true
在js中其实存在两种0值,+0和-0,它们将分别在0与正、负数值作乘除运算时产生(这在/object-is.js的测试用例中可以证明),区别在于符号不同,代表的意义(功能)不同。虽然在加减运算中没有区别,但一些特殊运算将会产生不同的结果,比如:
1/0 // Infinity
-1/0 //-Infinity
1/0 === -1/0 // false,即Infinity不等于-Infinity
0在运算中作为除数出现似乎有悖于我们的日常认知,而且在几乎所有语言中(golang/c/java/php/python等)这样处理都会抛出异常,但js不会。
golang中0作为除数抛出异常
所以同值相等的设计解决了一个问题:确定两个值是否在任何情况下功能上是相同的。
这个概念抽象吗?
我们可以借助数学方法中极限的定义来理解这个逻辑。对于反比例函数f(x)= k/x(k为常数,k≠0),当x = 0时无意义,但当k>0,x无限趋近于+0时,f(x)无限趋近于+∞,即Infinity,所以这里1/0输出结果为Infinity,同理可知-1/0为-Infinity。
事实上,Infinity这个值也是无意义的,是空值,这体现在函数式编程的幺元概念中。
因此es6同值相等的判定方法认为+0、-0不相等(-0 < +0),这从v8对Object.is()方法的测试用例中可以论证。
./test/object-is.js中的测试用例
断言函数assertSame对传入的两个参数值(expect, found)是否相等是这样判断的:
mjsunit.js中的assertSame断言函数
这里js巧妙借助了上面提到的符号互为相反的0值倒数不相等来区分+0与-0,用自身!==自身来区分NaN,解释了MDN中对Object.is()与严格相等运算符定义的区别。
在v8最新的代码中,assertSame()的上述核心判断逻辑已经改为了直接使用Object.is()方法来替代if条件语句,作者在注释中也直接说明了这一原因。这样一来,assertSame函数就直接与Object.is方法划上了等号。
写到这里,我们可能已经忘记开篇提到过的那个令人困惑的知识点了。
由Math静态方法默认值带来的思考
Math.min()
Math.max()
对于math的这两个静态方法,当无参传递时,有Math.min() > Math.max()成立。
如果第一次接触这个结论,你可能一时无法理解,Math.min()返回一个参数集合中的最小值,但事实上,当无参传递时其返回值是Infinity,同理Math.max()返回-Infinity。
这是js语言设计的一个怪异的地方,也可能是一个缺陷。要想论证其正确性首先依然可以从v8源码的单测用例入手。
在上一节中我们已经接触了一部分用测试用例论证的逻辑与结果。同样,这些用例也能够从结果上论证Math.min()返回Infinity的正确性。v8底层60%由C++编写,不过对于常用js-api的单元测试使用的是js,对于阅读来说没有什么障碍。
此处关注两个断言方法(mjsunit.js中为不同的api设计定义了不同的断言函数)
assertSame(expect, found)
assertEquals(expect, found)
理解这两个函数的区别可以从理解Same与Identical这两个单词含义的差异入手: 1. same: (adj.)相同的,同一的 2. identical: (adj./n.)同一的,完全相同的;同一性 感兴趣的话可以去找适用这两个断言函数的API,通过看源码去理解其中的差异。
从math-min-max.js的测试用例可以看到,Math.min()无参传递时返回了Infinity,而Math.max()返回了Number的NEGATIVE_INFINITY,也就是-Infinity。
assertEquals断言使用deepEquals方法作为其条件判断的核心。总的来说,assertSame与assertEquals底层的判断逻辑大同小异,只不过其适用的对象(js-api)不同。
但是,测试用例只从结果层面给出了答案,该如何从逻辑层面解释其合理性呢?
Monoid与单位元
上一篇的分享已经提到过函数式编程中的Monoid(幺半群)与单位元,,这里不再作详细展开,推荐一篇github上写的不错的blog《函数式编程(三)》作参考。
集合论与幺元的概念从逻辑上论证了Math.min()返回Infinity的正确性
半群(Semigroup)
定义一:对于非空集合 S,若在 S 上定义了二元运算 ○,使得对于任意的 a, b ∈ S,有 a ○ b ∈ S,则称 {S, ○} 为广群。若该广群的二元运算○满足结合律,则该广群称为半群。 * 幺半群(Monoid) 定义:存在单位元(幺元)的半群称为幺半群。 * 单位元(Identity Element,又叫幺元) 定义:对于半群 ,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a。
用最通俗的表述来理解,就是:对于运算集合S上的二元运算○,如果满足 a○x = x○a,则a就是该二元运算的幺元。比如:
1. 加法运算的幺元是0
add(0+x) = add(x+0) = x
2. 乘法运算的幺元是1
multi(1*x) = multi(x*1) = x
3. &&运算的幺元是true;
4. ||运算的幺元是false;
5. 数组合并操作concat的幺元是空数组
concat([], [1,2,3]) = concat([1,2,3], []) = [1,2,3]
concat('', 'chara') = concat('chara', '') = 'chara'
6. ......
因此,对于min与max运算,有下式成立:
min(x, Infinity) = min(Infinity, x) = x
max(x, -Infinity) = max(-Infinity, x) = x
看到这里,是不是感觉幺元就是运算的一种默认值?答案是肯定的。在不传参时,Infinity是min运算的默认参数,所以min() = min(Infinity) = Infinity,也就是说Infinity是min运算的幺元。同理可知-Infinity是max()的幺元。 这样的话,一切就说得通了。
思考一个问题
那么假设一个函数getMax()可以求一个数字中的最大值,该如何为maxVal赋初值?
function getMax(arr) {
let maxVal = ____; // ???
arr.forEach(item => {
if (item > maxVal) {
maxVal = item;
}
})
return maxVal;
}