JavaScript 展開運算子 (spread operator)

閱讀時間約 11 分鐘

近期接觸 API 後,開始有帶入 response data 到程式碼的需求。這時如果將 API 回傳的資料直接帶入到空陣列,會造成陣列只有一個子陣列元素,和我們原先預期總共 80 個物件的結果形成落差。

照片來自 Alpha Camp 教案

照片來自 Alpha Camp 教案

由於回傳的是一個陣列,我們當然可以運用 for 迴圈,或是陣列專屬的 for...offorEach() 等方法。但其實 ES6 提供了一種更為簡潔的語法稱作展開運算子,可以把陣列中的元素展開來供我們使用。

這篇文章將記錄展開運算子的使用案例,以及展開運算子與淺拷貝 (shallow copy) 的地雷。


什麼是展開運算子?

以下是 MDN 當中對於展開運算子的解釋

The spread (...) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.

簡單來說,展開運算子由三個連續的半形句點(...)組成,將可迭代(iterable)的 JavaScript 物件(例如陣列、物件、字串等)展開為獨立的元素。


展開運算子使用範例

在呼叫函示時使用展開運算子

JavaScript Math 函式庫提供了眾多實用的運算方法,這些方法通常只接受數字而已。假設今天我們希望找出陣列當中最大的數字,就可以使用 Math.max()。但由於 Math.max() 僅接受數字,所以我們帶入陣列 nums 會得到 NaN。

const nums = [1, 2, 3, 5,];

console.log(nums); // [1, 2, 3, 5,]
console.log(Math.max(nums)); //NaN


這時候展開運算子就能拜上用場!運作起來等於 Math.max(1, 2, 3, 5)。展開運算子會將陣列展開,讓陣列中的元素變成 argument list。

const nums = [1, 2, 3, 5];

console.log(Math.max(nums)); //NaN
console.log(...nums); // 1, 2, 3, 5
console.log(Math.max(...nums)); // 5


此外,由於字串也是可迭代的,所以我們同樣可以用展開運算子,將字串拆解成字元。

​console.log(...'Bonjour'); // B o n j o u r


展開運算子與陣列

以前若要合併兩個陣列,直覺上都會根據情境使用以下兩種方法:

  • array.concat():將 b 陣列合併至 a 陣列,會回傳一個新陣列
  • array.unshift:將 b 陣列插入至 a 陣列的尾端 (灬ºωº灬),直接改變 a 陣列

現在學會展開運算式之後,就能用更簡潔、易懂的方式達到合併的效果囉。

const cats = ['三花', '橘貓', '玳瑁', '金吉拉'];
const dogs = ['博美', '吉娃娃', '法鬥', '柯基'];

const kawaii = [...cats, ...dogs];
console.log(kawaii); // ['三花', '橘貓', '玳瑁', '金吉拉', '博美', '吉娃娃', '法鬥', '柯基']


這邊要注意的是,展開運算式不會改變到原先的陣列。

當然,我們也可以透過展開運算子來複製陣列,但這會牽扯到淺拷貝的問題,所以留到下面用比較長的篇幅說明。

展開運算子與物件

物件同樣可以運用和陣列的方式,透過展開運算子進行合併,但要注意,若兩個物件當中有重複的 key,JavaScript 將以後面的物件為主。

除此之外,由於物件在 JavaScript 是 by reference (其實更具體來說是 by sharing),因此若我們直接用賦值的方式進行物件複製,兩個變數其實背後都是儲存該物件的記憶體地址,所以任一個變數更改物件,都會影響到另一方。

這樣的背後指向的值相同,兩個物件操作會互相影響,即為淺拷貝(shallow copy)

const user = {
name: 'Mimiball',
age: 2,
email: 'mimiball@gmail.com'
}

const user2 = user;

user2.age = 7;

console.log(`The age of user: ${user.age}`); //7
console.log(`The age of user2: ${user2.age}`); //7


如果希望讓兩個變數互不影響,可以透過展開運算子。在下面的範例中,我們先利用展開運算子來展開 user 物件 (...user),然後將 user 物件內的屬性重新包覆在一個新的物件當中 ({...user}):

const user = {
name: 'Mimiball',
age: 2,
email: 'mimiball@gmail.com'
}

const user2 = {...user};

user2.age = 7;

console.log(`The age of user: ${user.age}`); //2
console.log(`The age of user2: ${user2.age}`); //7


太好了,看來展開運算子可以實現物件的深拷貝(Deep Copy),讓兩個物件拷貝好不會相互影響,但如果我們的物件當中還有另一個子物件呢?

const user = {
name: 'Mimiball',
age: 2,
email: 'mimiball@gmail.com',
role: {
isAdmin: true,
position: 'CEO'
}
}

const user2 = {...user};

user2.role.isAdmin = false;

console.log(user.role.isAdmin); //false
console.log(user2.role.isAdmin); //false


可惡,看起來展開運算子只會讓物件第一層不相互影響,但如果中間包含子層物件,仍舊為淺拷貝。事實上的確如此,第一層的 name、age、email 都是 primitive data type,所以展開之後即為基本的資料類型。相較之下,子層物件展開之後,role 還是個物件,而 role 實際上儲存的值是這個子層物件的記憶體位址。

照這樣的邏輯,如果手動展開兩次的話,也可以對 role 進行深拷貝囉?

const user = {
name: 'Mimiball',
age: 2,
email: 'mimiball@gmail.com',
role: {
isAdmin: true,
position: 'CEO'
}
}

const user2 = {...user, role: {...user.role}};

user2.role.isAdmin = false;

console.log(user.role.isAdmin); //true
console.log(user2.role.isAdmin); //false


當然實務上不太會這樣做,若要進行深拷貝,可以參考底下資料的建議~

後來我在 MDN 關於淺拷貝的文件中,找到了以下這段話,總算解決了心中的疑惑 (很怕自己是胡亂測試、胡亂說服自己的):

For shallow copies, only the top-level properties are copied, not the values of nested objects.

此外,這份文件也寫道,倘若物件當中全部都是 primitive value,也就是只有第一層的話,該物件便同時滿足深拷貝以及淺拷貝的定義。所以討論沒有子層的物件拷貝,是沒有意義的。深拷貝通常會在改變子層屬性的情境下才討論。

The copy of an object whose properties all have primitive values fits the definition of both a deep copy and a shallow copy. It is somewhat useless to talk about the depth of such a copy, though, because it has no nested properties and we usually talk about deep copying in the context of mutating nested properties.


結語

據說展開運算子的用法和地雷在往後的前端框架學習上很容易出現。這次剛好查了些資料也實際測試看看,記錄下來給未來的自己參考。前提是我之後沒有中途放棄網頁開發學習。


參考資料:


18會員
34Content count
Bonjour à tous,我本身是法文系畢業,這邊會刊登純文組學習網頁開發的筆記。如果能鼓勵更多文組夥伴一起學習,那就太開心了~
留言0
查看全部
發表第一個留言支持創作者!
蕭宇廷的沙龍 的其他內容
本文討論了箭頭函式 (arrow function) 對於 this 關鍵字的影響。
記錄 JavaScript 學習旅程中遇到的 method 與 this 問題
記錄 JavaScript 物件中點記法 (dot notation) 以及括號記法 (bracket notation) 之間的差別。
Alpha Camp C1 完課心得 (沒事,還活著)
本文討論了箭頭函式 (arrow function) 對於 this 關鍵字的影響。
記錄 JavaScript 學習旅程中遇到的 method 與 this 問題
記錄 JavaScript 物件中點記法 (dot notation) 以及括號記法 (bracket notation) 之間的差別。
Alpha Camp C1 完課心得 (沒事,還活著)
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
在一開始學習前端開發的時候,一直遇到講師在課程內容中提到 ES5、ES6 等關鍵字,當初的我,單純認為 ES5、ES6 是講述 JavaScript 的版本,所以在使用上就沒有想太多,反正就是 JavaScript 1.0 、2.0 的感覺吧?
Thumbnail
上篇介紹的promise chain的寫法,是已經比原本好維護了沒錯,但是可讀性似乎還是有點不足,其實還可以改成用async/await的寫法,如下: E 其中,async是非同步的意思,等於是把getData()這個function定義為非同步,因此從console可以看到,test是最先被pri
Thumbnail
輸入畫面 為什麼要做驗證? 因為作為設計者,永遠不該預設使用者會乖乖照設計者的意思輸入。
Thumbnail
本系列文為節選第 468 期 JavaScript Weekly 文章的讀後整理心得。 本文為「下」,收錄內容: Vue 3.0 的設計概念 TypeScript 的 const assertion Preact 華麗復活 阿 Svelte 不是很邱?
Thumbnail
本系列文為節選第 468 期 JavaScript Weekly 文章的整理心得。 本文為「上」,收錄內容: Tesseract.js 2.0 颯爽登場 State of JavaScript 2019 問卷結果 CNDJS 維護團隊(Cloudflare)的真情告白 ...
Thumbnail
JavaScript在ES6新增了let, const等宣告變數的方式,其中let, const是block scope的,而var則是function scope。
Thumbnail
promise是ES6才有的,它是一種非同步的技術,使用它除了可以在background處理一些事情以外,還可以增加程式碼的可維護性。
Thumbnail
你可能已經看過 “ES6” 或 “JavaScript ES6” 一詞,並想知道它實際意味著什麼。別再想了,因為我們將深入研究 ES6 究竟是什麼,以及它與 JavaScript 的關係!
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
在一開始學習前端開發的時候,一直遇到講師在課程內容中提到 ES5、ES6 等關鍵字,當初的我,單純認為 ES5、ES6 是講述 JavaScript 的版本,所以在使用上就沒有想太多,反正就是 JavaScript 1.0 、2.0 的感覺吧?
Thumbnail
上篇介紹的promise chain的寫法,是已經比原本好維護了沒錯,但是可讀性似乎還是有點不足,其實還可以改成用async/await的寫法,如下: E 其中,async是非同步的意思,等於是把getData()這個function定義為非同步,因此從console可以看到,test是最先被pri
Thumbnail
輸入畫面 為什麼要做驗證? 因為作為設計者,永遠不該預設使用者會乖乖照設計者的意思輸入。
Thumbnail
本系列文為節選第 468 期 JavaScript Weekly 文章的讀後整理心得。 本文為「下」,收錄內容: Vue 3.0 的設計概念 TypeScript 的 const assertion Preact 華麗復活 阿 Svelte 不是很邱?
Thumbnail
本系列文為節選第 468 期 JavaScript Weekly 文章的整理心得。 本文為「上」,收錄內容: Tesseract.js 2.0 颯爽登場 State of JavaScript 2019 問卷結果 CNDJS 維護團隊(Cloudflare)的真情告白 ...
Thumbnail
JavaScript在ES6新增了let, const等宣告變數的方式,其中let, const是block scope的,而var則是function scope。
Thumbnail
promise是ES6才有的,它是一種非同步的技術,使用它除了可以在background處理一些事情以外,還可以增加程式碼的可維護性。
Thumbnail
你可能已經看過 “ES6” 或 “JavaScript ES6” 一詞,並想知道它實際意味著什麼。別再想了,因為我們將深入研究 ES6 究竟是什麼,以及它與 JavaScript 的關係!