关于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表示负数,只有负数才会取反码和补码。位操作符就是按这个逻辑先将整数转换成二进制数(如果不是整数,则只取整数部分,舍去小数),再进行左移(<<)还是右移(>>)或者加上符号位变动(>>>),然后再将变换后的二进制数按相应规则转换成整数。

综上,>>> 操作符有两个特点:

  1. 结果为非负整数
  2. 结果的范围为 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;
};
posted @ 2020-08-29 17:07  白熊爱料理  阅读(333)  评论(0编辑  收藏  举报