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 11 - log服务想说的话

>>:  DAY7-PHP和MYSQL(一)

[Day 21] Leetcode 560. Subarray Sum Equals K (C++)

前言 今天这题也是来自top 100 liked的题目,题目是:560. Subarray Sum ...

Day 29 - ROS 树莓派光达履带小车实作 (3)

昨天把lidar配置完成,并且准备好做SLAM的工具,但还有一个最重要的功能就是,让车子动起来~~ ...

从零开始的8-bit迷宫探险【Level 18】为什麽他们开始乱跑?捉摸不定的怪物移动模式

「诶,好累喔。」 「天气这麽好,要不要去玩水啊?」喜欢玩耍的 Snow 提议着。 忘记要赶走山姆的...

面对自己阴暗面的修练

不管是Scrum Master或者扮演Product Owner,抑或是传统的专案管理者,在整个专案...

IT 铁人赛 k8s 入门30天 -- day5 k8s run tools: minikubes 安装与 kubectl 安装

前言 一般的 k8s 丛集都是多个Control Plane 还有多个 Node 然而在资源不足的情...