文章目錄
- 將 border-left-width 轉換成 borderLeftWidth
- 過濾范圍
- 原位(in place)過濾范圍
- 降序排列
- 復制和排序數組
- 創建一個可擴展的 calculator
- 映射到 names
- 映射到對象
- 按年齡對用戶排序
- 隨機排列數組
- 獲取平均年齡
- 數組去重
- 從數組創建鍵(值)對象
- Iterable object(可迭代對象)
- Symbol.iterator
- 字符串是可迭代的
- 顯式調用迭代器
- 可迭代(iterable)和類數組(array-like)
- Array.from
- 總結
?任務
將 border-left-width 轉換成 borderLeftWidth
重要程度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(匹配值)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
區間以外的所有值。檢查: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
不變。
創建一個函數 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 );
創建一個可擴展的 calculator
重要程度5??
創建一個構造函數 Calculator
,以創建“可擴展”的 calculator 對象。
該任務由兩部分組成。
-
首先,實現
calculate(str)
方法,該方法接受像"1 + 2"
這樣格式為“數字 運算符 數字”(以空格分隔)的字符串,并返回結果。該方法需要能夠理解加號+
和減號-
。用法示例:
let calc = new Calculator;alert( calc.calculate("3 + 7") ); // 10
-
然后添加方法
addMethod(name, func)
,該方法教 calculator 進行新操作。它需要運算符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;}; }
使用沙箱的測試功能打開解決方案。
映射到 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
對象數組,每個對象都有 name
,surname
和 id
。
編寫代碼以該數組為基礎,創建另一個具有 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
是一個“黑匣子”:我們將一個數組和一個比較函數放入其中,并期望其對數組進行排序。但是由于比較的完全隨機性,這個黑匣子瘋了,它發瘋地確切程度取決于引擎中的具體實現方法。還有其他很好的方法可以完成這項任務。例如,有一個很棒的算法叫作 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)”語法來實現它// 你將在后面的章節中找到有關該語法的更多詳細信息// 可以寫成:// 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
是一個數組。
創建一個函數 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
循環中對arr
的每個元素都進行了一次檢測。因此,如果
arr.length
是10000
,我們會有10000 * 10000
= 1 億次的比較。那真的太多了。所以該解決方案僅適用于小型數組。
進一步,在后面的 【Map and Set(映射和集合)】一章中,我們將看到如何對該方法進行優化。
使用沙箱的測試功能打開解決方案。
從數組創建鍵(值)對象
重要程度4??
假設我們收到了一個用戶數組,形式為:{id:..., name:..., age:... }
。
創建一個函數 groupById(arr)
從該數組創建對象,以 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; }, {}) }
打開帶有測試的沙箱。
Iterable object(可迭代對象)
可迭代(Iterable) 對象是數組的泛化。這個概念是說任何對象都可以被定制為可在 for..of
循環中使用的對象。
數組是可迭代的。但不僅僅是數組。很多其他內建對象也都是可迭代的。例如字符串也是可迭代的。
如果從技術上講,對象不是數組,而是表示某物的集合(列表,集合),for..of
是一個能夠遍歷它的很好的語法,因此,讓我們來看看如何使其發揮作用。
Symbol.iterator
通過自己創建一個對象,我們就可以輕松地掌握可迭代的概念。
例如,我們有一個對象,它并不是數組,但是看上去很適合使用 for..of
循環。
比如一個 range
對象,它代表了一個數字區間:
let range = {from: 1,to: 5
};// 我們希望 for..of 這樣運行:
// for(let num of range) ... num=1,2,3,4,5
為了讓 range
對象可迭代(也就讓 for..of
可以運行)我們需要為對象添加一個名為 Symbol.iterator
的方法(一個專門用于使對象可迭代的內建 symbol)。
- 當
for..of
循環啟動時,它會調用這個方法(如果沒找到,就會報錯)。這個方法必須返回一個 迭代器(iterator) —— 一個有next
方法的對象。 - 從此開始,
for..of
僅適用于這個被返回的對象。 - 當
for..of
循環希望取得下一個數值,它就調用這個對象的next()
方法。 next()
方法返回的結果的格式必須是{done: Boolean, value: any}
,當done=true
時,表示循環結束,否則value
是下一個值。
這是帶有注釋的 range
的完整實現:
let range = {from: 1,to: 5
};// 1. for..of 調用首先會調用這個:
range[Symbol.iterator] = function() {// ……它返回迭代器對象(iterator object):// 2. 接下來,for..of 僅與下面的迭代器對象一起工作,要求它提供下一個值return {current: this.from,last: this.to,// 3. next() 在 for..of 的每一輪循環迭代中被調用next() {// 4. 它將會返回 {done:.., value :...} 格式的對象if (this.current <= this.last) {return { done: false, value: this.current++ };} else {return { done: true };}}};
};// 現在它可以運行了!
for (let num of range) {alert(num); // 1, 然后是 2, 3, 4, 5
}
請注意可迭代對象的核心功能:關注點分離。
range
自身沒有next()
方法。- 相反,是通過調用
range[Symbol.iterator]()
創建了另一個對象,即所謂的“迭代器”對象,并且它的next
會為迭代生成值。
因此,迭代器對象和與其進行迭代的對象是分開的。
從技術上說,我們可以將它們合并,并使用 range
自身作為迭代器來簡化代碼。
就像這樣:
let range = {from: 1,to: 5,[Symbol.iterator]() {this.current = this.from;return this;},next() {if (this.current <= this.to) {return { done: false, value: this.current++ };} else {return { done: true };}}
};for (let num of range) {alert(num); // 1, 然后是 2, 3, 4, 5
}
現在 range[Symbol.iterator]()
返回的是 range
對象自身:它包括了必需的 next()
方法,并通過 this.current
記憶了當前的迭代進程。這樣更短,對嗎?是的。有時這樣也可以。
但缺點是,現在不可能同時在對象上運行兩個 for..of
循環了:它們將共享迭代狀態,因為只有一個迭代器,即對象本身。但是兩個并行的 for..of
是很罕見的,即使在異步情況下。
??無窮迭代器(iterator)
無窮迭代器也是可能的。例如,將
range
設置為range.to = Infinity
,這時range
則成為了無窮迭代器。或者我們可以創建一個可迭代對象,它生成一個無窮偽隨機數序列。也是可能的。
next
沒有什么限制,它可以返回越來越多的值,這是正常的。當然,迭代這種對象的
for..of
循環將不會停止。但是我們可以通過使用break
來停止它。
字符串是可迭代的
數組和字符串是使用最廣泛的內建可迭代對象。
對于一個字符串,for..of
遍歷它的每個字符:
for (let char of "test") {// 觸發 4 次,每個字符一次alert( char ); // t, then e, then s, then t
}
對于代理對(surrogate pairs),它也能正常工作!(譯注:這里的代理對也就指的是 UTF-16 的擴展字符)
let str = '𝒳😂';
for (let char of str) {alert( char ); // 𝒳,然后是 😂
}
顯式調用迭代器
為了更深層地了解底層知識,讓我們來看看如何顯式地使用迭代器。
我們將會采用與 for..of
完全相同的方式遍歷字符串,但使用的是直接調用。這段代碼創建了一個字符串迭代器,并“手動”從中獲取值。
let str = "Hello";// 和 for..of 做相同的事
// for (let char of str) alert(char);let iterator = str[Symbol.iterator]();while (true) {let result = iterator.next();if (result.done) break;alert(result.value); // 一個接一個地輸出字符
}
很少需要我們這樣做,但是比 for..of
給了我們更多的控制權。例如,我們可以拆分迭代過程:迭代一部分,然后停止,做一些其他處理,然后再恢復迭代。
可迭代(iterable)和類數組(array-like)
這兩個官方術語看起來差不多,但其實大不相同。請確保你能夠充分理解它們的含義,以免造成混淆。
- Iterable 如上所述,是實現了
Symbol.iterator
方法的對象。 - Array-like 是有索引和
length
屬性的對象,所以它們看起來很像數組。
當我們將 JavaScript 用于編寫在瀏覽器或任何其他環境中的實際任務時,我們可能會遇到可迭代對象或類數組對象,或兩者兼有。
例如,字符串即是可迭代的(for..of
對它們有效),又是類數組的(它們有數值索引和 length
屬性)。
但是一個可迭代對象也許不是類數組對象。反之亦然,類數組對象可能不可迭代。
例如,上面例子中的 range
是可迭代的,但并非類數組對象,因為它沒有索引屬性,也沒有 length
屬性。
下面這個對象則是類數組的,但是不可迭代:
let arrayLike = { // 有索引和 length 屬性 => 類數組對象0: "Hello",1: "World",length: 2
};// Error (no Symbol.iterator)
for (let item of arrayLike) {}
可迭代對象和類數組對象通常都 不是數組,它們沒有 push
和 pop
等方法。如果我們有一個這樣的對象,并想像數組那樣操作它,那就非常不方便。例如,我們想使用數組方法操作 range
,應該如何實現呢?
Array.from
有一個全局方法 Array.from 可以接受一個可迭代或類數組的值,并從中獲取一個“真正的”數組。然后我們就可以對其調用數組方法了。
例如:
let arrayLike = {0: "Hello",1: "World",length: 2
};let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)
在 (*)
行的 Array.from
方法接受對象,檢查它是一個可迭代對象或類數組對象,然后創建一個新數組,并將該對象的所有元素復制到這個新數組。
如果是可迭代對象,也是同樣:
// 假設 range 來自上文的例子中
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (數組的 toString 轉化方法生效)
Array.from
的完整語法允許我們提供一個可選的“映射(mapping)”函數:
Array.from(obj[, mapFn, thisArg])
可選的第二個參數 mapFn
可以是一個函數,該函數會在對象中的元素被添加到數組前,被應用于每個元素,此外 thisArg
允許我們為該函數設置 this
。
例如:
// 假設 range 來自上文例子中// 求每個數的平方
let arr = Array.from(range, num => num * num);alert(arr); // 1,4,9,16,25
現在我們用 Array.from
將一個字符串轉換為單個字符的數組:
let str = '𝒳😂';// 將 str 拆分為字符數組
let chars = Array.from(str);alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
與 str.split
方法不同,它依賴于字符串的可迭代特性。因此,就像 for..of
一樣,可以正確地處理代理對(surrogate pair)。(譯注:代理對也就是 UTF-16 擴展字符。)
技術上來講,它和下面這段代碼做的是相同的事:
let str = '𝒳😂';let chars = []; // Array.from 內部執行相同的循環
for (let char of str) {chars.push(char);
}alert(chars);
……但 Array.from
精簡很多。
我們甚至可以基于 Array.from
創建代理感知(surrogate-aware)的slice
方法(譯注:也就是能夠處理 UTF-16 擴展字符的 slice
方法):
function slice(str, start, end) {return Array.from(str).slice(start, end).join('');
}let str = '𝒳😂𩷶';alert( slice(str, 1, 3) ); // 😂𩷶// 原生方法不支持識別代理對(譯注:UTF-16 擴展字符)
alert( str.slice(1, 3) ); // 亂碼(兩個不同 UTF-16 擴展字符碎片拼接的結果)
總結
可以應用 for..of
的對象被稱為 可迭代的。
- 技術上來說,可迭代對象必須實現
Symbol.iterator
方法。obj[Symbol.iterator]()
的結果被稱為 迭代器(iterator)。由它處理進一步的迭代過程。- 一個迭代器必須有
next()
方法,它返回一個{done: Boolean, value: any}
對象,這里done:true
表明迭代結束,否則value
就是下一個值。
Symbol.iterator
方法會被for..of
自動調用,但我們也可以直接調用它。- 內建的可迭代對象例如字符串和數組,都實現了
Symbol.iterator
。 - 字符串迭代器能夠識別代理對(surrogate pair)。(譯注:代理對也就是 UTF-16 擴展字符。)
有索引屬性和 length
屬性的對象被稱為 類數組對象。這種對象可能還具有其他屬性和方法,但是沒有數組的內建方法。
如果我們仔細研究一下規范 —— 就會發現大多數內建方法都假設它們需要處理的是可迭代對象或者類數組對象,而不是“真正的”數組,因為這樣抽象度更高。
Array.from(obj[, mapFn, thisArg])
將可迭代對象或類數組對象 obj
轉化為真正的數組 Array
,然后我們就可以對它應用數組的方法。可選參數 mapFn
和 thisArg
允許我們將函數應用到每個元素。