一個偶然,了解到了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
的地方啊。不明覺厲了。