通过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翻页效果
Jul 23 Javascript
jquery easyui的tabs使用时的问题
Mar 23 Javascript
niceTitle 基于jquery的超链接提示插件
May 31 Javascript
JS自动倒计时30秒后按钮才可用(两种场景)
Aug 31 Javascript
JS控制按钮10秒钟后可用的方法
Dec 22 Javascript
jQuery的each循环用法简单示例
Jun 12 Javascript
JavaScript实现图片瀑布流和底部刷新
Jan 02 Javascript
vue实现密码显示隐藏切换功能
Feb 23 Javascript
vue实例中data使用return包裹的方法
Aug 27 Javascript
layui 地区三级联动 form select 渲染的实例
Sep 27 Javascript
JavaScript中layim之整合右键菜单的示例代码
Feb 06 Javascript
react使用antd的上传组件实现文件表单一起提交功能(完整代码)
Jun 29 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 判断变量类型实现代码
2009/10/23 PHP
PHP运行时强制显示出错信息的代码
2011/04/20 PHP
php数组索引与键值操作技巧实例分析
2015/06/24 PHP
CodeIgniter中使用Smarty3基本配置
2015/06/29 PHP
jquery对表单操作2
2011/04/06 Javascript
关于js注册事件的常用方法
2013/04/03 Javascript
jQuery设置指定网页元素宽度和高度的方法
2015/03/25 Javascript
Angular的Bootstrap(引导)和Compiler(编译)机制
2016/06/20 Javascript
jQuery获取同级元素的简单代码
2016/07/09 Javascript
jQuery表单对象属性过滤选择器实例详解
2016/09/13 Javascript
Javascript 实现放大镜效果实例详解
2016/12/03 Javascript
详解JavaScript的闭包、IIFE、apply、函数与对象
2016/12/21 Javascript
jQuery实现圣诞节礼物动画案例解析
2016/12/25 Javascript
用Nodejs搭建服务器访问html、css、JS等静态资源文件
2017/04/28 NodeJs
Ionic3 UI组件之Gallery Modal详解
2017/06/07 Javascript
jQuery Json数据格式排版高亮插件json-viewer.js使用方法详解
2017/06/12 jQuery
Vue组件模板形式实现对象数组数据循环为树形结构(实例代码)
2017/07/31 Javascript
基于VUE实现判断设备是PC还是移动端
2020/07/03 Javascript
[59:35]DOTA2上海特级锦标赛主赛事日 - 3 败者组第三轮#1COL VS Alliance第二局
2016/03/04 DOTA
TensorFlow模型保存/载入的两种方法
2018/03/08 Python
python list格式数据excel导出方法
2018/10/31 Python
python浪漫表白源码
2019/04/05 Python
在django中图片上传的格式校验及大小方法
2019/07/28 Python
新手学习Python2和Python3中print不同的用法
2020/06/09 Python
html5中JavaScript removeChild 删除所有节点
2014/05/16 HTML / CSS
中文系师范生自荐信
2013/10/01 职场文书
教师队伍管理制度
2014/01/14 职场文书
会计专业毕业生求职信
2014/07/04 职场文书
党干部专题民主生活会对照检查材料思想汇报
2014/10/06 职场文书
2014年初中班主任工作总结
2014/11/08 职场文书
2014年内勤工作总结
2014/11/24 职场文书
写给女朋友的检讨书
2015/05/06 职场文书
2016年小学生寒假总结
2015/10/10 职场文书
python 爬取京东指定商品评论并进行情感分析
2021/05/27 Python
python opencv旋转图片的使用方法
2021/06/04 Python
bootstrapv4轮播图去除两侧阴影及线框的方法
2022/02/15 HTML / CSS