一个偶然,了解到了github上的一个项目,fast.js,差不多3000多个star,留意了一下最后的更新时间。好吧,虽然是3年前的项目了,但是对于我来说还是很有研究的意义。
首先,看到的Array
的文件夹,点开发现,全是Array.prototype
的方法,不管了,就先看看它吧。
clone
function fastCloneArray (input) {
var length = input.length,
sliced = new Array(length),
i;
for (i = 0; i < length; i++) {
sliced[i] = input[i];
}
return sliced;
};
第一个是fastCloneArray
,顾名思义,应该是数组拷贝的相关方法。思路很简单,输入源数组,根据原数组的长度,new
一个长度为原数组的数组,然后循环赋值,把源数组的值依次拷贝给new
的新数组,返回新数组。
这种写法我认为有一个很巧妙的地方,var sliced = new Array(length)
,按照我的写法,我可能会直接var sliced = []
,而不是声明一个长度为lenght
的数组,他这样做的好处是,这个数组的长度不会改变,我们知道,在JavaScript中,数组的长度是不定的,是可以改变的,不像其他语言那样,声明一个数组,数组的长度必须声明,且固定,一旦超过数组的长度,就会报错之类的。而JavaScript不会,他会在内存里申请一个与原数组容量的1.5倍+16
这么大的空间,然后将原数组的值依次复制到新的空间里,作为当前数组,然后添加新值,这样的操作无形间会让程序慢一点。而直接声明了数组的长度,就免去了这一步。
当然,我认为这样的写法还是有点问题的,我们知道深拷贝的方法一般是循环浅拷贝,但是,如果输入的对象是一个二维以上的数组,这样的方法就不适用了。比如:
var a = [1,[2,3],4];
var b = fastCloneArray(a);
console.log(a[1]);//[2,3]
console.log(b[1]);//[2,3]
a[1].push(3);
console.log(a[1]);//[2,3,3]
console.log(b[1]);//[2,3,3]
当a[1].push(3)
,因为a[1]
,b[1]
里面保留的都是[2,3]
这个数组的引用,所以一改,全改。当然,本来在JavaScript中,二维数组用的地方就比较少。如果要真的修改的话,我觉得可以如下修改,做一个递归:
function fastCloneArray (input) {
var length = input.length,
sliced = new Array(length),
i;
for (i = 0; i < length; i++) {
if(input[i] instanceof Array){
sliced[i] = fastCloneArray(input[i]);
}else{
sliced[i] = input[i];
}
}
return sliced;
};
简单的判断一下input[i]
是否是一个数组,如果是,执行递归。
修改了一下之后,二维数组也可以拷贝了:
var a = [1,[2,3],4];
var b = fastCloneArray(a);
console.log(a[1]);//[2,3]
console.log(b[1]);//[2,3]
a[1].push(3);
console.log(a[1]);//[2,3,3]
console.log(b[1]);//[2,3]
有关clone
就告一段落了,下面看第二个方法。
concat
先上源码:
function fastConcat () {
var length = arguments.length,
arr = [],
i, item, childLength, j;
for (i = 0; i < length; i++) {
item = arguments[i];
if (Array.isArray(item)) {
childLength = item.length;
for (j = 0; j < childLength; j++) {
arr.push(item[j]);
}
}
else {
arr.push(item);
}
}
return arr;
};
根据代码,好像是把所有的参数都当做了加入的数组,貌似和原生的concat
方法有些不符,举个例子:
var a = [1,2,3];
var b = [4,5,6];
//原生concat
var c = a.concat(b);
console.log(c);//[1,2,3,4,5,6]
//因为concat不会修改原数组,所以可以接着使用 a, b
var d = fastConcat(a,b);
console.log(d);//[1,2,3,4,5,6]
看上去,好像除了写法上稍有不同,并没有其他的问题,是的,这个方法摆在3年前确实是一点问题都没有,但是现在,恐怕就有点问题了,我们知道ES6有Symbol.isConcatSpreadable
,来控制在concat
的时候到底是拆开放入,还是整体是一个元素放入,所以,如果是Symbol.isConcatSpreadable === false
的数组,就能看出区别了,比如:
var a = [1,2,3];
var b = [4,5,6];
b[Symbol.isConcatSpreadable] = false;
//原生concat
var c = a.concat(b);
console.log(c);//[1,2,3,[4,5,6]]
//因为concat不会修改原数组,所以可以接着使用 a, b
var d = fastConcat(a,b);
console.log(d);//[1,2,3,4,5,6]
所以,修改这个方法也非常简单,具体如下:
function fastConcat () {
var length = arguments.length,
arr = [],
i, item, childLength, j;
for (i = 0; i < length; i++) {
item = arguments[i];
if (Array.isArray(item)&& item[Symbol.isConcatSpreadable] !== false) {
childLength = item.length;
for (j = 0; j < childLength; j++) {
arr.push(item[j]);
}
}
else {
arr.push(item);
}
}
return arr;
};
concat
差不多就分析完毕了,下面开始第3个方法:
fill
fill
这个方法,简单粗暴,先直接上源码:
function fastFill (subject, value, start, end) {
var length = subject.length,
i;
if (start === undefined) {
start = 0;
}
if (end === undefined) {
end = length;
}
for (i = start; i < end; i++) {
subject[i] = value;
}
return subject;
};
不得不说,进过测试,发现,这个方法要比原生的fill
快差不多一倍左右。虽然我是没看出来哪里有过人之处。当然,这样的写法其实不太严谨。比如,如果end
比length
大的时候应该怎么处理之类的。看了MDN上的Polyfill,虽然实现的思路一致,不过更严谨,因为原代码是加在Array.prototype
上的扩展写法,不太建议直接在Array
原型上扩展,所以改变了一下写法,具体代码如下:
function fastFill(arr,value) {
// Steps 1-2.
if (arr == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(arr);
// Steps 3-5.
var len = O.length >>> 0;
// Steps 6-7.
var start = arguments[2];
var relativeStart = start >> 0;
// Step 8.
var k = relativeStart < 0 ?
Math.max(len + relativeStart, 0) :
Math.min(relativeStart, len);
// Steps 9-10.
var end = arguments[3];
var relativeEnd = end === undefined ?
len : end >> 0;
// Step 11.
var final = relativeEnd < 0 ?
Math.max(len + relativeEnd, 0) :
Math.min(relativeEnd, len);
// Step 12.
while (k < final) {
O[k] = value;
k++;
}
// Step 13.
return O;
};
思路和fast.js
是一样的,但是更加严谨了些。还有一些比较亮眼的地方是使用了>>>0
,>>0
。通过>>>0
,>>0
,可以确保作用的变量一定是数字,如果变量是undefind,null
字母之类的,都会被转为0
,比用parseInt()
之类的简单一点。这个Polyfill速度也是比原生的fill
快一倍左右。至于原生为什么这么慢。。因为代码是C++,我并不是很熟,所以,先这样,等研究好了在说。
every
every
方法,我看得是有点懵逼的。代码如下:
function fastEvery (subject, fn, thisContext) {
var length = subject.length,
iterator = thisContext !== undefined ? bindInternal3(fn, thisContext) : fn,
i;
for (i = 0; i < length; i++) {
if (!iterator(subject[i], i, subject)) {
return false;
}
}
return true;
};
function bindInternal3 (func, thisContext) {
return function (a, b, c) {
return func.call(thisContext, a, b, c);
};
};
其实这个逻辑不难,就是一个for
循环,把fn
函数,一个一个作用于subject
的元素,一旦有一个返回了false
,那就返回false
,全部返回了true
,才返回true
。我懵逼的地方是在于他的第三个参数thisContext
,他对于这个参数的解释是The context for the visitor.
也就是fn
的环境,最后的做法确实是把fn
与thisContext
绑在一起了,但是意义何在呢?确实想不透,有想到,可能与this
的隐式绑定有关,也许fn
里面有this
,指向一个固定的变量?但是也想不通啊,明明应该作用的是subject
里面的元素啊。百思不得其解。。。想不到thisContext
的应用场景。话说,原生的every
也没有需要输入thisContext
的地方啊。不明觉厉了。