从V8源码角度来手撕JS数组方法底层实现(三):filter

1,517 阅读4分钟

这是我参与8月更文挑战的第25天,活动详情查看:8月更文挑战

前言

filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 的元素创建一个新数组。callback 只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中,通常用于过滤数组中符合我们期望条件的元素。

var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

它的函数签名如下:

  • callback:用来测试每个元素是否符合条件的回调函数,符合则返回true,否则返回false

    • element:当前正在处理的元素。
    • index:当前正在处理的元素的索引
    • array:数组本身。
  • thisArg:作为 callback 调用时的 this

filter 不会改变原数组,而是返回过滤后的新数组。

同时,遍历的元素范围在第一次调用 callback 之前就已经确定了。在调用 filter 之后被添加到数组中的元素不会被 filter 遍历到。如果已经存在的元素被改变了,则它们传入 callback 的值是 filter 遍历到它们那一刻的值,空值会被忽略。

ECMA规范

先看下ECMA规范中规定的filter方法执行过程: ECMAScript® 2022 Language Specification (tc39.es) - Array.prototype.filter

image.png

大致意思表达如下:

  1. this具象化为一个对象变量,方便接下来的操作。
  2. 初始化数组的length,也就是对length值进行合法化的处理,它必须是一个有效范围内的整数。
  3. 判断是否存在回调函数callback,不存在则抛出一个TypeError异常。
  4. 创建一个与数组长度相同的数组,存储回调函数处理结果以及作为返回值。
  5. 遍历每个元素,逐个使用回调函数处理,然后判断回调函数返回值,如果为true则加入结果数组中,否则执行下一次遍历。
  6. 元素全部处理完毕后,返回结果数组。

手写实现

依照上面分析,来手写实现下

Array.prototype._filter = function(callback, thisArg) {
  // 首先判断回调函数是否合法,以及当前环境的this是否有指向的值。
  if ( !(typeof callback === 'function' && this) )
    throw new TypeError();
  // 初始化length值,必须是一个可表示范围内的整数
  let length = this.length >>> 0,
      result = new Array(length),
      o = Object(this),len = 0, i = -1;
  // thisArg不存在
  if (thisArg === undefined){
    while (++i !== length){
      if (i in this){
         // 回调函数调用时不需要绑定thisArg
        if (callback(o[i], i, o)){
          result[len++] = o[i];
        }
      }
    }
  }
  // thisArg存在
  else{
    while (++i !== length){
      if (i in this){
        // 回调函数调用时绑定thisArg
        if (callback.call(thisArg, o[i], i, o)){
          result[len++] = o[i];
        }
      }
    }
  }
  result.length = len;
  return result;
};

值得注意的是,与map方法不同,虽然都是返回一个处理后的数组,但是filter不会保持与调用数组相同的稀疏结构。

[,,1,2,3,,4.filter(i=>true)//[1, 2, 3, 6]

虽然过滤条件都是true,但新数组忽略的原数组的空值,这是我们手写实现时值得注意的,在上述实现中,len代表结果数组当前的长度,而空元素会被忽略而不被回调函数处理,从而不会执行len的自增。

V8实现

再来看看v8中是怎么实现的:v8/array.js -filter

// The following functions cannot be made efficient on sparse arrays while
// preserving the semantics, since the calls to the receiver function can add
// or delete elements from the array.
// filter方法的核心
function InnerArrayFilter(f, receiver, array, length, result) {
  var result_length = 0;
  for (var i = 0; i < length; i++) {
    if (i in array) {
      var element = array[i];
      if (%_Call(f, receiver, element, i, array)) {
        %CreateDataProperty(result, result_length, element);
        result_length++;
      }
    }
  }
  return result;
}



function ArrayFilter(f, receiver) {
   // CHECK_OBJECT_COERCIBLE函数对this进行检查,判断其是否可以具象化为对象变量,不行则抛出异常
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.filter");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  
  // this具象化为对象变量,也就是this指向的数组本身
  var array = TO_OBJECT(this);
  // length合法性处理
  var length = TO_LENGTH(array.length);
  // 判断回调函数是否合法
  if (!IS_CALLABLE(f)) throw %make_type_error(kCalledNonCallable, f);
  // 创建一个长度相同的结果数组
  var result = ArraySpeciesCreate(array, 0);
  // InnerArrayFilter中负责返回处理数组的数据,并返回处理后的数组
  return InnerArrayFilter(f, receiver, array, length, result);
}

对比我们自己实现和v8实现,我们的实现与v8相差无几,区别在于v8封装了一些安全性判断和边界处理,并且在实现上都使用语言最基本的表达式以及自封装的函数,这样没有兼容性问题。

依据ECMA规范,filter方法被设计成一个通用的方法,而不仅仅适用于数组,因此对于类数组,或者具备数组相关特性的对象,都能复用这个函数。