通过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 相关文章推荐
JS 实现完美include载入实现代码
Aug 05 Javascript
高性能JavaScript 重排与重绘(2)
Aug 11 Javascript
使用jQuery中的wrap()函数操作HTML元素的教程
May 24 Javascript
easyUI实现(alert)提示框自动关闭的实例代码
Nov 07 Javascript
bootstrap插件treeview实现全选父节点下所有子节点和反选功能
Jul 21 Javascript
requireJS模块化实现返回顶部功能的方法详解
Oct 16 Javascript
JavaScript模拟实现封装的三种方式及写法区别
Oct 27 Javascript
vue将对象新增的属性添加到检测序列的方法
Feb 24 Javascript
Vue进度条progressbar组件功能
Apr 17 Javascript
jQuery实现动态添加和删除input框代码实例
Mar 29 jQuery
vue3.0生命周期的示例代码
Sep 24 Javascript
Vue.js桌面端自定义滚动条组件之美化滚动条VScroll
Dec 01 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
从零开始学YII2框架(五)快速生成代码工具 Gii 的使用
2014/08/20 PHP
PHP抽奖算法程序代码分享
2015/10/08 PHP
如何使用php等比例缩放图片
2016/10/12 PHP
php连接微软MSSQL(sql server)完全攻略
2016/11/27 PHP
安装PHP扩展时解压官方 tgz 文件后没有configure文件无法进行配置编译的问题
2020/08/26 PHP
flash 得到自身url参数的代码
2009/11/15 Javascript
客户端限制只能上传jpg格式图片的js代码
2010/12/09 Javascript
JS 添加网页桌面快捷方式的代码详细整理
2012/12/27 Javascript
JQuery.Ajax之错误调试帮助信息介绍
2013/07/04 Javascript
JavaScript实现控制打开文件另存为对话框的方法
2015/04/17 Javascript
jQuery实现带水平滑杆的焦点图动画插件
2016/03/08 Javascript
javascript replace()第二个参数为函数时的参数用法
2016/12/26 Javascript
Vue.js使用$.ajax和vue-resource实现OAuth的注册、登录、注销和API调用
2017/05/10 Javascript
浅谈node中的exports与module.exports的关系
2017/08/01 Javascript
vue中配置mint-ui报css错误问题的解决方法
2017/10/11 Javascript
面包屑导航详解
2017/12/07 Javascript
使用express+multer实现node中的图片上传功能
2018/02/02 Javascript
vue 过滤器filter实例详解
2018/03/14 Javascript
js闭包学习心得总结
2018/04/17 Javascript
node和vue实现商城用户地址模块
2018/12/05 Javascript
微信小程序下拉框组件使用方法详解
2018/12/28 Javascript
JS回调函数原理与用法详解【附PHP回调函数】
2019/07/20 Javascript
vue2.0 获取从http接口中获取数据,组件开发,路由配置方式
2019/11/04 Javascript
原生js实现下拉选项卡
2019/11/27 Javascript
jQuery实现的图片点击放大缩小功能案例
2020/01/02 jQuery
Python将xml和xsl转换为html的方法
2015/03/10 Python
python实现指定字符串补全空格、前面填充0的方法
2018/11/16 Python
Python 装饰器原理、定义与用法详解
2019/12/07 Python
python读写Excel表格的实例代码(简单实用)
2019/12/19 Python
OpenCV中VideoCapture类的使用详解
2020/02/14 Python
css3弹性盒模型实例介绍
2013/05/27 HTML / CSS
周仰杰(JIMMY CHOO)英国官方网站:闻名世界的鞋子品牌
2018/10/28 全球购物
请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1
2015/07/16 面试题
2014年党的群众路线学习心得体会
2014/11/05 职场文书
培训心得体会怎么写
2016/01/25 职场文书
golang 实现菜单树的生成方式
2021/04/28 Golang