这是我参与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
大致意思表达如下:
- 将
this具象化为一个对象变量,方便接下来的操作。 - 初始化数组的
length,也就是对length值进行合法化的处理,它必须是一个有效范围内的整数。 - 判断是否存在回调函数
callback,不存在则抛出一个TypeError异常。 - 创建一个与数组长度相同的数组,存储回调函数处理结果以及作为返回值。
- 遍历每个元素,逐个使用回调函数处理,然后判断回调函数返回值,如果为
true则加入结果数组中,否则执行下一次遍历。 - 元素全部处理完毕后,返回结果数组。
手写实现
依照上面分析,来手写实现下
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方法被设计成一个通用的方法,而不仅仅适用于数组,因此对于类数组,或者具备数组相关特性的对象,都能复用这个函数。