通过V8源码看一个关于JS数组排序的诡异问题


Posted in Javascript onAugust 14, 2017

前言

前几天一个朋友在微信里面问我一个关于 JS 数组排序的问题。通过该问题发现了一些之前没发现的内容,下面话不多少了,来一起看看详细的介绍吧。

原始数组如下:

var data = [
 {value: 4}, 
 {value: 2}, 
 {value: undefined}, 
 {value: undefined}, 
 {value: 1}, 
 {value: undefined}, 
 {value: undefined}, 
 {value: 7}, 
 {value: undefined}, 
 {value: 4}
];

data 是个数组,数组的每一项都是一个拥有 value 作为 key 的对象,值为数字或者 undefined。

data
 .sort((x, y) => x.value - y.value)
 .map(x => x.value);

对数组的 value 进行排序,然后把排完序的数组进行 flat 处理。得到的结果如下:

[2, 4, undefined, undefined, 1, undefined, undefined, 7, undefined, 4]

显然这没有达到我们的目的。

现在我们修改一下排序,挑战一下函数的调用顺序:先对数组进行扁平化(flat)处理,然后再排序。

data
 .map(x => x.value)
 .sort((x, y) => x - y)

这时我们得到的结果和之前截然不同:

[1, 2, 4, 4, 7, undefined, undefined, undefined, undefined, undefined]

遇到这种情况第一感觉肯定是要去看看 ECMA 规范,万一是 JS 引擎的 bug 呢。

在 ES6 规范 22.1.3.24 节写道:

Calling comparefn(a,b) always returns the same value v when given a specific pair of values a and b as its two arguments. Furthermore, Type(v) is Number, and v is not NaN. Note that this implies that exactly one of a < b, a = b, and a > b will be true for a given pair of a and b.

简单翻译一下就是:第二个参数 comparefn 返回一个数字,并且不是 NaN。一个注意事项是,对于参与比较的两个数 a 小于 b、a 等于 b、a 大于 b 这三种情况必须有一个为 true。

所以严格意义上来说,这段代码是有 bug 的,因为比较的结果出现了 NaN。

在 MDN 文档上还有一个细节:

如果 comparefn(a, b) 等于 0, a 和 b 的相对位置不变。备注:ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守。

翻译成编程术语就是:sort 排序算法是不稳定排序。

其实我们最疑惑的问题上,上面两行代码为什么会输出不同的结果。我们只能通过查看 V8 源码去找答案了。

V8 对数组排序是这样进行的:

如果没有定义 comparefn 参数,则生成一个(高能预警,有坑啊):

comparefn = function (x, y) {
 if (x === y) return 0;
 if (%_IsSmi(x) && %_IsSmi(y)) {
 return %SmiLexicographicCompare(x, y);
 }
 x = TO_STRING(x); // <----- 坑
 y = TO_STRING(y); // <----- 坑
 if (x == y) return 0;
 else return x < y ? -1 : 1;
};

然后定义了一个插入排序算法:

function InsertionSort(a, from, to) {
 for (var i = from + 1; i < to; i++) {
 var element = a[i];
 for (var j = i - 1; j >= from; j--) {
  var tmp = a[j];
  var order = comparefn(tmp, element);
  if (order > 0) { // <---- 注意这里
  a[j + 1] = tmp;
  } else {
  break;
  }
 }
 a[j + 1] = element;
}

为什么是插入排序?V8 为了性能考虑,当数组元素个数少于 10 个时,使用插入排序;大于 10 个时使用快速排序。

后面还定义了快速排序函数和其它几个函数,我就不一一列出了。

函数都定义完成后,开始正式的排序操作:

// %RemoveArrayHoles returns -1 if fast removal is not supported.
var num_non_undefined = %RemoveArrayHoles(array, length);

if (num_non_undefined == -1) {
 // There were indexed accessors in the array.
 // Move array holes and undefineds to the end using a Javascript function
 // that is safe in the presence of accessors.
 num_non_undefined = SafeRemoveArrayHoles(array);
}

中间的注释:Move array holes and undefineds to the end using a Javascript function。排序之前会把数组里面的 undefined 移动到最后。因此第二个排序算法会把 undefined 移动到最后,然后对剩余的数据 [4,2,1,7,4] 进行排序。

而在第一种写法时,数组的每一项都是一个 Object,然后最 Object 调用 x.value - y.value 进行计算,当 undefined 参与运算时比较的结果是 NaN。

当返回 NaN 时 V8 怎么处理的呢?我前面标注过,再贴一次:

var order = comparefn(tmp, element);
if (order > 0) { // <---- 这里
 a[j + 1] = tmp;
} else {
 break;
}

NaN > 0 为 false,执行了 else 分支代码。

思考题,以下代码的结果:

[1, 23, 2, 3].sort()

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
flexigrid 类似ext grid的JS表格代码
Jul 17 Javascript
js弹出层之1:JQuery.Boxy (二)
Oct 06 Javascript
JavaScript中this关键词的使用技巧、工作原理以及注意事项
May 20 Javascript
通过js获取上传的图片信息(临时保存路径,名称,大小)然后通过ajax传递给后端的方法
Oct 01 Javascript
jsonp跨域请求数据实现手机号码查询实例分析
Dec 12 Javascript
jquery实现全选、不选、反选的两种方法
Sep 06 Javascript
Jquery Easyui搜索框组件SearchBox使用详解(19)
Dec 17 Javascript
jQuery实现输入框的放大和缩小功能示例
Jul 21 jQuery
js实现简单掷骰子效果
Oct 24 Javascript
vue 页面回退mounted函数不执行的解决方案
Jul 26 Javascript
Vue实现input宽度随文字长度自适应操作
Jul 29 Javascript
openLayer4实现动态改变标注图标
Aug 17 Javascript
关于Vue Webpack2单元测试示例详解
Aug 14 #Javascript
一篇文章让你彻底弄懂JS的事件冒泡和事件捕获
Aug 14 #Javascript
Vue.js如何实现路由懒加载浅析
Aug 14 #Javascript
JavaScript中的return布尔值的用法和原理解析
Aug 14 #Javascript
一个Js文件函数中调用另一个Js文件函数的方法演示
Aug 14 #Javascript
利用纯JS实现像素逐渐显示的方法示例
Aug 14 #Javascript
jQuery 实时保存页面动态添加的数据的示例
Aug 14 #jQuery
You might like
php录入页面中动态从数据库中提取数据的实现
2006/10/09 PHP
Yii框架布局文件的动态切换操作示例
2019/11/11 PHP
javascript判断单选框或复选框是否选中方法集锦
2007/04/04 Javascript
jQuery编写widget的一些技巧分享
2010/10/28 Javascript
关于window.pageYOffset和document.documentElement.scrollTop
2011/04/05 Javascript
javascript中将Object转换为String函数代码 (json str)
2012/04/29 Javascript
textarea 控制输入字符字节数(示例代码)
2013/12/27 Javascript
Nodejs学习笔记之NET模块
2015/01/13 NodeJs
详解JavaScript对象序列化
2016/01/19 Javascript
微信小程序  TLS 版本必须大于等于1.2问题解决
2017/02/22 Javascript
vue + socket.io实现一个简易聊天室示例代码
2017/03/06 Javascript
微信小程序获取微信运动步数的实例代码
2017/07/20 Javascript
React 无状态组件(Stateless Component) 与高阶组件
2018/08/14 Javascript
解决Vue2.0中使用less给元素添加背景图片出现的问题
2018/09/03 Javascript
11个Javascript小技巧帮你提升代码质量(小结)
2020/12/28 Javascript
[44:43]完美世界DOTA2联赛决赛日 FTD vs GXR 第一场 11.08
2020/11/11 DOTA
进一步探究Python中的正则表达式
2015/04/28 Python
Python3多进程 multiprocessing 模块实例详解
2018/06/11 Python
对Python3中bytes和HexStr之间的转换详解
2018/12/04 Python
Python mutiprocessing多线程池pool操作示例
2019/01/30 Python
python3编写ThinkPHP命令执行Getshell的方法
2019/02/26 Python
Django在pycharm下修改默认启动端口的方法
2019/07/26 Python
Python中文分词库jieba,pkusegwg性能准确度比较
2020/02/11 Python
哪些是python中web开发框架
2020/06/17 Python
Python基于callable函数检测对象是否可被调用
2020/10/16 Python
Html5 FileReader实现即时上传图片功能实例代码
2014/09/01 HTML / CSS
Speedo速比涛中国官方网站:全球领先泳装运动品牌
2018/04/24 全球购物
畜牧兽医本科生个人的自我评价
2013/10/11 职场文书
银行工作检查书范文
2014/01/31 职场文书
乡村文明行动实施方案
2014/03/29 职场文书
优秀家长自荐材料
2014/08/26 职场文书
副总经理党的群众路线教育实践活动个人对照检查材料思想汇报
2014/10/06 职场文书
科技活动周标语
2014/10/08 职场文书
毕业生个人总结
2015/02/28 职场文书
优秀教师工作总结2015
2015/07/22 职场文书
2019大学毕业晚会主持词
2019/06/21 职场文书