JavaScript 中最常用的兩種數據結構是 Object
和 Array
。
對象是壹種根據鍵存儲數據的實體。
數組是壹種直接存儲數據的有序列表。
但是,當我們把它們傳遞給函數時,函數可能不需要整個對象/數組,而只需要其中壹部分。
解構賦值 是壹種特殊的語法,它使我們可以將數組或對象“拆包”至壹系列變量中。有時這樣做更方便。
解構操作對那些具有很多參數和默認值等的函數也很奏效。下面有壹些例子。
這是壹個將數組解構到變量中的例子:
// 我們有壹個存放了名字和姓氏的數組 let arr = ["John", "Smith"] // 解構賦值 // 設置 firstName = arr[0] // 以及 surname = arr[1] let [firstName, surname] = arr; alert(firstName); // John alert(surname); // Smith
我們可以使用這些變量而非原來的數組項了。
當與 split
函數(或其他返回值爲數組的函數)結合使用時,看起來更優雅:
let [firstName, surname] = "John Smith".split(' '); alert(firstName); // John alert(surname); // Smith
正如我們所看到的,語法很簡單。但是有幾個需要注意的細節。讓我們通過更多的例子來加深理解。
“解構”並不意味著“破壞”
這種語法被叫做“解構賦值”,是因爲它“拆開”了數組或對象,將其中的各元素複制給壹些變量。原來的數組或對象自身沒有被修改。
換句話說,解構賦值只是寫起來簡潔壹點。以下兩種寫法是等價的:
// let [firstName, surname] = arr; let firstName = arr[0]; let surname = arr[1];
忽略使用逗號的元素
可以通過添加額外的逗號來丟棄數組中不想要的元素:
// 不需要第二個元素 let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert( title ); // Consul
在上面的代碼中,數組的第二個元素被跳過了,第三個元素被賦值給了 title
變量。數組中剩下的元素也都被跳過了(因爲在這沒有對應給它們的變量)。
等號右側可以是任何可叠代對象
……實際上,我們可以將其與任何可叠代對象壹起使用,而不僅限于數組:
let [a, b, c] = "abc"; // ["a", "b", "c"] let [one, two, three] = new Set([1, 2, 3]);
這種情況下解構賦值是通過叠代右側的值來完成工作的。這是壹種用于對在 =
右側的值上調用 for..of
並進行賦值的操作的語法糖。
賦值給等號左側的任何內容
我們可以在等號左側使用任何“可以被賦值的”東西。
例如,壹個對象的屬性:
let user = {}; [user.name, user.surname] = "John Smith".split(' '); alert(user.name); // John alert(user.surname); // Smith
與 .entries() 方法進行循環操作
在前面的章節中我們已經見過了 Object.entries(obj) 方法。
我們可以將 .entries() 方法與解構語法壹同使用,來遍曆壹個對象的“鍵—值”對:
let user = { name: "John", age: 30 }; // 使用循環遍曆鍵—值對 for (let [key, value] of Object.entries(user)) { alert(`${key}:${value}`); // name:John, then age:30 }
用于 Map
的類似代碼更簡單,因爲 Map 是可叠代的:
let user = new Map(); user.set("name", "John"); user.set("age", "30"); // Map 是以 [key, value] 對的形式進行叠代的,非常便于解構 for (let [key, value] of user) { alert(`${key}:${value}`); // name:John, then age:30 }
交換變量值的技巧
使用解構賦值來交換兩個變量的值是壹個著名的技巧:
let guest = "Jane"; let admin = "Pete"; // 讓我們來交換變量的值:使得 guest = Pete,admin = Jane [guest, admin] = [admin, guest]; alert(`${guest} ${admin}`); // Pete Jane(成功交換!)
這裏我們創建了壹個由兩個變量組成的臨時數組,並且立即以顛倒的順序對其進行了解構賦值。
我們也可以用這種方式交換兩個以上的變量。
通常,如果數組比左邊的列表長,那麽“其余”的數組項會被省略。
例如,這裏只取了兩項,其余的就被忽略了:
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert(name1); // Julius alert(name2); // Caesar // 其余數組項未被分配到任何地方
如果我們還想收集其余的數組項 —— 我們可以使用三個點 "..."
來再加壹個參數以獲取其余數組項:
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; // rest 是包含從第三項開始的其余數組項的數組 alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2
rest
的值就是數組中剩下的元素組成的數組。
不壹定要使用變量名 rest
,我們也可以使用任何其他的變量名。只要確保它前面有三個點,並且在解構賦值的最後壹個參數位置上就行了:
let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; // 現在 titles = ["Consul", "of the Roman Republic"]
如果數組比左邊的變量列表短,這裏不會出現報錯。缺少對應值的變量都會被賦 undefined
:
let [firstName, surname] = []; alert(firstName); // undefined alert(surname); // undefined
如果我們想要壹個“默認”值給未賦值的變量,我們可以使用 =
來提供:
// 默認值 let [name = "Guest", surname = "Anonymous"] = ["Julius"]; alert(name); // Julius(來自數組的值) alert(surname); // Anonymous(默認值被使用了)
默認值可以是更加複雜的表達式,甚至可以是函數調用。不過,這些表達式或函數只會在這個變量未被賦值的時候才會被計算。
舉個例子,我們使用了 prompt
函數來提供兩個默認值:
// 只會提示輸入姓氏 let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"]; alert(name); // Julius(來自數組) alert(surname); // 妳輸入的值
請注意:prompt
將僅針對缺失值(surname
)運行。
解構賦值同樣適用于對象。
基本語法是:
let {var1, var2} = {var1:…, var2:…}
在等號右側是壹個已經存在的對象,我們想把它拆分到變量中。等號左側包含了對象相應屬性的壹個類對象“模式(pattern)”。在最簡單的情況下,等號左側的就是 {...}
中的變量名列表。
如下所示:
let options = { title: "Menu", width: 100, height: 200 }; let {title, width, height} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200
屬性 options.title
、options.width
和 options.height
值被賦給了對應的變量。
變量的順序並不重要,下面這個代碼也是等價的:
// 改變 let {...} 中元素的順序 let {height, width, title} = { title: "Menu", height: 200, width: 100 }
等號左側的模式(pattern)可以更加複雜,指定屬性和變量之間的映射關系。
如果我們想把壹個屬性賦值給另壹個名字的變量,比如把 options.width
屬性賦值給名爲 w
的變量,那麽我們可以使用冒號來設置變量名稱:
let options = { title: "Menu", width: 100, height: 200 }; // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; // width -> w // height -> h // title -> title alert(title); // Menu alert(w); // 100 alert(h); // 200
冒號的語法是“從對象中什麽屬性的值:賦值給哪個變量”。上面的例子中,屬性 width
被賦值給了 w
,屬性 height
被賦值給了 h
,屬性 title
被賦值給了同名變量。
對于可能缺失的屬性,我們可以使用 "="
設置默認值,如下所示:
let options = { title: "Menu" }; let {width = 100, height = 200, title} = options; alert(title); // Menu alert(width); // 100 alert(height); // 200
就像數組或函數參數壹樣,默認值可以是任意表達式甚至可以是函數調用。它們只會在未提供對應的值時才會被計算/調用。
在下面的代碼中,prompt
提示輸入 width
值,但不會提示輸入 title
值:
let options = { title: "Menu" }; let {width = prompt("width?"), title = prompt("title?")} = options; alert(title); // Menu alert(width); // (prompt 的返回值)
我們還可以將冒號和等號結合起來:
let options = { title: "Menu" }; let {width: w = 100, height: h = 200, title} = options; alert(title); // Menu alert(w); // 100 alert(h); // 200
如果我們有壹個具有很多屬性的複雜對象,那麽我們可以只提取所需的內容:
let options = { title: "Menu", width: 100, height: 200 }; // 僅提取 title 作爲變量 let { title } = options; alert(title); // Menu
如果對象擁有的屬性數量比我們提供的變量數量還多,該怎麽辦?我們可以只取其中的某壹些屬性,然後把“剩余的”賦值到其他地方嗎?
我們可以使用剩余模式(pattern),與數組類似。壹些較舊的浏覽器不支持此功能(例如 IE,可以使用 Babel 對其進行 polyfill),但可以在現代浏覽器中使用。
看起來就像這樣:
let options = { title: "Menu", height: 200, width: 100 }; // title = 名爲 title 的屬性 // rest = 存有剩余屬性的對象 let {title, ...rest} = options; // 現在 title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100
不使用 let
時的陷阱
在上面的示例中,變量都是在賦值中通過正確方式聲明的:let {…} = {…}
。當然,我們也可以使用已有的變量,而不用 let
,但這裏有壹個陷阱。
以下代碼無法正常運行:
let title, width, height; // 這壹行發生了錯誤 {title, width, height} = {title: "Menu", width: 200, height: 100};
問題在于 JavaScript 把主代碼流(即不在其他表達式中)的 {...}
當做壹個代碼塊。這樣的代碼塊可以用于對語句分組,如下所示:
{ // 壹個代碼塊 let message = "Hello"; // ... alert( message ); }
因此,這裏 JavaScript 假定我們有壹個代碼塊,這就是報錯的原因。我們需要解構它。
爲了告訴 JavaScript 這不是壹個代碼塊,我們可以把整個賦值表達式用括號 (...)
包起來:
let title, width, height; // 現在就可以了 ({title, width, height} = {title: "Menu", width: 200, height: 100}); alert( title ); // Menu
如果壹個對象或數組嵌套了其他的對象和數組,我們可以在等號左側使用更複雜的模式(pattern)來提取更深層的數據。
在下面的代碼中,options
的屬性 size
是另壹個對象,屬性 items
是另壹個數組。賦值語句中等號左側的模式(pattern)具有相同的結構以從中提取值:
let options = { size: { width: 100, height: 200 }, items: ["Cake", "Donut"], extra: true }; // 爲了清晰起見,解構賦值語句被寫成多行的形式 let { size: { // 把 size 賦值到這裏 width, height }, items: [item1, item2], // 把 items 賦值到這裏 title = "Menu" // 在對象中不存在(使用默認值) } = options; alert(title); // Menu alert(width); // 100 alert(height); // 200 alert(item1); // Cake alert(item2); // Donut
對象 options
的所有屬性,除了 extra
屬性在等號左側不存在,都被賦值給了對應的變量:
最終,我們得到了 width
、height
、item1
、item2
和具有默認值的 title
變量。
注意,size
和 items
沒有對應的變量,因爲我們取的是它們的內容。
有時,壹個函數有很多參數,其中大部分的參數都是可選的。對用戶界面來說更是如此。想象壹個創建菜單的函數。它可能具有寬度參數,高度參數,標題參數和項目列表等。
下面是實現這種函數的壹個很不好的寫法:
function showMenu(title = "Untitled", width = 200, height = 100, items = []) { // ... }
在實際開發中,記憶如此多的參數的位置是壹個很大的負擔。通常集成開發環境(IDE)會盡力幫助我們,特別是當代碼有良好的文檔注釋的時候,但是…… 另壹個問題就是,在大部分的參數只需采用默認值的情況下,調用這個函數時會需要寫大量的 undefined。
像這樣:
// 在采用默認值就可以的位置設置 undefined showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
這太難看了。而且,當我們處理更多參數的時候可讀性會變得很差。
解構賦值可以解決這些問題。
我們可以用壹個對象來傳遞所有參數,而函數負責把這個對象解構成各個參數:
// 我們傳遞壹個對象給函數 let options = { title: "My menu", items: ["Item1", "Item2"] }; // ……然後函數馬上把對象解構成變量 function showMenu({title = "Untitled", width = 200, height = 100, items = []}) { // title, items – 提取于 options, // width, height – 使用默認值 alert( `${title} ${width} ${height}` ); // My Menu 200 100 alert( items ); // Item1, Item2 } showMenu(options);
我們也可以使用帶有嵌套對象和冒號映射的更加複雜的解構:
let options = { title: "My menu", items: ["Item1", "Item2"] }; function showMenu({ title = "Untitled", width: w = 100, // width goes to w height: h = 200, // height goes to h items: [item1, item2] // items first element goes to item1, second to item2 }) { alert( `${title} ${w} ${h}` ); // My Menu 100 200 alert( item1 ); // Item1 alert( item2 ); // Item2 } showMenu(options);
完整語法和解構賦值是壹樣的:
function({ incomingProperty: varName = defaultValue ... })
對于參數對象,屬性 incomingProperty
對應的變量是 varName
,默認值是 defaultValue
。
請注意,這種解構假定了 showMenu()
函數確實存在參數。如果我們想讓所有的參數都使用默認值,那我們應該傳遞壹個空對象:
showMenu({}); // 不錯,所有值都取默認值 showMenu(); // 這樣會導致錯誤
我們可以通過指定空對象 {}
爲整個參數對象的默認值來解決這個問題:
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) { alert( `${title} ${width} ${height}` ); } showMenu(); // Menu 100 200
在上面的代碼中,整個參數對象的默認是 {}
,因此總會有內容可以用來解構。
解構賦值可以簡潔地將壹個對象或數組拆開賦值到多個變量上。
解構對象的完整語法:
let {prop : varName = default, ...rest} = object
這表示屬性 prop
會被賦值給變量 varName
,如果沒有這個屬性的話,就會使用默認值 default
。
沒有對應映射的對象屬性會被複制到 rest
對象。
解構數組的完整語法:
let [item1 = default, item2, ...rest] = array
數組的第壹個元素被賦值給 item1
,第二個元素被賦值給 item2
,剩下的所有元素被複制到另壹個數組 rest
。
從嵌套數組/對象中提取數據也是可以的,此時等號左側必須和等號右側有相同的結構。
重要程度: 5
我們有壹個對象:
let user = { name: "John", years: 30 };
寫壹個解構賦值語句使得:
name
屬性賦值給變量 name
。
years
屬性賦值給變量 age
。
isAdmin
屬性賦值給變量 isAdmin
(如果屬性缺失則取默認值 false)。
下面是賦值完成後的值的情況:
let user = { name: "John", years: 30 }; // 等號左側是妳的代碼 // ... = user alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false
let user = { name: "John", years: 30 }; let {name, years: age, isAdmin = false} = user; alert( name ); // John alert( age ); // 30 alert( isAdmin ); // false
重要程度: 5
這兒有壹個 salaries
對象:
let salaries = { "John": 100, "Pete": 300, "Mary": 250 };
新建壹個函數 topSalary(salaries)
,返回收入最高的人的姓名。
如果 salaries
是空的,函數應該返回 null
。
如果有多個收入最高的人,返回其中任意壹個即可。
P.S. 使用 Object.entries
和解構語法來遍曆鍵/值對。
打開帶有測試的沙箱。
function topSalary(salaries) { let maxSalary = 0; let maxName = null; for(let [name, salary] of Object.entries(salaries)) { if (maxSalary < salary) { maxSalary = salary; maxName = name; } } return maxName; }
使用沙箱的測試功能打開解決方案。