通过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 相关文章推荐
javascript 动态添加事件代码
Nov 30 Javascript
jQuery源码分析-03构造jQuery对象-工具函数
Nov 14 Javascript
jquery入门—选择器实现隔行变色实例代码
Jan 04 Javascript
jquery入门—编写一个导航条(可伸缩)
Jan 07 Javascript
jquery ajax提交整个表单元素的快捷办法
Mar 27 Javascript
jQuery中get和post方法传值测试及注意事项
Aug 08 Javascript
JavaScript中数组继承的简单示例
Jul 29 Javascript
javascript瀑布流式图片懒加载实例
Jun 28 Javascript
Bootstrap Paginator分页插件与ajax相结合实现动态无刷新分页效果
May 27 Javascript
JS转换HTML转义符的方法
Aug 24 Javascript
jQuery 函数实例分析【函数声明、函数表达式、匿名函数等】
May 19 jQuery
Vue vee-validate插件的简单使用
Jun 22 Vue.js
关于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
2020年4月放送!《Princess Connect Re:Dive》制作组 & 角色声优公开!
2020/03/06 日漫
PHP中将字符串转化为整数(int) intval() printf() 性能测试
2020/03/20 PHP
php动态变量定义及使用
2015/06/10 PHP
完美的php分页类
2017/10/24 PHP
Laravel 实现关系模型取出需要的字段
2019/10/10 PHP
Laravel解决nesting level错误和隐藏index.php的问题
2019/10/12 PHP
javascript 写类方式之十
2009/07/05 Javascript
YUI的Tab切换实现代码
2010/04/11 Javascript
js实现的切换面板实例代码
2013/06/17 Javascript
sea.js常用的api简易文档
2016/11/15 Javascript
jQuery实现导航栏头部菜单项点击后变换颜色的方法
2017/07/19 jQuery
JavaScript中递归实现的方法及其区别
2017/09/12 Javascript
深入浅析AngularJs模版与v-bind
2018/07/06 Javascript
Vue Router history模式的配置方法及其原理
2019/05/30 Javascript
vue实现移动端图片上传功能
2019/12/23 Javascript
JS原型对象操作实例分析
2020/06/06 Javascript
[38:51]2014 DOTA2国际邀请赛中国区预选赛 Orenda VS LGD-CDEC
2014/05/22 DOTA
粗略分析Python中的内存泄漏
2015/04/23 Python
python使用socket进行简单网络连接的方法
2015/04/29 Python
关于Python面向对象编程的知识点总结
2017/02/14 Python
Python针对给定列表中元素进行翻转操作的方法分析
2018/04/27 Python
浅谈django url请求与数据库连接池的共享问题
2019/08/29 Python
python打印n位数“水仙花数”(实例代码)
2019/12/25 Python
在终端启动Python时报错的解决方案
2020/11/20 Python
CSS3 display知识详解
2015/11/25 HTML / CSS
美国瑜伽品牌:Gaiam
2017/10/31 全球购物
澳大利亚有机化妆品网上商店:The Well Store
2020/02/20 全球购物
历史系毕业生自荐信
2013/10/28 职场文书
高中毕业生生活的自我评价
2013/12/08 职场文书
电脑租赁公司创业计划书
2014/01/08 职场文书
培训楼经理岗位责任制
2014/02/10 职场文书
大学活动总结范文
2014/04/29 职场文书
没有孩子的离婚协议书怎么写
2014/09/17 职场文书
小学班主任经验交流材料
2014/12/16 职场文书
Python爬取科目四考试题库的方法实现
2021/03/30 Python
Mongodb 迁移数据块的流程介绍分析
2022/04/18 MongoDB