JavaScript數組所有API全解密

RYPJulius 7年前發布 | 32K 次閱讀 API JavaScript開發 ECMAScript JavaScript

數組是一種非常重要的數據類型,它語法簡單、靈活、高效。 在多數編程語言中,數組都充當著至關重要的角色,以至于很難想象沒有數組的編程語言會是什么模樣。特別是JavaScript,它天生的靈活性,又進一步發揮了數組的特長,豐富了數組的使用場景。可以毫不夸張地說,不深入地了解數組,不足以寫JavaScript。

截止ES7規范,數組共包含 33 個標準的API方法和一個非標準的API方法,使用場景和使用方案紛繁復雜,其中有不少淺坑、深坑、甚至神坑。下面將從 Array 構造器及ES6新特性開始,逐步幫助你掌握數組。

聲明:以下未特別標明的方法均為ES5已實現的方法。

Array構造器

Array 構造器用于創建一個新的數組。通常,我們推薦使用對象字面量創建數組,這是一個好習慣,但是總有對象字面量乏力的時候,比如說,我想創建一個長度為8的空數組。請比較如下兩種方式:

// 使用Array構造器
var a = Array(8); // => [undefined × 8]
// 使用對象字面量
var b = [];
b.length = 8;     // => [undefined × 8]

Array 構造器明顯要簡潔一些,當然你也許會說,對象字面量也不錯啊,那么我保持沉默。

如上,我使用了 Array(8) 而不是 new Array(8) ,這會有影響嗎?實際上,并沒有影響,這得益于 Array 構造器內部對 this 指針的判斷, ELS5_HTML規范 是這么說的:

When Array is called as a function rather than as a constructor, it creates and initialises a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments.

從規范來看,瀏覽器內部大致做了如下類似的實現:

function Array(){
    // 如果this不是Array的實例,那就重新new一個實例
    if(!(this instanceof arguments.callee)){
        return new arguments.callee();
    }
}

上面,我似乎跳過了對 Array 構造器語法的介紹,沒事,接下來我補上。

Array 構造器根據參數長度的不同,有如下兩種不同的處理:

  • new Array(arg1, arg2,…) ,參數長度為 0 或長度大于等于 2 時,傳入的參數將按照順序依次成為新數組的第 0 至 N 項(參數長度為 0 時,返回空數組)。
  • new Array(len) ,當 len 不是數值時,處理同上,返回一個只包含 len 元素一項的數組;當 len 為數值時,根據如下 規范 , len 最大不能超過 32 位無符號整型,即需要小于 2 的 32 次方( len 最大為 Math.pow(2,32) -1或-1>>>0) ,否則將拋出 RangeError

If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len) . If the argument len is a Number and ToUint32(len) is not equal to len , a RangeError exception is thrown.

以上,請注意 Array 構造器對于單個數值參數的特殊處理,如果僅僅需要使用數組包裹若干參數,不妨使用 Array.of ,具體請移步下一節。

ES6新增的構造函數方法

鑒于數組的常用性,ES6專門擴展了數組構造器 Array ,新增 2 個方法: Array.of() 、 Array.from() 。下面展開來聊。

Array.of

Array.of() 用于將參數依次轉化為數組中的一項,然后返回這個新數組,而不管這個參數是數字還是其它。它基本上與 Array 構造器功能一致,唯一的區別就在單個數字參數的處理上。如下:

Array.of(8.0); // => [8] 
Array(8.0);    // => [undefined × 8]

參數為多個,或單個參數不是數字時, Array.of() 與 Array 構造器等同。

Array.of(8.0, 5); // => [8, 5]
Array(8.0, 5);    // => [8, 5]
Array.of('8');    // => ["8"]
Array('8');       // => ["8"]

因此,若是需要使用數組包裹元素,推薦優先使用 Array.of() 方法。

即使其他版本瀏覽器不支持也不必擔心,由于 Array.of() 與 Array 構造器的這種高度相似性,實現一個polyfill十分簡單。如下:

if (!Array.of){
    Array.of = function(){
        return Array.prototype.slice.call(arguments);
    };
}

Array.from

// 語法:
Array.from(arrayLike[, processingFn[, thisArg]])

Array.from() 的設計初衷是快速便捷的基于其他對象創建新數組,準確來說就是從一個類似數組的可迭代對象創建一個新的數組實例,說人話就是,只要一個對象有迭代器, Array.from() 就能把它變成一個數組(當然,是返回新的數組,不改變原對象)。

從語法上看, Array.from() 擁有 3 個形參,第一個為類似數組的對象,必選。第二個為加工函數,新生成的數組會經過該函數的加工再返回。第三個為 this 作用域,表示加工函數執行時 this 的值。后兩個參數都是可選的。我們來看看用法。

var obj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj, function(value, index){
    console.log(value, index, this, arguments.length);
    return value.repeat(3); //必須指定返回值,否則返回undefined
}, obj);

執行結果如下:

可以看到加工函數的 this 作用域被 obj 對象取代,也可以看到加工函數默認擁有兩個形參,分別為迭代器當前元素的值和其索引。

注意,一旦使用加工函數,必須明確指定返回值,否則將隱式返回 undefined ,最終生成的數組也會變成一個只包含若干個 undefined 元素的空數組。

實際上,如果不需要指定 this ,加工函數完全可以是一個箭頭函數。上述代碼可以簡化如下:

Array.from(obj, (value) => value.repeat(3));

除了上述 obj 對象以外,擁有迭代器的對象還包括這些: String , Set , Map , arguments 等, Array.from() 統統可以處理。如下所示:

// String
Array.from('abc'); // => ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def'])); // => ["abc", "def"]
// Map
Array.from(new Map([[1, 'abc'], [2, 'def']])); // => [[1, 'abc'], [2, 'def']]
// 天生的類數組對象arguments
function fn(){
return Array.from(arguments);
}
fn(1, 2, 3); // => [1, 2, 3]

到這你可能以為 Array.from() 就講完了,實際上還有一個重要的擴展場景必須提下。比如說生成一個從 0 到指定數字的新數組, Array.from() 就可以輕易的做到。

Array.from({length: 10}, (v, i) => i); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

后面我們將會看到,利用數組的 keys 方法實現上述功能,可能還要簡單一些。

Array.isArray

顧名思義, Array.isArray 用來判斷一個變量是否數組類型。JS的弱類型機制導致判斷變量類型是初級前端開發者面試時的必考題,一般我都會將其作為考察候選人第一題,然后基于此展開。在ES5提供該方法之前,我們至少有如下5種方式去判斷一個值是否數組:

var a = [];
// 1.基于instanceof
a instanceof Array;

// 2.基于constructor
a.constructor === Array;

// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a);

// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype;

// 5.基于Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';

以上,除了 Object.prototype.toString 外,其它方法都不能正確判斷變量的類型。

要知道,代碼的運行環境十分復雜,一個變量可能使用渾身解數去迷惑它的創造者。且看:

var a = {
    __proto__: Array.prototype
};

// 分別在控制臺試運行以下代碼
// 1.基于instanceof
a instanceof Array; // => true

// 2.基于constructor
a.constructor === Array; // => true

// 3.基于Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a); // => true

// 4.基于getPrototypeOf
Object.getPrototypeOf(a) === Array.prototype; // => true

以上, 4 種方法將全部返回 true ,為什么呢?我們只是手動指定了某個對象的 __proto__ 屬性為 Array.prototype ,便導致了該對象繼承了 Array 對象,這種毫不負責任的繼承方式,使得基于繼承的判斷方案瞬間土崩瓦解。

不僅如此,我們還知道, Array 是堆數據,變量指向的只是它的引用地址,因此每個頁面的 Array 對象引用的地址都是不一樣的。 iframe 中聲明的數組,它的構造函數是 iframe 中的 Array 對象。如果在 iframe 聲明了一個數組 x ,將其賦值給父頁面的變量 y ,那么在父頁面使用 y instanceof Array ,結果一定是 false 的。而最后一種返回的是字符串,不會存在引用問題。實際上,多頁面或系統之間的交互只有字符串能夠暢行無阻。

鑒于上述的兩點原因,故筆者推薦使用最后一種方法去撩面試官(別提是我說的),如果你還不信,這里恰好有篇文章跟我持有相同的觀點: Determining with absolute accuracy whether or not a JavaScript object is an array

相反,使用 Array.isArray 則非常簡單,如下:

Array.isArray([]);                  // => true
Array.isArray({0: 'a', length: 1}); // => false

實際上,通過 Object.prototype.toString 去判斷一個值的類型,也是各大主流庫的標準。因此 Array.isArray 的polyfill通常長這樣:

if (!Array.isArray){
    Array.isArray = function(arg){
        return Object.prototype.toString.call(arg) === '[object Array]';
    };
}

數組遍歷

ES6對數組的增強不止是體現在API上,還包括語法糖。比如說 for ... of ,它就是借鑒其它語言而成的語法糖,這種基于原數組使用 for ... of 生成新數組的語法糖,叫做 數組遍歷 。 數組遍歷最初起早在ES6的草案中,但在第27版(2014年8月)中被移除,目前只有Firefox v30+支持,遍歷有風險,使用需謹慎。 所幸如今這些語言都還支持遍歷:CoffeeScript、Python、Haskell、Clojure,我們可以從中一窺端倪。這里我們以Python的 for ... in 遍歷打個比方:

# python for in 推導
a = [1, 2, 3, 4]
print [i * i for i in a if i == 3] # [9]

如下是SpiderMonkey引擎(Firefox)之前基于ES4規范實現的數組推導(與Python的推導十分相似):

[i * i for (i of a)] // => [1, 4, 9, 16]

ES6中數組有關的 for ... of 在ES4的基礎上進一步演化, for 關鍵字居首, in 在中間,最后才是運算表達式。如下:

[for (i of [1, 2, 3, 4]) i * i] // => [1, 4, 9, 16]

同Python的示例,ES6中數組有關的 for ... of 也可以使用 if 語句:

// 單個if
[for (i of [1, 2, 3, 4]) if (i == 3) i * i] // => [9]

// 甚至是多個if
[for (i of [1, 2, 3, 4]) if (i > 2) if (i < 4) i * i] // => [9]

更為強大的是,ES6數組推導還允許多重 for ... of 。

[for (i of [1, 2, 3]) for (j of [10, 100]) i * j] // => [10, 100, 20, 200, 30, 300]

甚至,數組推導還能夠嵌入另一個數組推導中。

[for (i of [1, 2, 3]) [for (j of [10, 100]) i * j] ] // => [[10, 100], [20, 200], [30, 300]]

對于上述兩個表達式,前者和后者唯一的區別,就在于后者的第二個推導是先返回數組,然后與外部的推導再進行一次運算。

除了多個數組推導嵌套外,ES6的數組推導還會為每次迭代分配一個新的作用域(目前Firefox也沒有為每次迭代創建新的作用域):

// ES6規范
[for (x of [0, 1, 2]) () => x][0]() // 0

// Firefox運行
[for (x of [0, 1, 2]) () => x][0]() // 2

通過上面的實例,我們看到使用數組推導來創建新數組比 forEach , map , filter 等遍歷方法更加簡潔,只是非常可惜,它不是標準規范。

ES6不僅新增了對 Array 構造器相關API,還新增了 8 個原型的方法。接下來我會在原型方法的介紹中穿插著ES6相關方法的講解,請耐心往下讀。

原型

繼承的常識告訴我們,JS中所有的數組方法均來自于 Array.prototype ,和其他構造函數一樣,你可以通過擴展 Array 的 prototype 屬性上的方法來給所有數組實例增加方法。

值得一說的是, Array.prototype 本身就是一個數組。

Array.isArray(Array.prototype);      // => true
console.log(Array.prototype.length); // => 0

以下方法可以進一步驗證:

console.log([].__proto__.length); // => 0
console.log([].__proto__);        // => [Symbol(Symbol.unscopables): Object]

有關 Symbol(Symbol.unscopables) 的知識,這里不做詳述,具體請移步后續章節。

方法

數組原型提供的方法非常之多,主要分為三種,一種是會改變自身值的,一種是不會改變自身值的,另外一種是遍歷方法。

由于 Array.prototype 的某些屬性被設置為 [[DontEnum]] ,因此不能用一般的方法進行遍歷,我們可以通過如下方式獲取 Array.prototype 的所有方法:

Object.getOwnPropertyNames(Array.prototype);

改變自身值的方法(9個)

基于ES6,改變自身值的方法一共有 9 個,分別為 pop() 、 push() 、 reverse() 、 shift() 、 sort() 、 splice() 、 unshift() ,以及兩個ES6新增的方法 copyWithin() 和 fill() 。

對于能改變自身值的數組方法,日常開發中需要特別注意,盡量避免在循環遍歷中去改變原數組的項。接下來,我們一起來深入地了解這些方法。

pop()

pop() 方法刪除一個數組中的最后的一個元素,并且返回這個元素。如果是棧的話,這個過程就是棧頂彈出。

var array = ["cat", "dog", "cow", "chicken", "mouse"];
var item = array.pop();
console.log(array); // => ["cat", "dog", "cow", "chicken"]
console.log(item);  // => mouse

由于設計上的巧妙, pop() 方法可以應用在類數組對象上,即 鴨式辨型 。 如下:

var o = {
    0:"cat", 
    1:"dog", 
    2:"cow", 
    3:"chicken", 
    4:"mouse", 
    length:5
}
var item = Array.prototype.pop.call(o);
console.log(o);    // => Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", length: 4}
console.log(item); // => mouse

但如果類數組對象不具有 length 屬性,那么該對象將被創建 length 屬性, length 值為 0 。如下:

var o = {
    0:"cat",
    1:"dog",
    2:"cow",
    3:"chicken",
    4:"mouse"
}
var item = Array.prototype.pop.call(o);
console.log(o);     // => Object {0: "cat", 1: "dog", 2: "cow", 3: "chicken", 4: "mouse", length: 0}
console.log(item);  // => undefined

push()

push() 方法添加一個或者多個元素到數組末尾,并且返回數組新的長度。如果是棧的話,這個過程就是棧頂壓入。

var array = ["football", "basketball", "volleyball", "Table tennis", "badminton"];
var i = array.push("golfball");
console.log(array);  // => ["football", "basketball", "volleyball", "Table tennis", "badminton", "golfball"]
console.log(i);      // => 6

同 pop() 方法一樣, push() 方法也可以應用到類數組對象上,如果 length 不能被轉成一個數值或者不存在 length 屬性時,則插入的元素索引為 0 ,且 length 屬性不存在時,將會創建它。

var o = {
    0:"football", 
    1:"basketball"
};
var i = Array.prototype.push.call(o, "golfball");
console.log(o); // => Object {0: "golfball", 1: "basketball", length: 1}
console.log(i); // => 1

實際上, push() 方法是根據 length 屬性來決定從哪里開始插入給定的值。

var o = {
    0:"football", 
    1:"basketball",
    length:1
};
var i = Array.prototype.push.call(o,"golfball");
console.log(o); // => Object {0: "football", 1: "golfball", length: 2}
console.log(i); // => 2

利用 push() 根據 length 屬性插入元素這個特點,可以實現數組的合并,如下:

var array = ["football", "basketball"];
var array2 = ["volleyball", "golfball"];
var i = Array.prototype.push.apply(array,array2);
console.log(array); // => ["football", "basketball", "volleyball", "golfball"]
console.log(i);     // => 4

reverse()

reverse() 方法顛倒數組中元素的位置,第一個會成為最后一個,最后一個會成為第一個,該方法返回對數組的引用。

var array = [1,2,3,4,5];
var array2 = array.reverse();
console.log(array);          // => [5,4,3,2,1]
console.log(array2===array); // => true

同上, reverse() 也是 鴨式辨型 的受益者,顛倒元素的范圍受 length 屬性制約。如下:

var o = {
    0:"a", 
    1:"b", 
    2:"c", 
    length:2
};
var o2 = Array.prototype.reverse.call(o);
console.log(o);        // => Object {0: "b", 1: "a", 2: "c", length: 2}
console.log(o === o2); // => true

如果 length 屬性小于 2 或者 length 屬性不為數值,那么原類數組對象將沒有變化。即使 length 屬性不存在,該對象也不會去創建 length 屬性。特別的是,當 length 屬性較大時,類數組對象的 『索引』 會盡可能的向 length 看齊。如下:

var o = {
    0:"a", 
    1:"b", 
    2:"c",
    length:100
};
var o2 = Array.prototype.reverse.call(o);
console.log(o);        // => Object {97: "c", 98: "b", 99: "a", length: 100}
console.log(o === o2); // => true

shift()

shift() 方法刪除數組的第一個元素,并返回這個元素。如果是棧的話,這個過程就是棧底彈出。

var array = [1,2,3,4,5];
var item = array.shift();
console.log(array); // => [2,3,4,5]
console.log(item);  // => 1

同樣受益于 鴨式辨型 ,對于類數組對象, shift() 仍然能夠處理。如下:

var o = {
    0:"a", 
    1:"b", 
    2:"c", 
    length:3
};
var item = Array.prototype.shift.call(o);
console.log(o);    // => Object {0: "b", 1: "c", length: 2}
console.log(item); // => a

如果類數組對象 length 屬性不存在,將添加 length 屬性,并初始化為 0 。如下:

var o = {
    0:"a", 
    1:"b", 
    2:"c"
};
var item = Array.prototype.shift.call(o);
console.log(o);    // => Object {0: "a", 1: "b", 2:"c" length: 0}
console.log(item); // => undefined

sort()

sort() 方法對數組元素進行排序,并返回這個數組。 sort() 方法比較復雜,這里我將多花些篇幅來講這塊。

// 語法:
arr.sort([comparefn])

comparefn 是可選的,如果省略,數組元素將按照各自轉換為字符串的Unicode(萬國碼)位點順序排序,例如 "Boy" 將排到 "apple" 之前。當對數字排序的時候, 25 將會排到 8 之前,因為轉換為字符串后, ”25” 將比 ”8” 靠前。例如:

var array = ["apple","Boy","Cat","dog"];
var array2 = array.sort();
console.log(array);           // => ["Boy", "Cat", "apple", "dog"]
console.log(array2 == array); // => true
array = [10, 1, 3, 20];
var array3 = array.sort();
console.log(array3);          // => [1, 10, 20, 3]

如果指明了 comparefn ,數組將按照調用該函數的返回值來排序。若 a 和 b 是兩個將要比較的元素:

  • 若 comparefn(a, b) < 0 ,那么 a 將排到 b 前面;
  • 若 comparefn(a, b) = 0 ,那么 a 和 b 相對位置不變;
  • 若 comparefn(a, b) > 0 ,那么 a , b 將調換位置;

如果數組元素為數字,則排序函數 comparefn 格式如下所示:

function compare(a, b){
    return a - b;
}

如果數組元素為非ASCII字符的字符串(如包含類似 e 、 é 、 è 、 a 、 ? 或中文字符等非英文字符的字符串),則需要使用 String.localeCompare 。下面這個函數將排到正確的順序。

var array = ['互','聯','網','改','變','世','界'];
var array2 = array.sort();
var array = ['互','聯','網','改','變','世','界']; // 重新賦值,避免干擾array2
var array3 = array.sort(function (a, b) {
    return a.localeCompare(b);
});
console.log(array2); 
console.log(array3);

這個示例中使用 String.localeCompare 跑出來的效果并無差異(使用的是Chrome瀏覽器),和原作者提供的示例有差別,特意這里提示一下。根據 String.prototype.localeCompare() - JavaScript | MDN 說明 , localeCompare 方法是和操作系統 locale 值相關。 換過一個語言來做示例,這個示例的代碼來自于《 排列含音節字母的字符串 》。

有兩種方法可以解決這個問題,由ECMAScript國際化API提供的 localeCompare 和 Intl.Collator 。

使用 localeCompare() :

['único','árbol', 'cosas', 'fútbol'].sort(function (a, b) {
    return a.localeCompare(b);
}); // => ["árbol", "cosas", "fútbol", "único"]

['Woche', 'w?chentlich', 'w?re', 'Wann'].sort(function (a, b) {
    return a.localeCompare(b);
}); // => ["Wann", "w?re", "Woche", "w?chentlich"]

使用 Intl.Collator() :

['único','árbol', 'cosas', 'fútbol'].sort(Intl.Collator().compare);
// => ["árbol", "cosas", "fútbol", "único"]

['Woche', 'w?chentlich', 'w?re', 'Wann'].sort(Intl.Collator().compare);
// => ["Wann", "w?re", "Woche", "w?chentlich"]

  • 兩個方法都可以自定義區域位置。
  • 根據Firefox,當比較大數量的字符串時,使用Intl.Collator更快。

所以當你處理一個由非英語組成的字符串數組時,記得使用此方法來避免排序出現意外。

同上, sort() 一樣受益于鴨式辨型,比如:

var o = {0:'互',1:'聯',2:'網',3:'改',4:'變',5:'世',6:'界',length:7};
Array.prototype.sort.call(o,function(a, b){
    return a.localeCompare(b);
});
console.log(o);

注意:使用 sort() 的鴨式辨型特性時,若類數組對象不具有 length 屬性,它并不會進行排序,也不會為其添加 length 屬性。

var o = {0:'互',1:'聯',2:'網',3:'改',4:'變',5:'世',6:'界'};
Array.prototype.sort.call(o,function(a, b){
    return a.localeCompare(b);
});
console.log(o);

comparefn 如果需要對數組元素多次轉換以實現排序,那么使用 map() 輔助排序將是個不錯的選擇。基本思想就是將數組中的每個元素實際比較的值取出來,排序后再將數組恢復。

// 需要被排序的數組
var array = ['dog', 'Cat', 'Boy', 'apple'];
// 對需要排序的數字和位置的臨時存儲
var mapped = array.map(function(el, i) {
    return { index: i, value: el.toLowerCase() };
})
// 按照多個值排序數組
mapped.sort(function(a, b) {
    return +(a.value > b.value) || +(a.value === b.value) - 1;
});
// 根據索引得到排序的結果
var result = mapped.map(function(el){
    return array[el.index];
});
console.log(result); // ["apple", "Boy", "Cat", "dog"]

實際上,ECMAscript規范中并未規定具體的 sort() 算法,這就勢必導致各個瀏覽器不盡相同的 sort() 算法,請看 sort() 方法在Chrome瀏覽器下表現:

var array = [
    { 
        n: "a", 
        v: 1 
    }, 
    { 
        n: "b",
        v: 1 
    }, 
    { 
        n: "c", 
        v: 1 
    }, 
    { 
        n: "d", 
        v: 1 
    }, 
    { 
        n: "e", 
        v: 1 
    }, 
    { 
        n: "f", 
        v: 1 
    }, 
    { 
        n: "g", 
        v: 1 
    }, 
    { 
        n: "h", 
        v: 1 
    }, 
    { 
        n: "i", 
        v: 1 
    }, 
    { 
        n: "j", 
        v: 1 
    }, 
    { 
        n: "k", 
        v: 1 
    }
];
array.sort(function (a, b) {
    return a.v - b.v;
});
for (var i = 0,len = array.length; i < len; i++) {
    console.log(array[i].n); // => f a c d e b g h i j k
}

由于 v 值相等, array 數組排序前后應該不變,然而Chrome卻表現異常,而其他瀏覽器(如IE 或 Firefox)表現正常。

這是因為v8引擎為了高效排序(采用了不穩定排序)。即數組長度超過 10 條時,會調用另一種排序方法(快速排序);而 10 條及以下采用的是插入排序,此時結果將是穩定的,如下:

var array = [
    { n: "a", v: 1 }, 
    { n: "b", v: 1 }, 
    { n: "c", v: 1 }, 
    { n: "d", v: 1 }, 
    { n: "e", v: 1 }, 
    { n: "f", v: 1 }, 
    { n: "g", v: 1 }, 
    { n: "h", v: 1 }, 
    { n: "i", v: 1 }, 
    { n: "j", v: 1 }
];
array.sort(function (a, b) {
    return a.v - b.v;
});
for (var i = 0,len = array.length; i < len; i++) {
    console.log(array[i].n); // => a b c d e f g h i j
}

從 a 到 j 剛好 10 條數據。

那么我們該如何規避Chrome瀏覽器的這種”bug”呢?其實很簡單,只需略動手腳,改變排序方法的返回值即可,如下:

array.sort(function (a, b) {
    return a.v - b.v || array.indexOf(a)-array.indexOf(b);
});

使用數組的 sort() 方法需要注意一點:各瀏覽器的針對 sort() 方法內部算法實現不盡相同,排序函數盡量只返回 -1 、 0 、 1 三種不同的值,不要嘗試返回 true 或 false 等其它數值,因為可能導致不可靠的排序結果。

sort() 方法傳入的排序函數如果返回布爾值會導致什么樣的結果呢?

以下是常見的瀏覽器以及腳本引擎:

瀏覽器名稱 ECMAScript引擎
IE6~IE8 JScript
IE9~I10 Chakra
Firefox SpiderMonkey, IonMonkey, TraceMonkey
Chrome V8
Safari JavaScriptCore(SquirrelFish Extreme)
Opera Carakan

分析以下代碼,預期將數組元素進行升序排序:

var array = [7, 6, 5, 4, 3, 2, 1, 0, 8, 9];
var comparefn = function (x, y) {
    return x > y;
};
array.sort(comparefn);

代碼中, comparefn 函數返回值為 bool 類型,并非為規范規定的 -1 、 0 、 1 值。那么執行此代碼,各 JS 腳本引擎實現情況如何?

ECMAScript引擎 輸出結果 是否符合預期
JScript [2, 3, 5, 1, 4, 6, 7, 0, 8, 9]
Carakan [0, 1, 3, 8, 2, 4, 9, 5, 6, 7]
Chakra & JavaScriptCore [7, 6, 5, 4, 3, 2, 1, 0, 8, 9]
SpiderMonkey [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
V8 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

根據表中數據可見,當數組內元素個數小于等于 10 時,現象如下:

  • JScript & Carakan 排序結果有誤
  • Chakra & JavaScriptCore 看起來沒有進行排序
  • SpiderMonkey 返回了預期的正確結果
  • V8 暫時看起來排序正確

將數組元素擴大至 11 位,現象如下:

var array = [7, 6, 5, 4, 3, 2, 1, 0, 10, 9, 8];
var comparefn = function (x, y) {
    return x > y;
};
array.sort(comparefn);
ECMAScript引擎 輸出結果 是否符合預期
JScript [2, 3, 5, 1, 4, 6, 7, 0, 8, 9, 10]
Carakan [0, 1, 3, 8, 2, 4, 9, 5, 10, 6, 7]
Chakra & JavaScriptCore [7, 6, 5, 4, 3, 2, 1, 0, 10, 8, 9]
IonMonkey [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
V8 [5, 0, 1, 2, 3, 4, 6, 7, 8, 9, 10]

根據表中數據可見,當數組內元素個數大于 10 時:

  • JScript & Carakan 排序結果有誤
  • Chakra & JavaScriptCore 看起來沒有進行排序
  • SpiderMonkey 返回了預期的正確結果
  • V8 排序結果由正確轉為不正確

splice()

splice() 方法用新元素替換舊元素的方式來修改數組。它是一個常用的方法,復雜的數組操作場景通常都會有它的身影,特別是需要維持原數組引用時,就地刪除或者新增元素, splice() 是最適合的。

// 語法:
arr.splice(start,deleteCount[, item1[, item2[, …]]])
  • start 指定從哪一位開始修改內容。如果超過了數組長度,則從數組末尾開始添加內容;如果是負值,則其指定的索引位置等同于 length + start ( length 為數組的長度),表示從數組末尾開始的第 -start 位。
  • deleteCount 指定要刪除的元素個數,若等于 0 ,則不刪除。這種情況下,至少應該添加一位新元素,若大于 start 之后的元素總和,則 start 及之后的元素都將被刪除。
  • itemN 指定新增的元素,如果缺省,則該方法只刪除數組元素。
  • 返回值 由原數組中被刪除元素組成的數組,如果沒有刪除,則返回一個空數組。

下面來舉栗子說明:

var array = ["apple","boy"];
var splices = array.splice(1,1);
console.log(array);   // => ["apple"]
console.log(splices); // => ["boy"] ,可見是從數組下標為1的元素開始刪除,并且刪除一個元素,由于itemN缺省,故此時該方法只刪除元素
array = ["apple","boy"];
splices = array.splice(2,1,"cat");
console.log(array);   // => ["apple", "boy", "cat"]
console.log(splices); // => [], 可見由于start超過數組長度,此時從數組末尾開始添加元素,并且原數組不會發生刪除行為
array = ["apple","boy"];
splices = array.splice(-2,1,"cat");
console.log(array);   // => ["cat", "boy"]
console.log(splices); // => ["apple"], 可見當start為負值時,是從數組末尾開始的第-start位開始刪除,刪除一個元素,并且從此處插入了一個元素
array = ["apple","boy"];
splices = array.splice(-3,1,"cat");
console.log(array);   // => ["cat", "boy"]
console.log(splices); // => ["apple"], 可見即使-start超出數組長度,數組默認從首位開始刪除
array = ["apple","boy"];
splices = array.splice(0,3,"cat");
console.log(array);   // => ["cat"]
console.log(splices); // => ["apple", "boy"], 可見當deleteCount大于數組start之后的元素總和時,start及之后的元素都將被刪除

同上, splice() 一樣受益于鴨式辨型, 比如:

var o = {
    0:"apple",
    1:"boy",
    length:2
};
var splices = Array.prototype.splice.call(o,1,1);
console.log(o);       // => Object {0: "apple", length: 1}, 可見對象o刪除了一個屬性,并且length-1
console.log(splices); // => ["boy"]

注意:如果類數組對象沒有 length 屬性, splice() 將為該類數組對象添加 length 屬性,并初始化為 0 。

如果需要刪除數組中一個已存在的元素,可參考如下:

var array = ['a','b','c'];
array.splice(array.indexOf('b'),1);

unshift()

unshift() 方法用于在數組開始處插入一些元素(就像是棧底插入),并返回數組新的長度。

var array = ["red", "green", "blue"];
var length = array.unshift("yellow");
console.log(array);  // => ["yellow", "red", "green", "blue"]
console.log(length); // => 4

如果給 unshift() 方法傳入一個數組呢?

var array = ["red", "green", "blue"];
var length = array.unshift(["yellow"]);
console.log(array);  // => [["yellow"], "red", "green", "blue"]
console.log(length); // => 4, 可見數組也能成功插入

同上, unshift() 也受益于鴨式辨型,呈上栗子:

var o = {
    0:"red", 
    1:"green", 
    2:"blue",
    length:3
};
var length = Array.prototype.unshift.call(o,"gray");
console.log(o);      // => Object {0: "gray", 1: "red", 2: "green", 3: "blue", length: 4}
console.log(length); // => 4

注意:如果類數組對象不指定 length 屬性,則返回結果是這樣的 Object {0: "gray", 1: "green", 2: "blue", length: 1} , shift() 會認為數組長度為 0 ,此時將從對象下標為 0 的位置開始插入,相應位置屬性將被替換,此時初始化類數組對象的 length 屬性為插入元素個數。

copyWithin()

copyWithin() 方法基于ECMAScript 2015(ES6)規范,用于數組內元素之間的替換,即替換元素和被替換元素均是數組內的元素。

// 語法:
arr.copyWithin(target, start[, end = this.length])

taget 指定被替換元素的索引, start 指定替換元素起始的索引, end 可選,指的是替換元素結束位置的索引。如果 start 為負,則其指定的索引位置等同于 length + start , length 為數組的長度。 end 也是如此。

var array = [1,2,3,4,5]; 
var array2 = array.copyWithin(0,3);
console.log(array===array2,array2);     // => true [4, 5, 3, 4, 5]
var array = [1,2,3,4,5]; 
console.log(array.copyWithin(0,3,4));   // => [4, 2, 3, 4, 5]
var array = [1,2,3,4,5]; 
console.log(array.copyWithin(0,-2,-1)); // => [4, 2, 3, 4, 5]

同上, copyWithin() 一樣受益于鴨式辨型,例如:

var o = {0:1, 1:2, 2:3, 3:4, 4:5,length:5}
var o2 = Array.prototype.copyWithin.call(o,0,3);
console.log(o === o2,o2);

fill()

fill() 方法基于ECMAScript 2015(ES6)規范,它同樣用于數組元素替換,但與 copyWithin() 略有不同,它主要用于將數組指定區間內的元素替換為某個值。

// 語法:
arr.fill(value, start[, end = this.length])

value 指定被替換的值, start 指定替換元素起始的索引, end 可選,指的是替換元素結束位置的索引。如果 start 為負,則其指定的索引位置等同于 length + start , length 為數組的長度。 end 也是如此。

var array = [1,2,3,4,5];
var array2 = array.fill(10,0,3);
console.log(array === array2,array2); // => true [10, 10, 10, 4, 5], 可見數組區間[0,3]的元素全部替換為10

同上, fill() 一樣受益于鴨式辨型,例如:

var o = {
    0:1, 
    1:2, 
    2:3, 
    3:4, 
    4:5,
    length:5
}
var o2 = Array.prototype.fill.call(o,10,0,2);
console.log(o === o2, o2);

不會改變自身的方法

基于ES7,不會改變自身的方法一共有 9 個,分別為 concat() 、 join() 、 slice() 、 toString() 、 toLocateString() 、 indexOf() 、 lastIndexOf() 、未標準的 toSource() 以及ES7新增的方法 includes() 。

concat()

concat() 方法將傳入的數組或者元素與原數組合并,組成一個新的數組并返回。

var array = [1, 2, 3];
var array2 = array.concat(4,[5,6],[7,8,9]);
console.log(array2);  // => [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array);   // => [1, 2, 3], 可見原數組并未被修改

若 concat() 方法中不傳入參數,那么將基于原數組淺復制生成一個一模一樣的新數組(指向新的地址空間)。

var array = [{a: 1}];
var array3 = array.concat();
console.log(array3);                 // => [{a: 1}]
console.log(array3 === array);       // => false
console.log(array[0] === array3[0]); // => true,新舊數組第一個元素依舊共用一個同一個對象的引用

同上, concat() 一樣受益于鴨式辨型,但其效果可能達不到我們的期望,如下:

var o = {0:"a", 1:"b", 2:"c",length:3};
var o2 = Array.prototype.concat.call(o,'d',{3:'e',4:'f',length:2},['g','h','i']);
console.log(o2); // => [{0:"a", 1:"b", 2:"c", length:3}, 'd', {3:'e', 4:'f', length:2}, 'g', 'h', 'i']

可見,類數組對象合并后返回的是依然是數組,并不是我們期望的對象。

join()

join() 方法將數組中的所有元素連接成一個字符串。

var array = ['We', 'are', 'Chinese'];
console.log(array.join());     // => "We,are,Chinese"
console.log(array.join('+'));  // => "We+are+Chinese"
console.log(array.join(''));   // => "WeareChinese"

同上, join() 一樣受益于鴨式辨型,如下:

var o = {
    0:"We", 
    1:"are", 
    2:"Chinese", 
    length:3
};
console.log(Array.prototype.join.call(o,'+')); // => "We+are+Chinese"
console.log(Array.prototype.join.call('abc')); // => "a,b,c"

slice()

slice() 方法將數組中一部分元素淺復制存入新的數組對象,并且返回這個數組對象。

// 語法:
arr.slice([start[, end]])

參數 start 指定復制開始位置的索引, end 如果有值則表示復制結束位置的索引(不包括此位置)。

如果 start 的值為負數,假如數組長度為 length ,則表示從 length + start 的位置開始復制,此時參數 end 如果有值,只能是比 start 大的負數,否則將返回空數組。

slice() 方法參數為空時,同 concat() 方法一樣,都是淺復制生成一個新數組。

var array = ["one", "two", "three","four", "five"];
console.log(array.slice());     // => ["one", "two", "three","four", "five"]
console.log(array.slice(2,3));  // => ["three"]

淺復制是指當對象的被復制時,只是復制了對象的引用,指向的依然是同一個對象。下面來說明 slice() 為什么是淺復制。

var array = [{color:"yellow"}, 2, 3];
var array2 = array.slice(0,1);
console.log(array2);       // => [{color:"yellow"}]
array[0]["color"] = "blue";
console.log(array2);       // => [{color:"bule"}]

由于 slice() 是淺復制,復制到的對象只是一個引用,改變原數組 array 的值, array2 也隨之改變。同時,稍微利用下 slice() 方法第一個參數為負數時的特性,我們可以非常方便的拿到數組的最后一項元素,如下:

console.log([1,2,3].slice(-1)); // => [3]

同上, slice() 一樣受益于鴨式辨型。如下:

var o = {
    0:{"color":"yellow"}, 
    1:2, 
    2:3, 
    length:3
};
var o2 = Array.prototype.slice.call(o,0,1);
console.log(o2); // => [{color:"yellow"}]

鑒于IE9以下版本對于該方法支持性并不是很好,如需更好的支持低版本IE瀏覽器,請參考 polyfill

toString()

toString() 方法返回數組的字符串形式,該字符串由數組中的每個元素的 toString() 返回值經調用 join() 方法連接(由逗號隔開)組成。

var array = ['Jan', 'Feb', 'Mar', 'Apr'];
var str = array.toString();
console.log(str); // => Jan,Feb,Mar,Apr

當數組直接和字符串作連接操作時,將會自動調用其 toString() 方法。

var str = ['Jan', 'Feb', 'Mar', 'Apr'] + ',May';
console.log(str); // => "Jan,Feb,Mar,Apr,May"

// 下面我們來試試鴨式辨型
var o = {0:'Jan', 1:'Feb', 2:'Mar', length:3};
var o2 = Array.prototype.toString.call(o);
console.log(o2);                // => [object Object]
console.log(o.toString()==o2);  // => true

可見, Array.prototype.toString() 方法處理類數組對象時,跟類數組對象直接調用 Object.prototype.toString() 方法結果完全一致,說好的鴨式辨型呢?

根據ES5語義, toString() 方法是通用的,可被用于任何對象。如果對象有一個 join() 方法,將會被調用,其返回值將被返回,沒有則調用 Object.prototype.toString() ,為此,我們給o對象添加一個 join() 方法。如下:

var o = {
    0:'Jan', 
    1:'Feb', 
    2:'Mar', 
    length:3, 
    join:function(){
        return Array.prototype.join.call(this);
    }
};
console.log(Array.prototype.toString.call(o)); // => "Jan,Feb,Mar"

toLocaleString()

toLocaleString() 類似 toString() 的變型,該字符串由數組中的每個元素的 toLocaleString() 返回值經調用 join() 方法連接(由逗號隔開)組成。

// 語法:
arr.toLocaleString()

數組中的元素將調用各自的 toLocaleString() 方法:

來看個簡單示例:

var array= [{name:'zz'}, 123, "abc", new Date()];
var str = array.toLocaleString();
console.log(str); // => [object Object],123,abc,2016/1/5 下午1:06:23

其鴨式辨型的寫法也同 toString() 保持一致,如下:

var o = {
    0:123, 
    1:'abc', 
    2:new Date(), 
    length:3, 
    join:function(){
        return Array.prototype.join.call(this);
    }
};
console.log(Array.prototype.toLocaleString.call(o)); // => 123,abc,2016/1/5 下午1:16:50

indexOf()

indexOf() 方法用于查找元素在數組中第一次出現時的索引,如果沒有,則返回 -1 。

// 語法:
arr.indexOf(element, fromIndex=0)

element 為需要查找的元素。 fromIndex 為開始查找的位置,缺省默認為 0 。如果超出數組長度,則返回 -1 。如果為負值,假設數組長度為 length ,則從數組的第 length + fromIndex 項開始往數組末尾查找,如果 length + fromIndex < 0 則整個數組都會被查找。

indexOf() 使用嚴格相等(即使用 === 去匹配數組中的元素)。

var array = ['abc', 'def', 'ghi','123'];
console.log(array.indexOf('def'));    // => 1
console.log(array.indexOf('def',-1)); // => -1 此時表示從最后一個元素往后查找,因此查找失敗返回-1
console.log(array.indexOf('def',-4)); // => 1 由于4大于數組長度,此時將查找整個數組,因此返回1
console.log(array.indexOf(123));      // => -1, 由于是嚴格匹配,因此并不會匹配到字符串'123'

得益于鴨式辨型, indexOf() 可以處理類數組對象。如下:

var o = {
    0:'abc', 
    1:'def', 
    2:'ghi', 
    length:3
};
console.log(Array.prototype.indexOf.call(o,'ghi',-4)); // => 2

然而該方法并不支持IE9以下版本,如需更好的支持低版本IE瀏覽器(IE6~8), 請參考 Polyfill

lastIndexOf()

lastIndexOf() 方法用于查找元素在數組中最后一次出現時的索引,如果沒有,則返回 -1 。并且它是 indexOf() 的逆向查找,即從數組最后一個往前查找。

// 語法:
arr.lastIndexOf(element, fromIndex=length-1)

element 為需要查找的元素。 fromIndex 為開始查找的位置,缺省默認為數組長度 length - 1 。如果超出數組長度,由于是逆向查找,則查找整個數組。如果為負值,則從數組的第 length + fromIndex 項開始往數組開頭查找,如果 length + fromIndex < 0 則數組不會被查找。

同 indexOf() 一樣, lastIndexOf() 也是嚴格匹配數組元素。

舉例請參考 indexOf() ,不再詳述,兼容低版本IE瀏覽器(IE6~8),請參考 Polyfill

includes()

includes() 方法基于ECMAScript 2016(ES7)規范,它用來判斷當前數組是否包含某個指定的值,如果是,則返回 true ,否則返回 false 。

// 語法:
arr.includes(element, fromIndex=0)

element 為需要查找的元素。 fromIndex 表示從該索引位置開始查找 element ,缺省為 0 ,它是正向查找,即從索引處往數組末尾查找。

var array = [-0, 1, 2];
console.log(array.includes(+0));   // => true
console.log(array.includes(1));    // => true
console.log(array.includes(2,-4)); // => true

以上, includes() 似乎忽略了 -0 與 +0 的區別,這不是問題,因為JavaScript一直以來都是不區分 -0 和 +0 的。

你可能會問,既然有了 indexOf() 方法,為什么又造一個 includes() 方法, arr.indexOf(x) > -1 不就等于 arr.includes(x) ?看起來是的,幾乎所有的時候它們都等同,唯一的區別就是 includes() 能夠發現 NaN ,而 indexOf() 不能。

var array = [NaN];
console.log(array.includes(NaN));   // => true
console.log(array.indexOf(NaN)>-1); // => false

該方法同樣受益于鴨式辨型。如下:

var o = {
    0:'a', 
    1:'b', 
    2:'c', 
    length:3
};
var bool = Array.prototype.includes.call(o, 'a');
console.log(bool); // => true

該方法只有在Chrome 47、opera 34、Safari 9版本及其更高版本中才被實現。如需支持其他瀏覽器,請參考 Polyfill

toSource()

toSource() 方法是非標準的,該方法返回數組的源代碼,目前只有 Firefox 實現了它。

var array = ['a', 'b', 'c'];
console.log(array.toSource()); // => ["a", "b", "c"]

// 測試鴨式辨型
var o = {
    0:'a', 
    1:'b', 
    2:'c', 
    length:3
};
console.log(Array.prototype.toSource.call(o)); // => ["a","b","c"]

遍歷方法

基于ES6,不會改變自身的方法一共有 12 個,分別為 forEach() 、 every() 、 some() 、 filter() 、 map() 、 reduce() 、 reduceRight() 以及ES6新增的方法 entries() 、 find() 、 findIndex() 、 keys() 、 values() 。

forEach()

forEach() 方法指定數組的每項元素都執行一次傳入的函數,返回值為 undefined 。

// 語法:
arr.forEach(fn, thisArg)

fn 表示在數組每一項上執行的函數,接受三個參數:

  • value 當前正在被處理的元素的值
  • index 當前元素的數組索引
  • array 數組本身

thisArg 可選,用來當做 fn 函數內的 this 對象。

forEach() 將為數組中每一項執行一次 fn 函數,那些已刪除,新增或者從未賦值的項將被跳過(但不包括值為 undefined 的項)。

遍歷過程中, fn 會被傳入上述三個參數。

var array = [1, 3, 5];
var obj = {name:'cc'};
var sReturn = array.forEach(function(value, index, array){
    array[index] = value * value;
    console.log(this.name); // => cc被打印了三次
},obj);
console.log(array);         // => [1, 9, 25], 可見原數組改變了
console.log(sReturn);       // => undefined, 可見返回值為undefined

得益于鴨式辨型,雖然 forEach() 不能直接遍歷對象,但它可以通過 call() 方式遍歷類數組對象。如下:

var o = {
    0:1, 
    1:3, 
    2:5, 
    length:3
};
Array.prototype.forEach.call(o,function(value, index, obj){
    console.log(value,index,obj);
    obj[index] = value * value;
},o);
// => 1 0 Object {0: 1, 1: 3, 2: 5, length: 3}
// => 3 1 Object {0: 1, 1: 3, 2: 5, length: 3}
// => 5 2 Object {0: 1, 1: 9, 2: 5, length: 3}
console.log(o); // => Object {0: 1, 1: 9, 2: 25, length: 3}

參考前面的文章 詳解JS遍歷 中 forEach() 的講解,我們知道, forEach() 無法直接退出循環,只能使用 return 來達到 for 循環中 continue 的效果,并且 forEach() 不能在低版本IE(6~8)中使用,兼容寫法請參考 Polyfill

every()

every() 方法使用傳入的函數測試所有元素,只要其中有一個函數返回值為 false ,那么該方法的結果為 false ;如果全部返回 true ,那么該方法的結果才為 true 。因此 every() 方法存在如下規律:

  • 若需檢測數組中存在元素大于 100 (即 one > 100 ),那么我們需要在傳入的函數中構造 “false” 返回值 (即返回 item <= 100 ),同時整個方法結果為 false 才表示數組存在元素滿足條件;(簡單理解為:若是單項判斷,可用 one false ===> false )
  • 若需檢測數組中是否所有元素都大于 100 (即 all > 100 )那么我們需要在傳入的函數中構造 “true” 返回值 (即返回 item > 100 ),同時整個方法結果為 true 才表示數組所有元素均滿足條件。(簡單理解為:若是全部判斷,可用 all true ===> true )

語法同上述 forEach() ,具體還可以參考 詳解JS遍歷 中 every() 的講解。

以下是鴨式辨型的寫法:

var o = {0:10, 1:8, 2:25, length:3};
var bool = Array.prototype.every.call(o,function(value, index, obj){
    return value >= 8;
},o);
console.log(bool); // => true

every() 一樣不能在低版本IE(6~8)中使用,兼容寫法請參考 Polyfill

some()

some() 方法剛好同 every() 方法相反, some() 測試數組元素時,只要有一個函數返回值為 true ,則該方法返回 true ,若全部返回 false ,則該方法返回 false 。 some() 方法存在如下規律:

  • 若需檢測數組中存在元素大于 100 (即 one > 100 ),那么我們需要在傳入的函數中構造 “true” 返回值 (即返回 item > 100 ),同時整個方法結果為 true 才表示數組存在元素滿足條件;(簡單理解為:若是單項判斷,可用 one true ===> true )
  • 若需檢測數組中是否所有元素都大于 100 (即 all > 100 ),那么我們需要在傳入的函數中構造 “false” 返回值 (即返回 item <= 100 ),同時整個方法結果為 false 才表示數組所有元素均滿足條件。(簡單理解為:若是全部判斷,可用 all false ===> false )

你注意到沒有, some() 方法與 includes() 方法有著異曲同工之妙,他們都是探測數組中是否擁有滿足條件的元素,一旦找到,便返回 true 。多觀察和總結這種微妙的關聯關系,能夠幫助我們深入理解它們的原理。

some() 的鴨式辨型寫法可以參照 every() ,同樣它也不能在低版本IE(6~8)中使用,兼容寫法請參考 Polyfill

filter()

filter() 方法使用傳入的函數測試所有元素,并返回所有通過測試的元素組成的新數組。它就好比一個過濾器,篩掉不符合條件的元素。

var array = [18, 9, 10, 35, 80];
var array2 = array.filter(function(value, index, array){
    return value > 20;
});
console.log(array2); // => [35, 80]

filter() 一樣支持鴨式辨型,具體請參考 every() 方法鴨式辨型寫法。其在低版本IE(6~8)的兼容寫法請參考 Polyfill

map()

map() 方法遍歷數組,使用傳入函數處理每個元素,并返回函數的返回值組成的新數組。

// 語法:
arr.map(fn, thisArg)

參數介紹同 forEach() 方法的參數介紹。

具體用法請參考 詳解JS遍歷 中 map() 的講解。

map() 一樣支持鴨式辨型, 具體請參考 every() 方法鴨式辨型寫法。

其在低版本IE(6~8)的兼容寫法請參考 Polyfill

reduce()

reduce() 方法接收一個方法作為累加器,數組中的每個值(從左至右) 開始合并,最終為一個值。

// 語法:
arr.reduce(fn, initialValue)

fn 表示在數組每一項上執行的函數,接受四個參數:

  • previousValue 上一次調用回調返回的值,或者是提供的初始值
  • value 數組中當前被處理元素的值
  • index 當前元素在數組中的索引
  • array 數組自身

initialValue 指定第一次調用 fn 的第一個參數。

當 fn 第一次執行時:

  • 如果 initialValue 在調用 reduce() 時被提供,那么第一個 previousValue 將等于 initialValue ,此時 item 等于數組中的第一個值;
  • 如果 initialValue 未被提供,那么 previousVaule 等于數組中的第一個值, item 等于數組中的第二個值。此時如果數組為空,那么將拋出 TypeError
  • 如果數組僅有一個元素,并且沒有提供 initialValue ,或提供了 initialValue 但數組為空,那么 fn 不會被執行,數組的唯一值將被返回。

看個簡單示例:

var array = [1, 2, 3, 4];
var s = array.reduce(function(previousValue, value, index, array){
    return previousValue * value;
},1);
console.log(s); // => 24

// ES6寫法更加簡潔
array.reduce((p, v) => p * v); // => 24

以上回調被調用4次,每次的參數和返回見下表:

callback previousValue currentValue index array retrun value
第1次 1 1 1 [1, 2, 3, 4] 1
第2次 1 2 2 [1, 2, 3, 4] 2
第3次 2 3 3 [1, 2, 3, 4] 6
第4次 6 4 4 [1, 2, 3, 4] 24

reduce() 一樣支持鴨式辨型,具體請參考 every() 方法鴨式辨型寫法。

其在低版本IE(6~8)的兼容寫法請參考 Polyfill

reduceRight()

reduceRight() 方法接收一個方法作為累加器,數組中的每個值(從右至左)開始合并,最終為一個值。除了與 reduce() 執行方向相反外,其他完全與其一致,請參考上述 reduce() 方法介紹。

其在低版本IE(6~8)的兼容寫法請參考 Polyfill

entries()

entries() 方法基于ECMAScript 2015(ES6)規范,返回一個數組迭代器對象,該對象包含數組中每個索引的鍵值對。

var array = ["a", "b", "c"];
var iterator = array.entries();
console.log(iterator.next().value); // => [0, "a"]
console.log(iterator.next().value); // => [1, "b"]
console.log(iterator.next().value); // => [2, "c"]
console.log(iterator.next().value); // => undefined, 迭代器處于數組末尾時, 再迭代就會返回undefined

很明顯, entries() 也受益于鴨式辨型,如下:

var o = {
    0:"a", 
    1:"b", 
    2:"c", 
    length:3
};
var iterator = Array.prototype.entries.call(o);
console.log(iterator.next().value); // => [0, "a"]
console.log(iterator.next().value); // => [1, "b"]
console.log(iterator.next().value); // => [2, "c"]

find() & findIndex()

find() 方法基于ECMAScript 2015(ES6)規范,返回數組中第一個滿足條件的元素(如果有的話), 如果沒有,則返回 undefined 。

findIndex() 方法也基于ECMAScript 2015(ES6)規范,它返回數組中第一個滿足條件的元素的索引(如果有的話)否則返回 -1 。

// 語法:
arr.find(fn, thisArg),arr.findIndex(fn, thisArg)

我們發現它們的語法與 forEach() 等十分相似,其實不光語法, find() (或 findIndex() )在參數及其使用注意事項上,均與 forEach() 一致。因此此處將略去 find() (或 findIndex() )的參數介紹。下面我們來看個例子:

var array = [1, 3, 5, 7, 8, 9, 10];
function f(value, index, array){
    return value%2==0; // 返回偶數
}
function f2(value, index, array){
    return value > 20; // 返回大于20的數
}
console.log(array.find(f));       // => 8
console.log(array.find(f2));      // => undefined
console.log(array.findIndex(f));  // => 4
console.log(array.findIndex(f2)); // => -1

由于其鴨式辨型寫法也與 forEach() 方法一致,故此處略去。

keys()

keys() 方法基于ECMAScript 2015(ES6)規范,返回一個數組索引的迭代器。

var array = ["abc", "xyz"];
var iterator = array.keys();
console.log(iterator.next()); // => Object {value: 0, done: false}
console.log(iterator.next()); // => Object {value: 1, done: false}
console.log(iterator.next()); // => Object {value: undefined, done: false}

索引迭代器會包含那些沒有對應元素的索引,如下:

var array = ["abc", , "xyz"];
var sparseKeys = Object.keys(array);
var denseKeys = [...array.keys()];
console.log(sparseKeys); // => ["0", "2"]
console.log(denseKeys);  // => [0, 1, 2]

其鴨式辨型寫法請參考上述 entries() 方法。

前面我們用 Array.from() 生成一個從 0 到指定數字的新數組,利用 keys() 也很容易實現。

[...Array(10).keys()];     // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...new Array(10).keys()]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

由于 Array 的特性, new Array 和 Array 對單個數字的處理相同,因此以上兩種均可行。

values()

values() 方法基于ECMAScript 2015(ES6)規范,返回一個數組迭代器對象,該對象包含數組中每個索引的值。其用法基本與上述 entries() 方法一致。

遺憾的是,現在沒有瀏覽器實現了該方法,因此下面將就著看看吧。

var array = ["abc", "xyz"];
var iterator = array.values();
console.log(iterator.next().value); // => abc
console.log(iterator.next().value); // => xyz

采用在線的 Babel工具 ,可以打印出結果。

Symbol.iterator()

該方法基于ECMAScript 2015(ES6)規范,同 values() 方法功能相同。

var array = ["abc", "xyz"];
var iterator = array[Symbol.iterator]();
console.log(iterator.next().value); // => abc
console.log(iterator.next().value); // => xyz

其鴨式辨型寫法請參考上述 entries() 方法。

小結

以上, Array.prototype 的各方法基本介紹完畢,這些方法之間存在很多共性。比如:

  • 所有插入元素的方法, 比如 push() 、 unshift() ,一律返回數組新的長度;
  • 所有刪除元素的方法,比如 pop() 、 shift() 、 splice() 一律返回刪除的元素,或者返回刪除的多個元素組成的數組;
  • 部分遍歷方法,比如 forEach() 、 every() 、 some() 、 filter() 、 map() 、 find() 、 findIndex() ,它們都包含 function(value,index,array){} 和 thisArg 這樣兩個形參。

Array.prototype 的所有方法均具有鴨式辨型這種神奇的特性。它們不止可以用來處理數組對象,還可以處理類數組對象。

例如 JavaScript 中一個純天然的類數組對象字符串( String ),像 join() 方法(不改變當前對象自身)就完全適用,可惜的是 Array.prototype 中很多方法均會去試圖修改當前對象的 length 屬性,比如說 pop() 、 push() 、 shift() , unshift() 方法,操作 String 對象時,由于 String 對象的長度本身不可更改,這將導致拋出 TypeError 錯誤。

還記得么, Array.prototype 本身就是一個數組,并且它的長度為 0 。

后續章節我們將繼續探索Array的一些事情。感謝您的閱讀!

 

來自:http://louiszhai.github.io/2017/04/28/array/

 

 本文由用戶 RYPJulius 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!