还没查阅过 ECMAScript 规范?那就让我们一起来开始吧!

1,971 阅读7分钟

作为前端搬砖中精通 Copy & Paste 的一员,如果你还没有查阅过 ECMA262 的规范文档,那么建议后续遇到有些问题的时候,可以试着往这方面靠,如果刚好可以查阅一下,那么往往能受益匪浅。

ECMAScript 是规范,JavaScript 是规范的实现。当我们有遇到 JavaScript 的一些疑问的时候,如果刚好能查阅 ECMAScript 规范来解决,那么说不定会更加豁然开朗。

举个例子,如下三种实现的性能比较如何?将数组第一个元素删除,然后再尾部增加元素。

var arr = [1,2,3];
// 第一种
arr.shift();
arr.push(4);

// 第二种
arr = arr.slice(1);
arr.push(4);

// 第三种
arr.splice(0, 1);
arr.push(4);

我这里就直接贴出测试结果,使用 benchmark 测试,数据大致如下:

  • shift#test x 207 ops/sec ±6.88% (78 runs sampled)
  • slice#test x 114 ops/sec ±1.89% (71 runs sampled)
  • splice#test x 80.90 ops/sec ±0.71% (67 runs sampled)

显然,splice 方法是最慢的!

如果我们想去知道为啥 splice 会是最慢的,和 shift ,slice 到底有什么区别!那么在这个时候,ECMA262 规范文档(https://tc39.es/ecma262/),就非常有用了。

我们就以 splice 来一起阅读以下规范文档。

我们先看下 splice 的基本用法:

var arr = ["a","b","c","d"];
var arr2 = arr.splice(1, 2 , "e");
console.log(arr);// ["a","e","d"]
console.log(arr2);// ["b", "c"]

从数组 arr 的索引为 1 的元素开始,依次删除 2 个元素,然后将 "e" 这个元素插入到索引为 1 的位置。

splice 的实现主要有三个步骤:

  1. 找出需要删除的元素
    找到 b , c 元素并保存起来
  2. 移动数组的位置
    将 d 元素移动到索引位置为 2 的位置(即 c )的位置
  3. 插入元素
    将 e 元素插入到索引为 1 的位置

好了,了解如上三个步骤之后,我们就正式开始阅读 ecma262 规范中的 splice 方法的实现!

文档地址:tc39.es/ecma262/#se…

22.1.3.28 Array.prototype.splice ( start, deleteCount, ...items )

NOTE 1 When the splice method is called with two or more arguments start, deleteCount and zero or more items, the deleteCount elements of the array starting at integer index start are replaced by the elements of items. An Array object containing the deleted elements (if any) is returned.

The following steps are taken:

1. Let O be ? ToObject(this value).  
    将数组转换为对象 O

2. Let len be ? LengthOfArrayLike(O).   
    获取这个对象 O 的长度,也就是数组的长度

3. Let relativeStart be ? ToInteger(start).   
    将第一个参数 start 强制转换为 int 类型,然后复制给 relativeStart

4. If relativeStart < 0, let actualStart be max((len + relativeStart), 0); else let actualStart be min
(relativeStart, len).   
    如果 relativeStart 小于 0 ,则设置实际的开始索引 actualStart 为 len + relativeStart(如果 actualStart 最终小于0 则设置为 0); 

    如果 relativeStart 大于等于 0,则设置 actualStart = relativeStart(如果 relativeStart 大于数组长度,则取数组长度)

5. If start is not present, then   
    如果 传参 start(参数第一个值) 未设置,则   

    a. Let insertCount be 0.   
        设置插入数量为 0 
    b. Let actualDeleteCount be 0.   
        设置实际删除数量为 0

6. Else if deleteCount is not present, then   
    如果参数的删除数量(函数第二个值) 没有设置,则   

    a. Let insertCount be 0.   
        设置插入数量为 0
    b. Let actualDeleteCount be len - actualStart.   
        设置实际删除数量为实际起始位置到最后

7. Else,   
    start 和 deleteCount 都设置了,则     

    a. Let insertCount be the number of elements in items.   
        设置 insertCount 为当前的数量     

    b. Let dc be ? ToInteger(deleteCount).     
        设置 dc 变量为 Integer 对象的 删除值    

    c. Let actualDeleteCount be min(max(dc, 0), len - actualStart).   
        实际删除的数如果大于其实之后所存在的数,则取实际的数量    

8. If len + insertCount - actualDeleteCount > 2<sub>53</sub> - 1, throw a TypeError exception.    
    如果计算最终数组的长度大于 2 的 53 次方 -1,则抛出异常     

9. Let A be ? ArraySpeciesCreate(O, actualDeleteCount).    
    创建一个新数组,长度就是需要删除的项目的长度    

10. Let k be 0.    
    设置变量 k 为 0 

11. Repeat, while k < actualDeleteCount,  
    循环 ,从 k = 0 到 实际需要删除的长度 actualDeleteCount   

    a. Let from be ! ToString(actualStart + k).   
        设置变量 from 为 需要处理的索引值    

    b. Let fromPresent be ? HasProperty(O, from).  
        判断 O 是否有 from 这个属性     

    c. If fromPresent is true, then   
        如果有    

        i. Let fromValue be ? Get(O, from).    
            就从 O 中读取 from 的值给变量 fromValue    

        ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(k), fromValue).   
            将 fromValue 值设置到 A 数组    

    d. Set k to k + 1.    
        k 自增,继续循环    

12. Perform ? Set(A, "length", actualDeleteCount, true).   
    设置 A 数组的 length 属性为需要删除的数量 actualDeleteCount   

13. Let itemCount be the number of elements in items.   
    设置变量 itemCount 需要插入的数组的长度    

14. If itemCount < actualDeleteCount, then   
    如果插入的长度小于需要删除的长度(则最终长度是变小的),那么    

    a. Set k to actualStart.   
        设置 k 变量为 实际开始处理的位置 actualStart    

    b. Repeat, while k < (len - actualDeleteCount),     
        循环,当 k 小于 数组长度减去实际需要删除的长度,即 k 小于数组删除之后的长度    

        i. Let from be ! ToString(k + actualDeleteCount).   
            设置 from 为 k + actualDeleteCount    

        ii. Let to be ! ToString(k + itemCount).   
            设置 to 为 k + itemCount (k 位置是需要删除的项目,先把需要插入的数据写入 k)    

        iii. Let fromPresent be ? HasProperty(O, from).   
            看是否 o 有 from 属性    
        iv. If fromPresent is true, then   
            如果有,则   

            1. Let fromValue be ? Get(O, from).   
                获取 O 对象的 from 属性的值     

            2. Perform ? Set(O, to, fromValue, true).   
                设置 O 对象 to 属性的值为 fromValue    

        v. Else,       
            1. Assert: fromPresent is false.   
                断言:fromPresent 是false     

            2. Perform ? DeletePropertyOrThrow(O, to).   
                则删除 O 对象的 to 值    

        vi. Set k to k + 1.   
            k = k + 1    

    c. Set k to len.   
        将 k 赋值为整个数组长度 len   

    d. Repeat, while k > (len - actualDeleteCount + itemCount),  
        循环,如果当前 k 大于 最终处理完的速度的长度,则    

        i. Perform ? DeletePropertyOrThrow(O, ! ToString(k - 1)).   
            删除 O 对象末尾多余的值  (末尾的值已经移位到前面了)   

        ii. Set k to k - 1.   
            k = k - 1   
            
15. Else if itemCount > actualDeleteCount, then   
    如果需要插入的数据的长度大于需要删除的数据的长度(则最终长度是变长的),则    

    a. Set k to (len - actualDeleteCount).   
        设置 k 为 len - actualDeleteCount     

    b. Repeat, while k > actualStart,   
        循环,当 k 大于 actualStart 时    

        i. Let from be ! ToString(k + actualDeleteCount - 1).   
            设置 from 为 k + actualDeleteCount - 1 (即从最后开始,第一次循环的时候,值为最后一个项,然后倒数第二项)    

        ii. Let to be ! ToString(k + itemCount - 1).   
            设置 to 为 k + itemCount - 1 (即删除数据之后,超出当前数组之后所能达到的最大的位置,当第一次循环的时候,是索引最大的位置,其次逐步缩小)    

        iii. Let fromPresent be ? HasProperty(O, from).   
            对象 O 是否有 值为 from 的属性    

        iv. If fromPresent is true, then    
            如果 fromPreset 值为 true    

            1. Let fromValue be ? Get(O, from).   
                则获取 O 对象的 值为 from 的值 fromValue     

            2. Perform ? Set(O, to, fromValue, true).   
                将 fromValue 的值设置到 to 属性。  

        v. Else,        
            1. Assert: fromPresent is false.   
                断言: fromPresent 是 false    

            2. Perform ? DeletePropertyOrThrow(O, to).   
                删除 O 对象的 to 值     

        vi. Set k to k - 1.   
            k  = k - 1   
 
16. Set k to actualStart.        
    设置变量 k 的值为 actualStart    

17. For each element E of items, do    
    循环处理插入的 items   

    a. 1Perform ? Set(O, ! ToString(k), E, true).   
        从 k 位置开始,依次插入数据    

    b. Set k to k + 1.    
        k = k + 1    

18. Perform ? Set(O, "length", len - actualDeleteCount + itemCount, true).   
    设置 O 对象的 length 属性值为 原来长度  减去  删除的 加上 插入的 总长度    

19. Return A.    
    返回 A 数组,即删除的数据集合   

NOTE 2 The explicit setting of the "length" property of the result Array in step 18 was necessary in previous editions of ECMAScript to ensure that its length was correct in situations where the trailing elements of the result Array were not present. Setting "length" became unnecessary starting in ES2015 when the result Array was initialized to its proper length rather than an empty Array but is carried forward to preserve backward compatibility.

NOTE 3 The splice function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method.

是吧,真的很简单!再尝试看完 slice,shift 的实现之后,我们就清晰的知道三者的性能差距了。后续如有不太清楚的,第一时间去查阅 ECMAScript 规范吧!

比如 reduce 的性能如何?