关于Array.prototype.map 的 polyfill 函数中使用>>>的疑问,以及改进方法?
Polyfill
在 MDN 网站上关于数组的 map 方法在低版本浏览器上使用一个垫片函数,地址:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map
这个垫片函数的实现如下:
if (!Array.prototype.map) {
Array.prototype.map = function (callback) {
var T, A, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = arguments[1];
}
A = new Array(len);
k = 0;
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
mappedValue = callback.call(T, kValue, k, O);
A[k] = mappedValue;
}
k++;
}
return A;
};
};
在上述函数中对调用对象进行了无符号的左移操作,也就是:
O.length >>> 0
Problem
那这么做的意义是什么呢?解决了什么问题?会不会带来什么问题?
先说我对于此的理解:左移0个操作数位并不是没有意义的。首先,这保证数组的 length 是一个非负整数,其次保证了 length 对新数组是可用的。至于为什么,我会在接下来的篇幅中阐述。其二是会不会有问题?我认为这么做是不好的,是有问题的,最起码在此 polyfill 中是不完备的。
Why
>>>
这个操作符是无符号右移位操作符,MDN上有详细的介绍:
[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Unsigned_right_shift]。
但是要注意它并不是把一个数实际在内存中存储二进制码进行操作,而是以计算机转换整数的二进制码规则转换后进行操作。什么意思呢?我们知道,JavaScript 是动态类型语言,和 Java 、C 等静态类型语言不同,他把整数和浮点数都按 Number 类型处理,遵循 IEEE 754 国际标准,实际上就是将所有的数都按双精度浮点数的规则进行存储,简书上的这篇文章算是讲的简单易懂了:[https://www.jianshu.com/p/ab2bc4d7e001]。>>>
并不是操作这种64位的二进制数,而是32位的二进制整数。《计算机原理》书上说的很清楚,计算机方便运算,将整数按源码-->反码-->补码的方式转换成二进制进行计算,在32位二进制中最高位为符号位,0表示正数,1表示负数,只有负数才会取反码和补码。位操作符就是按这个逻辑先将整数转换成二进制数(如果不是整数,则只取整数部分,舍去小数),再进行左移(<<)还是右移(>>)或者加上符号位变动(>>>),然后再将变换后的二进制数按相应规则转换成整数。
综上,>>>
操作符有两个特点:
- 结果为非负整数
- 结果的范围为 0 ~ 20+21+22...+231,也就是 0 ~ 4294967295
其实这个范围正好也是数组的 length 属性的取值范围,虽然在 JS 中数组的长度是动态增加的,但也并不是没有上线,如果length > 4294967295
,数组的索引就不会再增加了,当然数组还可以添加属性,但是不能在索引(index)上添加元素。你可以做如下操作试一下:
var arr = new Array(4294967295);
arr.push(1); //Uncaught RangeError: Invalid array length
当然,直到这里是不是认为 O.length >>> 0
多此一举?答案是否定的。因为 JS 中伪数组的存在。
比方说,你定义一个对象:
var obj = {
0:'a',
1:'b',
length:2
}
你可以通过这种方法将其转换成数组:
Array.prototype.map.call(obj,x=>x);
现在,不管那些数组或者伪数组的定义,设置 length 为无意义的值:
obj.length = -2;
或者;
obj.length = 4294967296;
那么:
var len = O.length >>> 0;
至少可以保证 len 为一个可用的值。
如果你将 obj.length
改为一个非 Number 类型的值,比如:
obj.length = '10';
var len = obj.length;
以此创建一个新数组,长度为10,得到的并不是理想的结果:
var A = new Array(len);
//output: A:["10"]{0:"10",length:1}
而位移操作符始终得到的都是一个整数,不管对谁操作。
虽然一个操作符解决了所有问题,少写了好几行代码,但是我认为这样处理并不好。
还是用上面的特殊情况的例子:
obj.length = -2 >>>0;
//output:4294967294
那么在 map 函数中循环就有42亿次以上,我在 node 环境下运行了15分钟,其二:
obj.length = 4294967297 >>> 0;
//output:1
那么我将 obj 转换成数组只有 key 为 0 的属性会存入数组中,其他的值就会丢失。其三:
obj.length = "a" >>>0;
//output:0
我在编码中误把 length 值的类型变成了字符串类型,那么我在数组转换时,得到的是一个空数组,而且我得不到程序的反馈,错误在哪里。
注:之前没有想到的,位操作符这里应该也会做隐式转换。在位操作之前应该会把被操作的对象转换成数字
'10'>>>0; //output:10 '1e2'>>>0; //output:100
Resolution
既然是“Invalid array length”,一种解决方式是把他们都抛出 Uncaught RangeError;
var O = Object(this);
var len = O.length;
if(typeof(len)!=='number'||len<0||len>2**32-1){
throw RangeError("Invalid array length");
}
但是这样做不精细,我在浏览器中试了一下 Array 中的 map,当 length 不能转换成 Number 或者小于0,则会返回空数组,超过数组最大长度会抛出错误:
var len = Number(O.length) && Number(O.length) > 0 ? Number(O.length) : 0;
这样就和原生的 map 函数输出结果是一样的了,完整代码如下:
Array.prototype.myMap = function (callback) {
var T, A, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(this);
var len = Number(O.length) && Number(O.length) > 0 ? Number(O.length) : 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = arguments[1];
}
A = new Array(len);
k = 0;
while (k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[k];
mappedValue = callback.call(T, kValue, k, O);
A[k] = mappedValue;
}
k++;
}
return A;
};