2023 年 12 月 31 日

陣列方法

陣列提供了許多方法。為了讓事情更簡單,在本章中,它們被分組。

新增/移除項目

我們已經知道從頭或尾新增和移除項目的方法

  • arr.push(...items) – 將項目新增到尾端,
  • arr.pop() – 從尾端取出一個項目,
  • arr.shift() – 從頭端取出一個項目,
  • arr.unshift(...items) – 將項目新增到頭端。

以下是其他一些方法。

splice

如何從陣列中刪除元素?

陣列是物件,所以我們可以嘗試使用 delete

let arr = ["I", "go", "home"];

delete arr[1]; // remove "go"

alert( arr[1] ); // undefined

// now arr = ["I",  , "home"];
alert( arr.length ); // 3

元素已移除,但陣列仍有 3 個元素,我們可以看到 arr.length == 3

這是很自然的,因為 delete obj.key 會透過 key 移除值。這就是它所做的。對於物件來說很好。但對於陣列,我們通常希望其餘元素移動並佔用釋放的位置。我們期望現在有一個較短的陣列。

因此,應使用特殊方法。

arr.splice 方法是陣列的瑞士刀。它可以執行所有操作:插入、移除和取代元素。

語法為

arr.splice(start[, deleteCount, elem1, ..., elemN])

它從索引 start 開始修改 arr:移除 deleteCount 個元素,然後在它們的位置插入 elem1, ..., elemN。傳回已移除元素的陣列。

透過範例可以輕鬆掌握此方法。

讓我們從刪除開始

let arr = ["I", "study", "JavaScript"];

arr.splice(1, 1); // from index 1 remove 1 element

alert( arr ); // ["I", "JavaScript"]

簡單,對吧?從索引 1 開始,它移除 1 個元素。

在以下範例中,我們移除 3 個元素,並用其他兩個元素取代它們

let arr = ["I", "study", "JavaScript", "right", "now"];

// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // now ["Let's", "dance", "right", "now"]

在這裡,我們可以看到 splice 傳回已移除元素的陣列

let arr = ["I", "study", "JavaScript", "right", "now"];

// remove 2 first elements
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- array of removed elements

splice 方法也可以在不移除任何元素的情況下插入元素。為此,我們需要將 deleteCount 設定為 0

let arr = ["I", "study", "JavaScript"];

// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"
允許使用負索引

在這裡和其他陣列方法中,允許使用負索引。它們指定從陣列尾端的位置,如下所示

let arr = [1, 2, 5];

// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

slice

arr.slice 方法比外觀相似的 arr.splice 簡單得多。

語法為

arr.slice([start], [end])

它傳回一個新陣列,將所有項目從索引 start 複製到 end(不包含 end)。startend 兩個都可以是負數,在這種情況下,假設位置從陣列尾端開始。

它類似於字串方法 str.slice,但它不是建立子字串,而是建立子陣列。

例如

let arr = ["t", "e", "s", "t"];

alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)

alert( arr.slice(-2) ); // s,t (copy from -2 till the end)

我們也可以在沒有參數的情況下呼叫它:arr.slice() 會建立 arr 的副本。這通常用於取得副本,以進行進一步的轉換,而這些轉換不應影響原始陣列。

concat

arr.concat 方法會建立一個新陣列,其中包含來自其他陣列和附加項目的值。

語法為

arr.concat(arg1, arg2...)

它接受任何數量的參數,無論是陣列或值。

結果是一個新陣列,其中包含來自 arrarg1arg2 等的項目。

如果參數 argN 是陣列,則會複製其所有元素。否則,複製參數本身。

例如

let arr = [1, 2];

// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

通常,它只複製陣列中的元素。其他物件,即使看起來像陣列,也會整體加入

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]

…但如果類陣列物件有特殊 Symbol.isConcatSpreadable 屬性,則 concat 會將其視為陣列:加入其元素

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

反覆運算:forEach

arr.forEach 方法允許對陣列的每個元素執行函式。

語法

arr.forEach(function(item, index, array) {
  // ... do something with an item
});

例如,這會顯示陣列的每個元素

// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

而這段程式碼會更詳細說明其在目標陣列中的位置

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});

函式的結果(如果有回傳的話)會被丟棄並忽略。

在陣列中搜尋

現在讓我們介紹在陣列中搜尋的方法。

indexOf/lastIndexOf 和 includes

arr.indexOfarr.includes 方法有類似的語法,本質上與其字串對應項相同,但操作的是項目而不是字元

  • arr.indexOf(item, from) – 從索引 from 開始尋找 item,並回傳找到的索引,否則為 -1
  • arr.includes(item, from) – 從索引 from 開始尋找 item,如果找到則回傳 true

通常,這些方法只使用一個參數:要搜尋的 item。預設從頭開始搜尋。

例如

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

請注意,indexOf 使用嚴格相等 === 進行比較。因此,如果我們尋找 false,它只會找到 false,而不是零。

如果我們想檢查 item 是否存在於陣列中,不需要索引,則建議使用 arr.includes

arr.lastIndexOf 方法與 indexOf 相同,但從右到左尋找。

let fruits = ['Apple', 'Orange', 'Apple']

alert( fruits.indexOf('Apple') ); // 0 (first Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple)
includes 方法正確處理 NaN

includes 的一個小但值得注意的功能是它正確處理 NaN,這與 indexOf 不同

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0)
alert( arr.includes(NaN) );// true (correct)

這是因為 includes 是在很久之後才加入 JavaScript,並在內部使用較新的比較演算法。

find 和 findIndex/findLastIndex

想像我們有一個物件陣列。我們如何找到符合特定條件的物件?

這裡 arr.find(fn) 方法派上用場。

語法為

let result = arr.find(function(item, index, array) {
  // if true is returned, item is returned and iteration is stopped
  // for falsy scenario returns undefined
});

這個函式會對陣列中的元素逐一呼叫

  • item 是元素。
  • index 是它的索引。
  • array 是陣列本身。

如果它傳回 true,搜尋就會停止,並傳回 item。如果什麼都沒找到,就會傳回 undefined

例如,我們有一個使用者陣列,每個使用者都有 idname 欄位。我們來找出 id == 1 的使用者

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

在現實生活中,物件陣列是很常見的,所以 find 方法非常有用。

請注意,在這個範例中,我們提供給 find 的函式 item => item.id == 1 有一個參數。這是很典型的,這個函式的其他參數很少使用。

arr.findIndex 方法有相同的語法,但傳回找到元素的索引,而不是元素本身。如果什麼都沒找到,就會傳回 -1

arr.findLastIndex 方法類似於 findIndex,但從右到左搜尋,類似於 lastIndexOf

以下是一個範例

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"},
  {id: 4, name: "John"}
];

// Find the index of the first John
alert(users.findIndex(user => user.name == 'John')); // 0

// Find the index of the last John
alert(users.findLastIndex(user => user.name == 'John')); // 3

filter

find 方法會尋找單一(第一個)讓函式傳回 true 的元素。

如果可能有很多,我們可以使用 arr.filter(fn)

語法類似於 find,但 filter 會傳回一個包含所有符合元素的陣列

let results = arr.filter(function(item, index, array) {
  // if true item is pushed to results and the iteration continues
  // returns empty array if nothing found
});

例如

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

轉換陣列

讓我們繼續探討轉換和重新排列陣列的方法。

map

arr.map 方法是最有用且最常使用的其中一個方法。

它會對陣列中的每個元素呼叫函式,並傳回結果陣列。

語法為

let result = arr.map(function(item, index, array) {
  // returns the new value instead of item
});

例如,我們在這裡將每個元素轉換為其長度

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6

sort(fn)

呼叫 arr.sort() 會對陣列進行原地排序,改變其元素順序。

它也會傳回已排序的陣列,但傳回的值通常會被忽略,因為 arr 本身已修改。

例如

let arr = [ 1, 2, 15 ];

// the method reorders the content of arr
arr.sort();

alert( arr );  // 1, 15, 2

你注意到結果中有什麼奇怪的地方嗎?

順序變成 1, 15, 2。不正確。但為什麼?

預設會將項目當成字串排序。

基本上,所有元素都會轉換為字串進行比較。對於字串,會套用字典順序,而 "2" > "15" 確實成立。

若要使用我們自己的排序順序,我們需要提供一個函式作為 arr.sort() 的參數。

這個函式應該比較兩個任意值,並傳回

function compare(a, b) {
  if (a > b) return 1; // if the first value is greater than the second
  if (a == b) return 0; // if values are equal
  if (a < b) return -1; // if the first value is less than the second
}

例如,要當成數字排序

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

現在它會按照預期運作。

讓我們暫停一下,想想正在發生什麼事。arr 可以是任何東西的陣列,對吧?它可能包含數字、字串、物件或其他任何東西。我們有一組某些項目。要對它排序,我們需要一個排序函式,知道如何比較其元素。預設是字串順序。

arr.sort(fn) 方法實作了一種通用排序演算法。我們不需要關心它在內部如何運作(大部分時間都是最佳化的 快速排序Timsort)。它會遍歷陣列,使用提供的函式比較其元素並重新排列它們,我們只需要提供執行比較的 fn 即可。

順帶一提,如果我們想要知道哪些元素被比較,沒有什麼可以阻止我們警告它們

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
  return a - b;
});

演算法可能會在過程中將一個元素與多個其他元素比較,但它會盡量減少比較的次數。

比較函式可以傳回任何數字

實際上,比較函式只需要傳回正數表示「大於」和負數表示「小於」。

這允許編寫更簡短的函式

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15
箭頭函式最佳

還記得 箭頭函式 嗎?我們可以在這裡使用它們來進行更簡潔的排序

arr.sort( (a, b) => a - b );

這與上面較長的版本完全相同。

對字串使用 localeCompare

還記得 字串 比較演算法嗎?它預設會根據字母的代碼來比較字母。

對於許多字母表,最好使用 str.localeCompare 方法來正確排序字母,例如 Ö

例如,讓我們用德語對幾個國家進行排序

let countries = ['Österreich', 'Andorra', 'Vietnam'];

alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong)

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!)

反轉

方法 arr.reverse 會反轉 arr 中元素的順序。

例如

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

它也會在反轉後傳回陣列 arr

分割和合併

以下是一個真實生活中的情況。我們正在撰寫一個訊息應用程式,而使用者輸入了以逗號分隔的收件者清單:John, Pete, Mary。但對我們來說,一個名稱陣列會比一個單一字串來得更方便。要如何取得它?

方法 str.split(delim) 正好可以做到這一點。它會根據給定的分隔符號 delim 將字串分割成一個陣列。

在以下範例中,我們根據逗號後接空格來分割

let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // A message to Bilbo  (and other names)
}

split 方法有一個可選的第二個數字引數,也就是陣列長度的限制。如果提供了這個引數,則會忽略額外的元素。不過在實務上很少使用它

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);

alert(arr); // Bilbo, Gandalf
分割成字母

使用一個空的 s 來呼叫 split(s) 會將字串分割成一個字母陣列

let str = "test";

alert( str.split('') ); // t,e,s,t

呼叫 arr.join(glue) 會對 split 執行相反的動作。它會建立一個 arr 項目字串,並在它們之間加入 glue

例如

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';'); // glue the array into a string using ;

alert( str ); // Bilbo;Gandalf;Nazgul

reduce/reduceRight

當我們需要遍歷陣列時,可以使用 forEachforfor..of

當我們需要遍歷並傳回每個元素的資料時,可以使用 map

方法 arr.reducearr.reduceRight 也屬於此類,但稍微複雜一些。它們用於根據陣列計算單一值。

語法為

let value = arr.reduce(function(accumulator, item, index, array) {
  // ...
}, [initial]);

此函式會逐一套用至所有陣列元素,並將其結果「傳遞」至下一次呼叫。

引數

  • accumulator – 是前一次函式呼叫的結果,第一次等於 initial(如果提供了 initial)。
  • item – 是目前的陣列項目。
  • index – 是其位置。
  • array – 是陣列。

當套用函式時,前一次函式呼叫的結果會作為第一個引數傳遞至下一次呼叫。

因此,第一個引數基本上是累加器,用於儲存所有前一次執行結果的組合結果。最後,它會成為 reduce 的結果。

聽起來很複雜嗎?

最簡單的理解方式就是透過範例。

以下是在一行中取得陣列總和

let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

傳遞至 reduce 的函式只使用 2 個引數,這通常就足夠了。

讓我們看看發生了什麼事。

  1. 在第一次執行時,suminitial 值(reduce 的最後一個引數),等於 0,而 current 是第一個陣列元素,等於 1。因此函式結果為 1
  2. 在第二次執行時,sum = 1,我們將第二個陣列元素(2)加到其中並傳回。
  3. 在第三次執行時,sum = 3,我們再將一個元素加到其中,以此類推…

計算流程

或以表格形式表示,其中每一列代表對下一個陣列元素的函式呼叫

sum current result
第一次呼叫 0 1 1
第二次呼叫 1 2 3
第三次呼叫 3 3 6
第四次呼叫 6 4 10
第五次呼叫 10 5 15

在這裡,我們可以清楚地看到前一個呼叫的結果如何成為下一個呼叫的第一個參數。

我們也可以省略初始值

let arr = [1, 2, 3, 4, 5];

// removed initial value from reduce (no 0)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

結果是一樣的。這是因為如果沒有初始值,則 reduce 會將陣列的第一個元素作為初始值,並從第二個元素開始迭代。

計算表與上述相同,減去第一行。

但這種用法需要極度小心。如果陣列為空,則沒有初始值的 reduce 呼叫會產生錯誤。

以下是一個範例

let arr = [];

// Error: Reduce of empty array with no initial value
// if the initial value existed, reduce would return it for the empty arr.
arr.reduce((sum, current) => sum + current);

因此建議總是指定初始值。

方法 arr.reduceRight 執行相同的工作,但從右到左。

Array.isArray

陣列不形成單獨的語言類型。它們基於物件。

因此 typeof 無法幫助區分一般物件和陣列

alert(typeof {}); // object
alert(typeof []); // object (same)

…但陣列使用得如此頻繁,因此有一個特殊的方法:Array.isArray(value)。如果 value 是陣列,則傳回 true,否則傳回 false

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true

大多數方法支援「thisArg」

幾乎所有呼叫函式的陣列方法(例如 findfiltermap,但 sort 是個值得注意的例外)都接受一個額外的選用參數 thisArg

此參數未在上述各節中說明,因為它很少使用。但為了完整性,我們必須涵蓋它。

以下是這些方法的完整語法

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument

thisArg 參數的值會成為 functhis

例如,這裡我們使用 army 物件的方法作為篩選器,而 thisArg 傳遞上下文

let army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  }
};

let users = [
  {age: 16},
  {age: 20},
  {age: 23},
  {age: 30}
];

// find users, for who army.canJoin returns true
let soldiers = users.filter(army.canJoin, army);

alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23

如果在上述範例中,我們使用 users.filter(army.canJoin),則 army.canJoin 會被呼叫為獨立函式,其中 this=undefined,因此會立即導致錯誤。

可以將對 users.filter(army.canJoin, army) 的呼叫替換為 users.filter(user => army.canJoin(user)),這會執行相同的工作。後者使用得更頻繁,因為對大多數人來說,它更容易理解。

摘要

陣列方法的秘笈

  • 新增/移除元素

    • push(...items) – 在尾端新增項目
    • pop() – 從尾端移除項目
    • shift() – 從頭端移除項目
    • unshift(...items) – 在頭端新增項目
    • splice(pos, deleteCount, ...items) – 在索引 pos 處刪除 deleteCount 個元素並插入 items
    • slice(start, end) – 建立一個新陣列,將索引 startend(不包含)的元素複製到其中
    • concat(...items) – 傳回一個新陣列:複製目前陣列的所有成員並將 items 新增到其中。如果任一 items 是陣列,則會取出其元素
  • 在元素中搜尋

    • indexOf/lastIndexOf(item, pos) – 從位置 pos 開始尋找 item,並傳回索引或找不到時傳回 -1
    • includes(value) – 如果陣列有 value,則傳回 true,否則傳回 false
    • find/filter(func) – 透過函式篩選元素,傳回第一個/所有讓函式傳回 true 的值
    • findIndex 類似於 find,但傳回索引而非值
  • 在元素中反覆運算

    • forEach(func) – 對每個元素呼叫 func,不傳回任何內容
  • 轉換陣列

    • map(func) – 從對每個元素呼叫 func 的結果建立一個新陣列
    • sort(func) – 就地排序陣列,然後傳回陣列
    • reverse() – 就地反轉陣列,然後傳回陣列
    • split/join – 將字串轉換為陣列並反過來
    • reduce/reduceRight(func, initial) – 透過對每個元素呼叫 func 並在呼叫之間傳遞中間結果,計算陣列中的單一值
  • 此外

    • Array.isArray(value) 檢查 value 是否為陣列,如果是則傳回 true,否則傳回 false

請注意,方法 sortreversesplice 會修改陣列本身

這些方法是最常用的方法,它們涵蓋了 99% 的使用案例。但還有其他方法

  • arr.some(fn)/arr.every(fn) 檢查陣列

    函式 fn 會在陣列的每個元素上呼叫,類似於 map。如果任一/所有結果為 true,則傳回 true,否則傳回 false

    這些方法的行為類似於 ||&& 算子:如果 fn 傳回真值,arr.some() 會立即傳回 true 並停止反覆運算其餘項目;如果 fn 傳回假值,arr.every() 會立即傳回 false 並停止反覆運算其餘項目

    我們可以使用 every 來比較陣列

    function arraysEqual(arr1, arr2) {
      return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
    }
    
    alert( arraysEqual([1, 2], [1, 2])); // true
  • arr.fill(value, start, end) – 以重複的 value 填滿陣列,從索引 startend

  • arr.copyWithin(target, start, end) – 將其元素從位置 start 複製到位置 end,到 本身,在位置 target(覆寫現有元素)。

  • arr.flat(depth)/arr.flatMap(fn) 從多維陣列建立新的平面陣列。

完整清單,請參閱 手冊

乍看之下,方法似乎很多,很難記住。但實際上,這容易多了。

瀏覽秘笈,僅僅是為了知道它們。然後,解此章節的作業以練習,這樣您就有使用陣列方法的經驗。

之後,每當您需要對陣列執行某些操作,卻不知道如何執行時,請到這裡來,查看秘笈,並找到正確的方法。範例將協助您正確撰寫。很快地,您將自動記住這些方法,而無需特別費力。

作業

重要性:5

撰寫函式 camelize(str),將以連字號分隔的字詞(例如「my-short-string」)轉換為使用駝峰式大小寫的「myShortString」。

也就是:移除所有連字號,連字號後的每個字詞改為大寫。

範例

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

提示:使用 split 將字串分割為陣列,轉換它,並使用 join 重新組合。

開啟沙盒,查看測試。

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // capitalizes first letters of all array items except the first one
      // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

開啟沙盒中的解答,查看測試。

重要性:4

撰寫函式 filterRange(arr, a, b),它取得陣列 arr,尋找值大於或等於 a 且小於或等於 b 的元素,並傳回結果陣列。

此函式不應修改陣列。它應傳回新的陣列。

例如

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

開啟沙盒,查看測試。

function filterRange(arr, a, b) {
  // added brackets around the expression for better readability
  return arr.filter(item => (a <= item && item <= b));
}

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1 (matching values)

alert( arr ); // 5,3,8,1 (not modified)

開啟沙盒中的解答,查看測試。

重要性:4

撰寫一個函式 filterRangeInPlace(arr, a, b),取得陣列 arr 並移除所有介於 ab 之間的值。測試為:a ≤ arr[i] ≤ b

這個函式應僅修改陣列。它不應回傳任何內容。

例如

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

alert( arr ); // [3, 1]

開啟沙盒,查看測試。

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // remove if outside of the interval
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

alert( arr ); // [3, 1]

開啟沙盒中的解答,查看測試。

重要性:4
let arr = [5, 2, 1, -10, 8];

// ... your code to sort it in decreasing order

alert( arr ); // 8, 5, 2, 1, -10
let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );
重要性:5

我們有一個字串陣列 arr。我們想要取得它的排序副本,但保持 arr 不變。

建立一個函式 copySorted(arr) 來回傳此副本。

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS (no changes)

我們可以使用 slice() 來製作副本並對其執行排序

function copySorted(arr) {
  return arr.slice().sort();
}

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
重要性:5

建立一個建構函式 Calculator,用來建立「可延伸」的計算器物件。

這個任務包含兩個部分。

  1. 首先,實作方法 calculate(str),它會取得一個字串,例如 "1 + 2",格式為「數字 運算子 數字」(以空格分隔),並回傳結果。應了解加號 + 和減號 -

    使用範例

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  2. 然後新增方法 addMethod(name, func),用來教導計算器新的運算。它會取得運算子 name 和實作它的兩個引數函式 func(a,b)

    例如,我們來新增乘法 *、除法 / 和次方 **

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8
  • 此任務中沒有括號或複雜的運算式。
  • 數字和運算子以恰好一個空格分隔。
  • 如果你想新增,可以進行錯誤處理。

開啟沙盒,查看測試。

  • 請注意方法的儲存方式。它們會直接新增到 this.methods 屬性。
  • 所有測試和數字轉換都在 calculate 方法中完成。未來可能會延伸支援更複雜的運算式。
function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2];

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  };

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

開啟沙盒中的解答,查看測試。

重要性:5

你有一個 user 物件陣列,每個物件都有 user.name。撰寫程式碼將它轉換成名稱陣列。

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = /* ... your code */

alert( names ); // John, Pete, Mary
let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = users.map(item => item.name);

alert( names ); // John, Pete, Mary
重要性:5

你有一個 user 物件陣列,每個物件都有 namesurnameid

撰寫程式碼從中建立另一個陣列,其中包含 idfullName 的物件,其中 fullName 是從 namesurname 產生的。

例如

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = /* ... your code ... */

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith

因此,你實際上需要將一個物件陣列對應到另一個物件陣列。請嘗試在此處使用 =>。這裡有一個小技巧。

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // John Smith

請注意,在箭頭函式中,我們需要使用額外的括號。

我們無法這樣寫

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

如我們所知,有兩個箭頭函式:沒有主體的 value => expr 和有主體的 value => {...}

在這裡,JavaScript 會將 { 視為函式主體的開頭,而不是物件的開頭。解決方法是將它們包在「一般」括號中

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

現在沒問題了。

重要性:5

撰寫函式 sortByAge(users),它會取得一個物件陣列,其中包含 age 屬性,並依 age 對它們排序。

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// now: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
function sortByAge(arr) {
  arr.sort((a, b) => a.age - b.age);
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// now sorted is: [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete
重要性:3

撰寫函式 shuffle(array),它會洗牌(隨機重新排列)陣列的元素。

執行 shuffle 多次可能會導致元素順序不同。例如

let arr = [1, 2, 3];

shuffle(arr);
// arr = [3, 2, 1]

shuffle(arr);
// arr = [2, 1, 3]

shuffle(arr);
// arr = [3, 1, 2]
// ...

所有元素順序應具有相等的機率。例如,[1,2,3] 可以重新排列為 [1,2,3][1,3,2][3,1,2] 等,每個案例的機率相等。

簡單的解決方案可能是

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

let arr = [1, 2, 3];
shuffle(arr);
alert(arr);

這在某種程度上可行,因為 Math.random() - 0.5 是可能為正或負的隨機數,因此排序函式會隨機重新排列元素。

但是,由於排序函式並非設計為這樣使用,因此並非所有排列都具有相同的機率。

例如,考慮以下程式碼。它執行 shuffle 1000000 次,並計算所有可能結果的出現次數

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

// counts of appearances for all possible permutations
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

範例結果(取決於 JS 引擎)

123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223

我們可以清楚地看到偏差:123213 出現的頻率遠高於其他數字。

程式碼的結果可能會因 JavaScript 引擎而異,但我們已經可以看出這種方法不可靠。

為什麼它不起作用?一般來說,sort 是一個「黑盒子」:我們將陣列和比較函式丟入其中,並期望陣列會被排序。但由於比較的完全隨機性,黑盒子會發瘋,而它發瘋的確切方式取決於引擎之間不同的具體實作。

還有其他方法可以完成這項任務。例如,有一個很棒的演算法稱為 Fisher-Yates 洗牌。這個構想是反向遍歷陣列,並將每個元素與它前面的隨機元素交換

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i

    // swap elements array[i] and array[j]
    // we use "destructuring assignment" syntax to achieve that
    // you'll find more details about that syntax in later chapters
    // same can be written as:
    // let t = array[i]; array[i] = array[j]; array[j] = t
    [array[i], array[j]] = [array[j], array[i]];
  }
}

讓我們用相同的方式測試它

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// counts of appearances for all possible permutations
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

範例輸出

123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316

現在看起來不錯:所有排列都以相同的機率出現。

此外,在效能方面,Fisher-Yates 演算法好得多,沒有「排序」的開銷。

重要性:4

撰寫函式 getAverageAge(users),它會取得一個包含屬性 age 的物件陣列,並傳回平均年齡。

平均值的公式為 (age1 + age2 + ... + ageN) / N

例如

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
function getAverageAge(users) {
  return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // 28
重要性:4

假設 arr 是陣列。

建立函式 unique(arr),它應該傳回包含 arr 中唯一項目的陣列。

例如

function unique(arr) {
  /* your code */
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

開啟沙盒,查看測試。

讓我們遍歷陣列項目

  • 對於每個項目,我們將檢查結果陣列是否已包含該項目。
  • 如果是,則忽略,否則新增至結果。
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

此程式碼有效,但其中存在潛在的效能問題。

方法 result.includes(str) 在內部會遍歷陣列 result,並將每個元素與 str 進行比較以尋找匹配項。

因此,如果 result 中有 100 個元素,且沒有任何元素與 str 匹配,則它將遍歷整個 result 並執行正好 100 次比較。如果 result 很大的話,例如 10000,則會進行 10000 次比較。

這本身不是問題,因為 JavaScript 引擎非常快,所以遍歷 10000 個陣列只是微秒的問題。

但是我們會對 arr 的每個元素執行此類測試,在 for 迴圈中。

因此,如果 arr.length10000,我們將有大約 10000*10000 = 1 億次比較。這很多。

因此,此解決方案僅適用於小型陣列。

在章節 Map 和 Set 中,我們將看到如何最佳化它。

開啟沙盒中的解答,查看測試。

重要性:4

假設我們收到一個陣列的使用者,格式為 {id:..., name:..., age:... }

建立一個函式 groupById(arr),從陣列中建立一個物件,其中 id 為鍵,陣列項目為值。

例如

let users = [
  {id: 'john', name: "John Smith", age: 20},
  {id: 'ann', name: "Ann Smith", age: 24},
  {id: 'pete', name: "Pete Peterson", age: 31},
];

let usersById = groupById(users);

/*
// after the call we should have:

usersById = {
  john: {id: 'john', name: "John Smith", age: 20},
  ann: {id: 'ann', name: "Ann Smith", age: 24},
  pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/

當使用伺服器資料時,此類函式非常方便。

在這個任務中,我們假設 id 是唯一的。沒有兩個陣列項目具有相同的 id

請在解答中使用陣列 .reduce 方法。

開啟沙盒,查看測試。

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

開啟沙盒中的解答,查看測試。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有改善建議,請 提交 GitHub 議題 或提出 pull request,不要留言。
  • 如果你無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行,請將它們包覆在 <pre> 標籤中,對於超過 10 行,請使用沙盒 (plnkrjsbincodepen…)