通过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 相关文章推荐
jquery isType() 类型判断代码
Feb 14 Javascript
单击和双击事件的冲突处理示例代码
Apr 03 Javascript
JS是按值传递还是按引用传递
Jan 30 Javascript
jQuery插件实现可输入和自动匹配的下拉框
Oct 24 Javascript
JavaScript实现垂直向上无缝滚动特效代码
Nov 23 Javascript
使用clipboard.js实现复制功能的示例代码
Oct 16 Javascript
关于axios如何全局注册浅析
Jan 14 Javascript
如何使用VuePress搭建一个类型element ui文档
Feb 14 Javascript
详解webpack 最简打包结果分析
Feb 20 Javascript
node.js中 mysql 增删改查操作及async,await处理实例分析
Feb 11 Javascript
js实现查询商品案例
Jul 22 Javascript
vue.js 解决v-model让select默认选中不生效的问题
Jul 28 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
用libTemplate实现静态网页的生成
2006/10/09 PHP
一个程序下载的管理程序(三)
2006/10/09 PHP
PHP实现把MySQL数据库导出为.sql文件实例(仿PHPMyadmin导出功能)
2014/05/10 PHP
php中实现用数组妩媚地生成要执行的sql语句
2015/07/10 PHP
PHP简单预防sql注入的方法
2016/09/27 PHP
微信公众平台开发教程⑤ 微信扫码支付模式介绍
2019/04/10 PHP
JavaScript isArray()函数判断对象类型的种种方法
2010/10/11 Javascript
分享20款好玩的jQuery游戏
2011/04/17 Javascript
Extjs4 GridPanel 的几种样式使用介绍
2013/04/18 Javascript
小结Node.js中非阻塞IO和事件循环
2014/09/18 Javascript
浅谈JavaScript实现面向对象中的类
2014/12/09 Javascript
详解JavaScript中getFullYear()方法的使用
2015/06/10 Javascript
jQuery网页右侧广告跟随滚动代码分享
2020/04/20 Javascript
JS与HTML结合使用marquee标签实现无缝滚动效果代码
2016/07/05 Javascript
jquery层级选择器的实现(匹配后代元素div)
2016/09/05 Javascript
获取jqGrid中选择的行的数据
2016/11/30 Javascript
jquery点赞功能实现代码 点个赞吧!
2020/05/29 jQuery
移动前端图片压缩上传的实例
2017/12/06 Javascript
详解微信小程序自定义组件的实现及数据交互
2019/07/22 Javascript
webpack打包html里面img后src为“[object Module]”问题
2019/12/22 Javascript
微信小程序实现上传多个文件 超过10个
2020/03/30 Javascript
vue中配置scss全局变量的步骤
2020/12/28 Vue.js
Python中set与frozenset方法和区别详解
2016/05/23 Python
python实现树形打印目录结构
2018/03/29 Python
获取django框架orm query执行的sql语句实现方法分析
2019/06/20 Python
Python单链表原理与实现方法详解
2020/02/22 Python
pandas dataframe 中的explode函数用法详解
2020/05/18 Python
python3.7添加dlib模块的方法
2020/07/01 Python
解决运行django程序出错问题 'str'object has no attribute'_meta'
2020/07/15 Python
玩具反斗城美国官网:Toys"R"Us
2016/09/17 全球购物
医学院学生求职简历的自我评价
2013/10/24 职场文书
公司部门司机岗位职责
2014/01/03 职场文书
就业协议书范本
2014/04/11 职场文书
观看《杨善洲》宣传教育片心得体会
2016/01/23 职场文书
2016社区平安家庭事迹材料
2016/02/26 职场文书
MyBatis-Plus 批量插入数据的操作方法
2021/09/25 Java/Android