iT邦幫忙

2021 iThome 鐵人賽

DAY 4
1
Modern Web

Javascript 從寫對到寫好系列 第 4

Day 4 - Object 物件組合技

前言

前兩天講完了一些常用的 Array 組合技,今天來介紹一下它的青梅竹馬(?) - Object

物件也算是早期會接觸到的重點之一,重點在於它 key/value 的 pair 組合,可以將所有相關屬性的變數都集中在一個 Object 內。

// 這幾個變數其實都屬於同一個人
const name = 'Joey';
const height = 173;
const weight = 63;

// 可以透過 Object 把大家放在一起
const person = {
    name: 'Joey',
    height: 173,
    weight: 63
};

其實,Array 也是 Object 的其中一種,只是 Array 的 key 全都是索引化的(0,1,2,3...),而一般的 Object 則有各種可自訂的 key(name,id,price...)。

不僅自己容易用到,連 call 一些第三方的 API,回傳回來也經常是一個 Object,操作起來其實很方便。

今天讓我們來看看有什麼好用的實戰組合技!

各種 Method 的實戰

  • keys / values / entries
  • delete
  • JSON.stringify / JSON.parse

keys / values / entries

Object.keys() 方法會回傳一個由指定 Object 所有可列舉之屬性組成的陣列。

Object.values() 方法會回傳一個由指定 Object 所有可列舉之屬性 「的對應值」 組成的陣列。

Object.entries() 方法會回傳一個由指定 Object 所有可列舉之 「屬性與其對應值組成的陣列」 組成的陣列。(這太饒舌了,請直接看下方範例)

const person = {
    name: 'Joey',
    height: 173,
    weight: 63
};
Object.keys(person) // ['name', 'height', 'weight']
Object.values(person) // ['Joey', 173, 63]
Object.entries(person) // [['name', 'Joey'], ['height', 173], ['weight', 63]]

for...in 迴圈也可以做到類似的效果,差別是,Object.keys 系列只會回傳本身的 own property,而 for...in 還會迭代出 Object 從原型鏈所繼承來的屬性

這三個 method 本質上都是在做「取得物件的 key/value」,並且回傳一個陣列(Object.entries 會回傳 Array of Array),因此通常都會跟 Array 的 method 混搭,將物件中每一個屬性都用迴圈跑一次。

✔ 把 Object 拿來跑迴圈

雖然前面有提到一般的 Object 比起 Array 少了順序的特性,因此無法直接使用 forEach 之類的迭代函式,但仍然可以透過 keys/values/entries 做到將所有物件內屬性跑一輪的效果。

const obj = {
   item1: {id: "item1", name: "TV", price: 13500},
   item2: {id: "item2", name: "washing machine", price: 8200},
   item3: {id: "item3", name: "laptop", price: 25000}
};

// keys
Object.keys(obj)
      .forEach(key => console.log(obj[key].name));
      
// values
Object.values(obj)
      .forEach(value => console.log(value.name));
      
// entries
Object.entries(obj)
      .forEach(([key, value]) => console.log(value.name));

三者執行結果都是

TV
washing machine
laptop

✔ 檢核表單

雖然目前框架當道,使用一個搭配的表單&檢核系統幾乎是個標配(比如 React 就有 Redux Form / React Hook Form / Formik)。

不過若是回歸到表單的本質,通常都會是個 Object,用欄位名稱當 key,欄位值當 value,這時候就能夠在表單 submit 的時候處理一些事情啦!

// 必填欄位
const requiredFields = [
    'productId', 
    'quantity', 
    'deliverAddress', 
    'deliverDate'
];
// 已填的表單欄位
const form = {
   productId: '612b06609ea3b35614c0edbd',
   quantity: 5,
   deliverAddress: '台灣'
};
const formFields = Object.keys(form);

const valid = requiredFields.every(key => {
    return formFields.includes(key) && typeof form[key] !== 'undefined'
});

if (!valid) {
    console.log('尚有必填欄位未填');
}

執行結果

尚有必填欄位未填

delete

正所謂一個基本的存取操作都包含 CRUD,為什麼這邊只介紹 delete 呢?

沒什麼,單純是因為它有個 delete 的保留字XD

delete 可以用來移除 Object 的 property,其實很普通,但因為大部分的時候,Array 有多餘的元素會直接影響到程式,但 Object 有多餘的 property 卻不一定會出現問題

主要是因為 Array 大多時候都會用 forEach 等 method 來跑迴圈,因此多一個會立馬被發現(比如說畫面上就多一個商品):

const arr = ['TV', 'washing machine', 'laptop', '我是多的QQ'];

console.log(arr.join('、'));

執行結果

TV、washing machine、laptop、我是多的QQ

但 Object 有多餘的 property,很多時候我們根本不在意,因為大多時候都是正向表列取出需要的 property,只要沒用到 Object.keys 之類的迭代 method,或者 { ...obj } 這種 spread operator,好像也不會發現它的存在:

const obj = {
    name: 'TV',
    price: 12000,
    quantity: 3,
    redundant: '我是多的QQ'
};

console.log(`商品名稱: ${obj.name},價格: ${obj.price},數量: ${obj.quantity}`);

執行結果

商品名稱: TV,價格: 12000,數量: 3

因此很容易會忽略要去清理 Object,如果都在同一隻 js 內處理還沒什麼,若是前端的資料要送後端,往往後端不會那麼清楚前端送了什麼過來,這時如果夾雜了一些非預期的 property 進去,就很容易出狀況。

因此我個人的習慣是,除非要送後端的欄位爆炸多,不然我還是傾向正向表列要後送的欄位,並將沒用到的欄位移除。

✔ 刪除多餘的表單欄位

const submitFields = ['name', 'price', 'quantity'];
const submitForm = {
    name: 'TV',
    price: 12000,
    quantity: 3,
    redundant: '我是多的QQ'
};

Object.keys(submitForm).forEach(key => {
    if (!submitFields.includes(key)) {
        delete submitForm[key];
    }
});

console.log(submitForm);

執行結果

{
    name: 'TV',
    price: 12000,
    quantity: 3
}

JSON.stringify / JSON.parse

JSON.parse() 方法把會把一個 JSON 字串,轉換成 Javascript 的數值或是物件。(參考MDN)

JSON.parse(text)

JSON.stringify() 則是反過來,會把一個 Javascript 的數值或是物件,轉換成 JSON 字串。(參考MDN)

JSON.stringify(value)

這一組組合技有 encode 跟 decode 的感覺,提供了一個可以在 primitive 與 non-primitive 兩種類型間轉換的方法,可以把物件轉成字串,也可以把字串轉成物件。

但仔細一想,什麼時候會需要做這件事呢?

✔ 物件轉存 Web storage

web storage 是前端在瀏覽器儲存資料的地方,可以儲存不那麼重要,但可以提升使用者體驗的資料,比如語系、幣別、是否跳廣告,以及未登入之前暫時的購物車等。

雖然是存 key/value 的結構,但 value 僅限儲存 string 類別的資料,因此,如果要把 Object 或 Array 存進去 web storage,就會需要藉助 JSON.stringify,反之取資料需要 JSON.parse

const userData = {
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
};
const stringifiedData = JSON.stringify(userData);
localStorage.setItem('userData', stringifiedData);

const parsedData = JSON.parse(localStorage.getItem('userData'));

console.log(stringifiedData);
console.log('// 上面這個看起來像 Object,其實是個 string 哦!');
console.log(parsedData);

執行結果

{"locale":"zh-TW","popupAd":false,"currency":"TWD","shoppingCart":["612b06609ea3b35614c0edbd","6107a10580d7ca000a3c2357"]}
// 上面這個看起來像 Object,其實是個 string 哦!
{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
}

✔ 物件深層拷貝(deep copy)

要將資料從舊變數拷貝到新變數,而且希望新變數修改時不要影響到舊變數,往往會用到 spread operator:

const userData = {
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
};
const shoppingCart = ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357'];

const copiedUserData = { ...userData };
const copiedShoppingCart = [ ...shoppingCart ];

// 這個修改不會影響到舊的 userData 與 shoppingCart
copiedUserData.currency = 'USD';
copiedShoppingCart.push('60f94fe18ad4be000c7decb0');

console.log('舊的 userData');
console.log(userData);
console.log('新的 userData');
console.log(copiedUserData);
console.log('舊的 shoppingCart');
console.log(shoppingCart);
console.log('新的 shoppingCart');
console.log(copiedShoppingCart);

執行結果

舊的 userData
{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
}
新的 userData
{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'USD',
}
舊的 shoppingCart
['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
新的 shoppingCart
['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357', '60f94fe18ad4be000c7decb0']

但那是因為剛好 Object 跟 Array 裡面都只有 primitive 的資料,比如 string、boolean、number 等,若是裡面也有 non-primitive 的資料,比如 Object、Array、function,那這一招就會出現瑕疵:

// Object 裡面藏著一個 Array
const userData = {
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
};
const copiedUserData = { ...userData };

// 這個修改會影響到舊的 userData 內的 shoppingCart
copiedUserData.currency = 'USD';
copiedUserData.shoppingCart.push('60f94fe18ad4be000c7decb0');

console.log(userData);
console.log(copiedUserData);

執行結果

{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357', '60f94fe18ad4be000c7decb0']
}
{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'USD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357', '60f94fe18ad4be000c7decb0']
}

主要是因為 spread operator 只能做到淺層拷貝(shallow copy),代表只有拷貝 primitive 是複製「值」(value),而 non-primitive 則是複製「址」(reference)

那要怎麼做到深層拷貝呢?網路上解法很多種,但這篇主題是 Object,所以來看看 JSON.stringify / JSON.parse 這兩兄弟怎麼辦到的吧:

const userData = {
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
};
const copiedUserData = JSON.parse(JSON.stringify(userData));

// 這個修改不會影響到舊的 userData
copiedUserData.currency = 'USD';
copiedUserData.shoppingCart.push('60f94fe18ad4be000c7decb0');

console.log(userData);
console.log(copiedUserData);

執行結果

{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'TWD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357']
}
{
    locale: 'zh-TW',
    popupAd: false,
    currency: 'USD',
    shoppingCart: ['612b06609ea3b35614c0edbd', '6107a10580d7ca000a3c2357', '60f94fe18ad4be000c7decb0']
}

沒錯只要把 JSON.stringify / JSON.parse 這兩兄弟交疊(?)在一起就可以了!

但記得順序不要錯哦,因為先讓 Object 轉成 string,接著再 parse 回原本的 Object,雖然最後長得跟原本一模一樣,但經過了 primitive 與 non-primitive 的轉換,其實已經是完全不相關的兩個變數了

而這方式雖然方便,但其實是非常消耗效能的,因為 JSON.parse 在解析一個 JSON 字串的時候,需要把整個結構跑過一次,還需要檢查是否有 type error,以確保是一個合格健康(?)的 Object,因此除非很清楚資料量不會造成 performance issue,不然這個方式能免則免哦。

結語

Array 與 Object 是相輔相成的好夥伴,妥善運用兩者周遭的 method,並考慮到這兩者都是 non-primitive 的特性,往往已經足夠處理這個階段會遇到的問題了。

自己寫下自己的屬性
自己決定自己的命運

參考資料

Object(MDN)


上一篇
Day 3 - Array 陣列組合技 (2)
下一篇
Day 5 - 陣列與物件的進化 - Set & Map
系列文
Javascript 從寫對到寫好30

2 則留言

0
TD
iT邦新手 4 級 ‧ 2021-09-19 21:44:47

Object 的 keys/values/entries 真的是好用的工具!

1
Chiahsuan
iT邦新手 5 級 ‧ 2021-09-23 16:08:28

除非要送後端的欄位爆炸多,不然我還是傾向正向表列要後送的欄位,並將沒用到的欄位移除。

你好~看你的文章收穫非常多~
想請問傾向正向表列要後送的欄位,是什麼意思?可以從你提供的範例中看出這個概念嗎?非常感謝!!!

看更多先前的回應...收起先前的回應...

跟隨佳萱腳步來到這

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-23 20:01:58 檢舉

你們好啊:)

先說結論,正向表列(或正面表列)的意思是:「只有我提到的允許,我沒提到的就都不允許」,因此套用到我的 code 就是:「只有我列出來的欄位可以送到後端,我沒列的一律不准

所以,我列出來的欄位在這裡,只有這三個可以送到後端:

const submitFields = ['name', 'price', 'quantity'];

因此,原本有一個欄位叫做 redundant,因為不在我列出來的欄位中,所以是不允許的,我透過 delete 把它移除掉了

以下碎碎念:
其實我是跟以前的主管學到這句話的,當時也沒有去查證說這四個字代表什麼意思,剛剛才偷偷跑去 google,發現好像都是「政策」、「公文」之類比較正式的文件才會出現這種字,而且大部分的解釋我都看不懂XD

反之會有負向表列(負面表列),代表「只有我提到的不允許,我沒提到的就都允許」,其實都隱含有 include 與 exclude 的意思

正向表列: 只有操場可以打球
負向表列: 只有教室不可以打球

被這麼一問才覺得這問題博大精深(?),謝謝你的提問~

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-23 20:14:10 檢舉

啊附一下我看到的參考新聞XD
無人機正、負面表列哪個好?

Chiahsuan iT邦新手 5 級 ‧ 2021-09-23 22:11:57 檢舉

/images/emoticon/emoticon41.gif

謝謝你的回答~
很感謝你這系列的分享(剛剛從第一篇追到第七篇),覺得例子既實際,解釋又清楚,真的學到很多!非常期待你接下來的文章~~再次感謝!!!

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-24 12:48:21 檢舉

太棒了!「實際」與「清楚」是我寫文章非常重視的點,能讓你學到東西讓我也很開心!未來如果還有看不懂的東西,再歡迎你留言發問囉!

Chiahsuan iT邦新手 5 級 ‧ 2021-09-24 22:24:07 檢舉

好~~再次感謝~:)

我要留言

立即登入留言