通过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的事件描述
Sep 08 Javascript
用javascript连接access数据库的方法
Nov 17 Javascript
js优化针对IE6.0起作用(详细整理)
Dec 25 Javascript
javascript中如何处理引号编码&amp;#034;
Aug 15 Javascript
基于jQuery+JSON的省市二三级联动效果
Jun 05 Javascript
js制作带有遮罩弹出层实现登录注册表单特效代码分享
Sep 05 Javascript
基于JavaScript实现快速转换文本语言(繁体中文和简体中文)
Mar 07 Javascript
浅谈Web页面向后台提交数据的方式和选择
Sep 23 Javascript
详解AngularJs HTTP响应拦截器实现登陆、权限校验
Apr 11 Javascript
jQuery实现可拖动进度条实例代码
Jun 21 jQuery
浅谈vue首屏加载优化
Jun 28 Javascript
jQuery实现手风琴效果(蒙版)
Jan 11 jQuery
关于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获取数组元素中头一个数组元素值的实现方法
2014/12/20 PHP
Laravel 5框架学习之Eloquent (laravel 的ORM)
2015/04/08 PHP
laravel5创建service provider和facade的方法详解
2016/07/26 PHP
event.keyCode键码值表 附只能输入特定的字符串代码
2009/05/15 Javascript
40个有创意的jQuery图片、内容滑动及弹出插件收藏集之一
2011/12/31 Javascript
javascript学习笔记(十) js对象 继承
2012/06/19 Javascript
jQuery动态添加删除select项(实现代码)
2013/09/03 Javascript
Ajax中解析Json的两种方法对比分析
2015/06/25 Javascript
js实现防止被iframe的方法
2015/07/03 Javascript
JavaScript实现图片轮播的方法
2015/07/31 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
2016/05/12 Javascript
js编写一个简单的产品放大效果代码
2016/06/27 Javascript
使用HTML5+Boostrap打造简单的音乐播放器
2016/08/05 Javascript
vue 1.0 结合animate.css定义动画效果
2018/07/11 Javascript
超好用的jQuery分页插件jpaginate用法示例【附源码下载】
2018/12/06 jQuery
JavaScript变速动画函数封装添加任意多个属性
2019/04/03 Javascript
html+vue.js 实现漂亮分页功能可兼容IE
2020/11/07 Javascript
Python跳出循环语句continue与break的区别
2014/08/25 Python
Python实现的递归神经网络简单示例
2017/08/11 Python
Python实现列表删除重复元素的三种常用方法分析
2017/11/24 Python
python3+PyQt5实现自定义流体混合窗口部件
2018/04/24 Python
解决pycharm界面不能显示中文的问题
2018/05/23 Python
Selenium(Python web测试工具)基本用法详解
2018/08/10 Python
Python计算机视觉里的IOU计算实例
2020/01/17 Python
keras tensorflow 实现在python下多进程运行
2020/02/06 Python
利用Python实现某OA系统的自动定位功能
2020/05/27 Python
Python利用Xpath选择器爬取京东网商品信息
2020/06/01 Python
python BeautifulSoup库的安装与使用
2020/12/17 Python
用HTML5实现手机摇一摇的功能的教程
2012/10/30 HTML / CSS
红色康乃馨酒店:Red Carnation Hotels
2017/06/22 全球购物
工程专业求职自荐书范文
2014/02/18 职场文书
银行竞聘上岗演讲稿
2014/09/12 职场文书
帝企鹅日记观后感
2015/06/10 职场文书
Pytorch可视化的几种实现方法
2021/06/10 Python
Android存储中最基本的文件存储方式
2022/04/30 Java/Android
Redis基本数据类型哈希Hash常用操作命令
2022/06/01 Redis