背景
filter
这个函数,相信大家都不会陌生,他是 es2015 新增的数组的方法,用以过滤。它接受一个函数,返回符合函数条件的数组,如果没有,则返回空数组。
语法如下
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg]) |
各个参数的具体含义如下(来源于 MDN)
callback
用来测试数组的每个元素的函数。返回true
表示该元素通过测试,保留该元素,false
则不保留。它接受以下三个参数:
element
数组中当前正在处理的元素。
index 可选
正在处理的元素在数组中的索引。
array 可选
调用了filter
的数组本身。
thisArg 可选
执行callback
时,用于this
用法
filter
是一个基础的数组操作用法,它的基本用法如下
//过滤普通数组 |
当然,它还可以和别的东西结合起来,比如取数组交集
/** |
一个 bug 引发的思考
接下来切入正题,之前 review 代码,然后发现了一个关于 filter
的 bug,bug 的原因很简单,写错了 filter
传入的函数的判断体。大致代码如下
var data = [{ a: 1, b: 4 }, { a: 5, b: 8 }] |
相信各位同学一眼就能看出问题所在,的确就是判断的 ===
写成了=
那么问题来了,这个res
的结果是多少,各位同学知道吗?是空数组?还是报错(Uncaught SyntaxError: Unexpected token
)?
不知道的同学可以尝试一下,结果其实是下面这样子的
var data = [{ a: 1, b: 4 }, { a: 5, b: 8 }] |
????我们再来打印一下原始数组
var data = [{ a: 1, b: 4 }, { a: 5, b: 8 }] |
嗯?很奇怪是不是,难不成返回了原数组?我们接下来继续判断一下是不是原数组
var data = [{ a: 1, b: 4 }, { a: 5, b: 8 }] |
看到这里,我们可以发现,这个问题不是filter
返回了原始数组,所以并不是 bug,那么究竟是什么原因导致的问题呢?我们继续往下看。
Polyfill
分析此类问题,最好的办法还是查看源码,既然filter
是 es2015 新增的特性,那么,必然会有 Polyfill 对其进行向下兼容。于是我们翻看MDN,找到如下代码
注:
filter
被添加到 ECMA-262 标准第 5 版中,因此在某些实现环境中不被支持。可以把下面的代码插入到脚本的开头来解决此问题,该代码允许在那些没有原生支持filter
的实现环境中使用它。该算法是 ECMA-262 第 5 版中指定的算法,假定fn.call
等价于Function.prototype.call
的初始值,且Array.prototype.push
拥有它的初始值。
if (!Array.prototype.filter) { |
具体分析
从上面的代码我们可以很明显的看出,过滤操作主要发生在while
循环内,首先先判断func(t[i], i, t)
是否为true
,如果是,则将此项复制一份到res
中,最后循环结束,返回res
整个 Polyfill 思路清晰,我们可以得出一点,就是之前的问题,原因产生在下面这段代码中
if (func(t[i], i, t)) { |
根据实际情况,我们可以得出以下假设
if
函数体判断一直为true
,然后函数体再对原始数组进行修改,导致,返回结果与原数组一样(注意,这里的原数组指的是修改之后的)
我们尝试在Array
上挂载一个filters
的方法(做一个对比,不覆盖原有的filter
方法),代码就为上述 Polyfill 的代码(加上了两句console.log
)
if (!Array.prototype.filters) { |
上面这段代码和我们之前的 bug 结果一样,那我们再改一下,把最后函数体改为 row => (row.a = 0)
,然后你会发现,返回的是一个空数组。Why?
这里需要说明的就是 JavaScript 的类型转换规则了,我们翻看红宝书,可以得到下列对于Boolean
转换规则
数据类型 | 转换为true |
转换为false |
---|---|---|
Boolean |
true |
false |
String |
任何非空字符串 | '' (空字符串) |
Number |
任何非零数值(包括无穷) | 0 和NaN |
Object |
任何对象 | null |
Undefined |
n/a (N/A 代表不适用) |
undefined |
我们还是会有一个疑问,为什么 row => (row.a = 0)
这段函数体调用打印结果和最后赋值有关?
我们接着尝试
var test = row => { |
下面还有个例子
var obj = {} |
这个问题,我们回到表达式上面来,JS 中表达式分为两种,单值表达式和复合表达式
这里得感谢mutoe大佬的提点,我也算查漏补缺了…
JavaScript
表达式总有返回值,其中,单值表达式的结果是值本身,其他表达式结果是根据运算符进行运算的结果值
于是,上面的一切我们都可以解释的通了,因为我们传入的函数体 row => (row.a = 0)
包含一个单值表达式,所以它相当于 row => return 0
还剩下最后一个问题,关于原始数组的修改。其实这个问题很好理解,因为传入的是对象类型,JS 的对象类型都是由引用地址进行传递,如果没有进行深拷贝,那么它所传递的就是一个堆地址的一个指针。所以,修改传递进来的对象,原始数组也会修改(因为说到底,内存的堆区域,就只有一个数组)
可以参考下面的例子
let objc = { a: 77 } |
接下来,我们做一个总结,为什么filter
传入的函数体写法错误(比如传入row => row.a = 1
),会导致原始结果改变
根本原因是由于
filter
的实现过程,决定了输出结果由传入函数决定,而函数的输出(return
)结果,则由函数体内部运算决定,当内部是一个单值表达式的时候,由于 JS 表达式的返回值,它会返回值本身,然后函数也返回这个结果,进而进入if
判断,接着由于 JS 的Boolean
类型转换规则,输出true
/false
,然后得到意外的结果。而这个过程中,如果传入的数组是一个对象数组,那么原始的结果会进行改变,如果不是,那么将会得到原数组的一份拷贝(见下面代码)
var arr = [1, 2, 3] |
总结
没想到一个小小的
filter
,由一次 bug 引发了诸多思考,尽管个人认为filter
的规范里面应该将上面 bug 操作进行Uncaught SyntaxError: Unexpected token
报错,但是进而思考,它并不能检测函数体内部(或许可以,但是很麻烦,实现起来也并不现实),因而,我们实际使用中还是要多多注意细节问题。
参考文章
[1]无符号右移 0 操作