Javascript 數組方法

2023-02-17 10:44 更新

數組提供的方法有很多。為了方便起見,在本章中,我們將按組講解。

添加/移除數組元素

我們已經學了從數組的首端或尾端添加和刪除元素的方法:

  • ?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); // 從索引 1 開始刪除 1 個元素

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

簡單,對吧?從索引 1 開始刪除 1 個元素。(譯注:當只填寫了 splice 的 start 參數時,將刪除從索引 start 開始的所有數組項)

在下一個例子中,我們刪除了 3 個元素,并用另外兩個元素替換它們:

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

// 刪除數組的前三項,并使用其他內容代替它們
arr.splice(0, 3, "Let's", "dance");

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

在這里我們可以看到 splice 返回了被刪除的元素所組成的數組:

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

// 刪除前兩個元素
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- 被從數組中刪除了的元素

我們可以將 deleteCount 設置為 0,splice 方法就能夠插入元素而不用刪除任何元素:

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

// 從索引 2 開始
// 刪除 0 個元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"

允許負向索引

在這里和其他數組方法中,負向索引都是被允許的。它們從數組末尾計算位置,如下所示:

let arr = [1, 2, 5];

// 從索引 -1(尾端前一位)
// 刪除 0 個元素,
// 然后插入 3 和 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)的數組項復制到一個新的數組。start 和 end 都可以是負數,在這種情況下,從末尾計算索引。

它和字符串的 str.slice 方法有點像,就是把子字符串替換成子數組。

例如:

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

alert( arr.slice(1, 3) ); // e,s(復制從位置 1 到位置 3 的元素)

alert( arr.slice(-2) ); // s,t(復制從位置 -2 到尾端的元素)

我們也可以不帶參數地調用它:arr.slice() 會創(chuàng)建一個 arr 的副本。其通常用于獲取副本,以進行不影響原始數組的進一步轉換。

concat

arr.concat 創(chuàng)建一個新數組,其中包含來自于其他數組和其他項的值。

語法:

arr.concat(arg1, arg2...)

它接受任意數量的參數 —— 數組或值都可以。

結果是一個包含來自于 arr,然后是 arg1,arg2 的元素的新數組。

如果參數 argN 是一個數組,那么其中的所有元素都會被復制。否則,將復制參數本身。

例如:

let arr = [1, 2];

// 從 arr 和 [3,4] 創(chuàng)建一個新數組
alert( arr.concat([3, 4]) ); // 1,2,3,4

// 從 arr、[3,4] 和 [5,6] 創(chuàng)建一個新數組
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// 從 arr、[3,4]、5 和 6 創(chuàng)建一個新數組
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 item
});

例如,下面這個程序顯示了數組的每個元素:

// 對每個元素調用 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.indexOf 和 arr.includes 方法語法相似,并且作用基本上也與字符串的方法相同,只不過這里是對數組元素而不是字符進行操作:

  • ?arr.indexOf(item, from)? —— 從索引 ?from ?開始搜索 ?item?,如果找到則返回索引,否則返回 ?-1?。
  • ?arr.includes(item, from)? —— 從索引 ?from ?開始搜索 ?item?,如果找到則返回 ?true?(譯注:如果沒找到,則返回 ?false?)。

通常使用這些方法時只會傳入一個參數:傳入 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 和 includes 使用嚴格相等 === 進行比較。所以,如果我們搜索 false,它會準確找到 false 而不是數字 0。

如果我們想檢查數組中是否包含元素 item,并且不需要知道其確切的索引,那么 arr.includes 是首選。

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

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

alert( fruits.indexOf('Apple') ); // 0(第一個 Apple)
alert( fruits.lastIndexOf('Apple') ); // 2(最后一個 Apple)

方法 ?includes?可以正確的處理 ?NaN?

方法 includes 的一個次要但值得注意的特性是,它可以正確處理 NaN,這與 indexOf 不同:

const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1(錯,應該為 0)
alert( arr.includes(NaN) );// true(正確)

這是因為 includes 是在比較晚的時候才被添加到 JavaScript 中的,并且在內部使用了更新了的比較算法。

find 和 findIndex/findLastIndex

想象一下,我們有一個對象數組。我們如何找到具有特定條件的對象?

這時可以用 arr.find 方法。

語法如下:

let result = arr.find(function(item, index, array) {
  // 如果返回 true,則返回 item 并停止迭代
  // 對于假值(falsy)的情況,則返回 undefined
});

依次對數組中的每個元素調用該函數:

  • ?item ?是元素。
  • ?index ?是它的索引。
  • ?array ?是數組本身。

如果它返回 true,則搜索停止,并返回 item。如果沒有搜索到,則返回 undefined。

例如,我們有一個存儲用戶的數組,每個用戶都有 id 和 name 字段。讓我們找到 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。這很典型,并且 find 方法的其他參數很少使用。

arr.findIndex 方法(與 arr.find)具有相同的語法,但它返回找到的元素的索引,而不是元素本身。如果沒找到,則返回 -1。

arr.findLastIndex 方法類似于 findIndex,但從右向左搜索,類似于 lastIndexOf。

這是一個例子:

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

// 尋找第一個 John 的索引
alert(users.findIndex(user => user.name == 'John')); // 0

// 尋找最后一個 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) {
  // 如果 true item 被 push 到 results,迭代繼續(xù)
  // 如果什么都沒找到,則返回空數組
});

例如:

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

// 返回前兩個用戶的數組
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

轉換數組

讓我們繼續(xù)學習進行數組轉換和重新排序的方法。

map

arr.map 方法是最有用和經常使用的方法之一。

它對數組的每個元素都調用函數,并返回結果數組。

語法:

let result = arr.map(function(item, index, array) {
  // 返回新值而不是當前元素
})

例如,在這里我們將每個元素轉換為它的字符串長度:

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

sort(fn)

arr.sort 方法對數組進行 原位(in-place) 排序,更改元素的順序。(譯注:原位是指在此數組內,而非生成一個新數組。)

它還返回排序后的數組,但是返回值通常會被忽略,因為修改了 ?arr本身。

語法:

let arr = [ 1, 2, 15 ];

// 該方法重新排列 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 (a == b) return 0; // 如果兩個值相等
  if (a < b) return -1; // 如果第一個值比第二個值小
}

例如,按數字進行排序:

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

現在結果符合預期了。

我們思考一下這兒發(fā)生了什么。arr 可以是由任何內容組成的數組,對嗎?它可能包含數字、字符串、對象或其他任何內容。我們有一組 一些元素。要對其進行排序,我們需要一個 排序函數 來確認如何比較這些元素。默認是按字符串進行排序的。

arr.sort(fn) 方法實現了通用的排序算法。我們不需要關心它的內部工作原理(大多數情況下都是經過 快速排序 或 Timsort 算法優(yōu)化的)。它將遍歷數組,使用提供的函數比較其元素并對其重新排序,我們所需要的就是提供執(zhí)行比較的函數 fn。

順便說一句,如果我們想知道要比較哪些元素 —— 那么什么都不會阻止 alert 它們:

[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? for strings

你記得 字符串比較 算法嗎?默認情況下,它通過字母的代碼比較字母。

對于許多字母,最好使用 str.localeCompare 方法正確地對字母進行排序,例如 ?。

例如,讓我們用德語對幾個國家/地區(qū)進行排序:

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

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

alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,?sterreich,Vietnam(對的?。?/code>

reverse

arr.reverse 方法用于顛倒 ?arr? 中元素的順序。

例如:

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

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

它也會返回顛倒后的數組 arr。

split 和 join

舉一個現實生活場景的例子。我們正在編寫一個消息應用程序,并且該人員輸入以逗號分隔的接收者列表: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(和其他名字)
}

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 相反。它會在它們之間創(chuàng)建一串由 glue 粘合的 arr 項。

例如:

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

let str = arr.join(';'); // 使用分號 ; 將數組粘合成字符串

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

reduce/reduceRight

當我們需要遍歷一個數組時 —— 我們可以使用 forEach,for 或 for..of

當我們需要遍歷并返回每個元素的數據時 —— 我們可以使用 map。

arr.reduce 方法和 arr.reduceRight 方法和上面的種類差不多,但稍微復雜一點。它們用于根據數組計算單個值。

語法是:

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

該函數一個接一個地應用于所有數組元素,并將其結果“搬運(carry on)”到下一個調用。

參數:

  • ?accumulator ?—— 是上一個函數調用的結果,第一次等于 ?initial?(如果提供了 ?initial ?的話)。
  • ?item ?—— 當前的數組元素。
  • ?index ?—— 當前索引。
  • ?arr ?—— 數組本身。

應用函數時,上一個函數調用的結果將作為第一個參數傳遞給下一個函數。

因此,第一個參數本質上是累加器,用于存儲所有先前執(zhí)行的組合結果。最后,它成為 ?reduce? 的結果。

聽起來復雜嗎?

掌握這個知識點的最簡單的方法就是通過示例。

在這里,我們通過一行代碼得到一個數組的總和:

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

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

alert(result); // 15

傳遞給 reduce 的函數僅使用了 2 個參數,通常這就足夠了。

讓我們看看細節(jié),到底發(fā)生了什么。

  1. 在第一次運行時,?sum ?的值為初始值 ?initial?(?reduce ?的最后一個參數),等于 0,?current ?是第一個數組元素,等于 ?1?。所以函數運行的結果是 ?1?。
  2. 在第二次運行時,?sum = 1?,我們將第二個數組元素(?2?)與其相加并返回。
  3. 在第三次運行中,?sum = 3?,我們繼續(xù)把下一個元素與其相加,以此類推……

計算流程:


或者以表格的形式表示,每一行代表的是對下一個數組元素的函數調用:

sum current result
第 1 次調用 0 1 1
第 2 次調用 1 2 3
第 3 次調用 3 3 6
第 4 次調用 6 4 10
第 5 次調用 10 5 15

在這里,我們可以清楚地看到上一個調用的結果如何成為下一個調用的第一個參數。

我們也可以省略初始值:

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

// 刪除 reduce 的初始值(沒有 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
// 如果初始值存在,則 reduce 將為空 arr 返回它(即這個初始值)。
arr.reduce((sum, current) => sum + current);

所以建議始終指定初始值。

arr.reduceRight 和 arr.reduce 方法的功能一樣,只是遍歷為從右到左。

Array.isArray

數組是基于對象的,不構成單獨的語言類型。

所以 ?typeof ?不能幫助從數組中區(qū)分出普通對象:

alert(typeof {}); // object
alert(typeof []); // object(相同)

……但是數組經常被使用,因此有一種特殊的方法用于判斷:Array.isArray(value)。如果 value 是一個數組,則返回 true;否則返回 false。

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

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

大多數方法都支持 “thisArg”

幾乎所有調用函數的數組方法 —— 比如 find,filter,map,除了 sort 是一個特例,都接受一個可選的附加參數 thisArg。

上面的部分中沒有解釋該參數,因為該參數很少使用。但是為了完整性,我們需要講講它。

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

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg 是可選的最后一個參數

thisArg 參數的值在 func 中變?yōu)?nbsp;this

例如,在這里我們使用 army 對象方法作為過濾器,thisArg 用于傳遞上下文(passes the context):

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}
];

// 找到 army.canJoin 返回 true 的 user
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(user => army.canJoin(user)) 替換對 users.filter(army.canJoin, army) 的調用。前者的使用頻率更高,因為對于大多數人來說,它更容易理解。

總結

數組方法備忘單:

  • 添加/刪除元素:
    • ?push(...items)? —— 向尾端添加元素,
    • ?pop()? —— 從尾端提取一個元素,
    • ?shift()? —— 從首端提取一個元素,
    • ?unshift(...items)? —— 向首端添加元素,
    • ?splice(pos, deleteCount, ...items)? —— 從 ?pos? 開始刪除 ?deleteCount? 個元素,并插入 ?items?。
    • ?slice(start, end)? —— 創(chuàng)建一個新數組,將從索引 ?start? 到索引 ?end?(但不包括 ?end?)的元素復制進去。
    • ?concat(...items)? —— 返回一個新數組:復制當前數組的所有元素,并向其中添加 ?items?。如果 ?items? 中的任意一項是一個數組,那么就取其元素。
  • 搜索元素:
    • ?indexOf/lastIndexOf(item, pos)? —— 從索引 ?pos? 開始搜索 ?item?,搜索到則返回該項的索引,否則返回 ?-1?。
    • ?includes(value)? —— 如果數組有 ?value?,則返回 ?true?,否則返回 ?false?。
    • ?find/filter(func)? —— 通過 ?func? 過濾元素,返回使 ?func? 返回 ?true? 的第一個值/所有值。
    • ?findIndex? 和 ?find? 類似,但返回索引而不是值。
  • 遍歷元素:
    • ?forEach(func)? —— 對每個元素都調用 ?func?,不返回任何內容。
  • 轉換數組:
    • ?map(func)? —— 根據對每個元素調用 ?func? 的結果創(chuàng)建一個新數組。
    • ?sort(func)? —— 對數組進行原位(in-place)排序,然后返回它。
    • ?reverse()? —— 原位(in-place)反轉數組,然后返回它。
    • ?split/join? —— 將字符串轉換為數組并返回。
    • ?reduce/reduceRight(func, initial)? —— 通過對每個元素調用 ?func? 計算數組上的單個值,并在調用之間傳遞中間結果。
  • 其他:
    • ?Array.isArray(value)? 檢查 ?value? 是否是一個數組,如果是則返回 ?true?,否則返回 ?false?。

請注意,sort,reverse 和 splice 方法修改的是數組本身。

這些是最常用的方法,它們覆蓋 99% 的用例。但是還有其他幾個:

  • arr.some(fn)/arr.every(fn) 檢查數組。
  • 與 map 類似,對數組的每個元素調用函數 fn。如果任何/所有結果為 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

有關完整列表,請參閱 手冊。

乍看起來,似乎有很多方法,很難記住。但實際上這比看起來要容易得多。

瀏覽這個備忘單,以了解這些方法。然后解決本章中的習題來進行練習,以便讓你有數組方法的使用經驗。

然后,每當你需要對數組進行某些操作,而又不知道怎么做的時候,請回到這兒,查看這個備忘單,然后找到正確的方法。示例將幫助你正確編寫它。用不了多久,你就自然而然地記住這些方法了,根本不需要你死記硬背。

任務


將 border-left-width 轉換成 borderLeftWidth

重要程度: 5

編寫函數 camelize(str) 將諸如 “my-short-string” 之類的由短劃線分隔的單詞變成駱駝式的 “myShortString”。

即:刪除所有短橫線,并將短橫線后的每一個單詞的首字母變?yōu)榇髮憽?/p>

示例:

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(匹配值)

alert( arr ); // 5,3,8,1(未修改)

解決方案

function filterRange(arr, a, b) {
  // 在表達式周圍添加了括號,以提高可讀性
  return arr.filter(item => (a <= item && item <= b));
}

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

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

alert( filtered ); // 3,1(匹配的值)

alert( arr ); // 5,3,8,1(未經改動的數組中的值)

原位(in place)過濾范圍

重要程度: 4

寫一個函數 filterRangeInPlace(arr, a, b),該函數獲取一個數組 arr,并刪除其中介于 a 和 b 區(qū)間以外的所有值。檢查:a ≤ arr[i] ≤ b。

該函數應該只修改數組。它不應該返回任何東西。

例如:

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

filterRangeInPlace(arr, 1, 4); // 刪除了范圍在 1 到 4 之外的所有值

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

解決方案

function filterRangeInPlace(arr, a, b) {

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

    // 如果超出范圍,則刪除
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

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

filterRangeInPlace(arr, 1, 4); // 刪除 1 到 4 范圍之外的值

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

降序排列

重要程度: 4

let arr = [5, 2, 1, -10, 8];

// ……你的代碼以降序對其進行排序

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 不變。

創(chuàng)建一個函數 copySorted(arr) 返回這樣一個副本。

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

let sorted = copySorted(arr);

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

解決方案

我們可以使用 slice() 來創(chuàng)建一個副本并對其進行排序:

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

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

let sorted = copySorted(arr);

alert( sorted );
alert( arr );

創(chuàng)建一個可擴展的 calculator

重要程度: 5

創(chuàng)建一個構造函數 Calculator,以創(chuàng)建“可擴展”的 calculator 對象。

該任務由兩部分組成。

  1. 首先,實現 ?calculate(str)? 方法,該方法接受像 ?"1 + 2"? 這樣格式為“數字 運算符 數字”(以空格分隔)的字符串,并返回結果。該方法需要能夠理解加號 ?+? 和減號 ?-?。
  2. 用法示例:

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10
  3. 然后添加方法 ?addMethod(name, func)?,該方法教 calculator 進行新操作。它需要運算符 ?name? 和實現它的雙參數函數 ?func(a,b)?。
  4. 例如,我們添加乘法 ?*?,除法 ?/? 和求冪 ?**?:

    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;
  };
}

映射到 names

重要程度: 5

你有一個 user 對象數組,每個對象都有 user.name。編寫將其轉換為 names 數組的代碼。

例如:

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 對象數組,每個對象都有 namesurname 和 id。

編寫代碼以該數組為基礎,創(chuàng)建另一個具有 id 和 fullName 的對象數組,其中 fullName 由 name 和 surname 生成。

例如:

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);

// 排序后的數組為:[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);
}

因為 sort() 方法的語法為 arr.sort([compareFunction]),如果沒有指明 compareFunction,那么元素會被按照轉換為的字符串的諸個字符的 Unicode 編碼進行排序,如果指明了 compareFunction,那么數組會按照調用該函數的返回值排序。即 a 和 b 是兩個將要被比較的元素:

  • 如果 ?compareFunction(a, b)? 小于 ?0?,那么 ?a? 會被排列到 ?b? 之前;
  • 如果 ?compareFunction(a, b)? 等于 ?0?,那么 ?a? 和 ?b? 的相對位置不變。備注:ECMAScript 標準并不保證這一行為,而且也不是所有瀏覽器都會遵守(例如 Mozilla 在 2003 年之前的版本);
  • 如果 ?compareFunction(a, b)? 大于 ?0?,那么 ?b? 會被排列到 ?a? 之前。

因此,升序排列的函數可以簡寫為:(a, b) => a.age - b.age。


隨機排列數組

重要程度: 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 是一個可能是正數或負數的隨機數,因此排序函數會隨機地對數組中的元素進行重新排序。

但是,由于排序函數并非旨在以這種方式使用,因此并非所有的排列都具有相同的概率。

例如,請考慮下面的代碼。它運行 100 萬次 shuffle 并計算所有可能結果的出現次數:

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

// 所有可能排列的出現次數
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('')]++;
}

// 顯示所有可能排列的出現次數
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

示例結果(取決于 Javascript 引擎):

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

我們可以清楚地看到這種傾斜:123 和 213 的出現頻率比其他情況高得多。

使用不同的 JavaScript 引擎運行這個示例代碼得到的結果可能會有所不同,但是我們已經可以看到這種方法是不可靠的。

為什么它不起作用?一般來說,sort 是一個“黑匣子”:我們將一個數組和一個比較函數放入其中,并期望其對數組進行排序。但是由于比較的完全隨機性,這個黑匣子瘋了,它發(fā)瘋地確切程度取決于引擎中的具體實現方法。

還有其他很好的方法可以完成這項任務。例如,有一個很棒的算法叫作 Fisher-Yates shuffle。其思路是:逆向遍歷數組,并將每個元素與其前面的隨機的一個元素互換位置:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // 從 0 到 i 的隨機索引

    // 交換元素 array[i] 和 array[j]
    // 我們使用“解構分配(destructuring assignment)”語法來實現它
    // 你將在后面的章節(jié)中找到有關該語法的更多詳細信息
    // 可以寫成:
    // 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]];
  }
}

// 所有可能排列的出現次數
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('')]++;
}

// 顯示所有可能排列的出現次數
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? 是一個數組。

創(chuàng)建一個函數 ?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? 個元素的數組只需要幾微秒。

但是我們在 ?for?循環(huán)中對 ?arr? 的每個元素都進行了一次檢測。

因此,如果 ?arr.length? 是 ?10000?,我們會有 ?10000 * 10000? = 1 億次的比較。那真的太多了。

所以該解決方案僅適用于小型數組。

進一步,在后面的 Map and Set(映射和集合) 一章中,我們將看到如何對該方法進行優(yōu)化。


從數組創(chuàng)建鍵(值)對象

重要程度: 4

假設我們收到了一個用戶數組,形式為:?{id:..., name:..., age:... }?。

創(chuàng)建一個函數 ?groupById(arr)? 從該數組創(chuàng)建對象,以 ?id? 為鍵(key),數組項為值。

例如:

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);

/*
// 調用函數后,我們應該得到:

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;
  }, {})
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號