iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 27
1
Modern Web

JavaScript 原力覺醒 - 成為絕地武士之路系列 第 27

JS 原力覺醒 Day27 - JS 常用 API - Object.assign && Object.defineProperty

今天要講的是是兩個在操作物件時常用到的 JS API ,有時候我們會需要做一些比較進階的操作,例如對物件屬性做一些比較細節的微調;還有複製物件,但是複製物件的話,因為物件傳參考的特性的關係,在結構複雜的物件上,往往需要特別處理,例如物件內的屬性是另外一個物件。所以我們也會帶到「深拷貝」和「淺拷貝」的概念。

Outline

  • Object.defineProperty
  • Object.assign
  • 深拷貝
  • 淺拷貝

Object.defineProperty

Object.defineProperty 其實是 Object 函式建構子上的靜態方法(還記得 Obejct 其實是一個函式?),用來對某個物件直接定義一個新的屬性,用法如下:

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false
}); 

這個方法接受三個參數,第一個是要新增屬性的目標物件,第二個是屬性名稱,第三個是這個屬性的描述器設定。 屬性的描述器?那是什麼?

JS 內物件屬性的描述器有兩種類型,每一種各有不同設定值:

  • 資料描述器 ( Data descriptor ):

    資料描述器是一個帶有值的屬性,其實也就是你要定義屬性的 value 啦。這個屬性有可能是可修改、或是不可被修改的。

  • 存取描述器 ( Accessor descriptor ):

    存取描述器定義的內容包含的 gettersetter 兩個函式。要怎麼存、取這個屬性,就是由存取描述器來負責的。

兩種描述器都有屬於自己的屬性設定值,先分別介紹:

  1. 資料描述器上,有兩個可選值:
  • value ( undefined ) : 定義這個屬性對應的值。
  • writable ( false ): 定義這個屬性是某可以被指派,如果為 true 就代表這個屬性可以透過 如 ob.name= 'new value' 被更新。
  1. 存取描述器上也有兩個可選值:
  • get ( undefined ) : 即物件的 getter 函式,是一個定義物件如何被取用的函式,當物件屬性被取用的時候會被呼叫。
  • set ( undefined ) : 即物件的 setter 函式,是一個定義物件如何被指派的函式。

剩下的幾個設定值是兩種描述器都能夠使用且可選、非必須的。分別是( 括號內的是預設值 ):

  • configurable ( false ) : 定義了這個物件屬性的描述器設定是否可以被修改,如enumerablewritable 、 或是自己本身 configurable
  • enumerable ( false ) : 定義這個屬性在物件裡就屬於可以被巡訪的,也就是使用 Object.keys 或是 for...in 來對物件作遍歷的時候能不能夠存取到。

而要定義的物件屬性的描述器必須一定要是上述兩者中的其中一種,兩者無法同時屬於兩者。

Example - 描述器預設值

var o = {}; // 創造新物件 

Object.defineProperty(o, 'a', {}); // empty descriptor setting

Object.getOwnPropertyDescriptor(o,'a')  
//預設描述器值:
// configurable : false 
// value : undefined 
// writable : false 
// enumable : false 

剛剛說描述器無法同時是資料描述器跟存取描述器,也就是說在 ****defineProperty 的第三個參數描述器設定內,如果有 get 這個設定值出現,就不能再有 value ,否則就會報錯:

var o = {}; // 創造新物件 

Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true,
	get(){
		return 123
	}
});

//Invalid property descriptor. Cannot both specify accessors and a value or writable attribute

Example - 自訂 gettersetter 函式

自訂 gettersetter 一樣是在 Object.defineProperty 裡面的第三個參數自訂屬性的行為:

var o = {}; 
Object.defineProperty(o, 'a', {
		get() {
        return 'It will return this value if I access o.a' ;
    },
    set() {
        this.myname = 'this is my name string';
    }
});

Object.assign

Object.assign 用來複製所有物件內可被尋訪 (Enumable) 的屬性,而且複製的來源不限於某個物件,可以多個物件一起進行屬性的複製,這個方法的第一個參數跟 defineProperty ㄧ樣都是目標物件,後面可以有複數個參數,就是要被複製屬性的來源。而使用 Object.assign 來進行複製的時候,後面的相同物件屬性會蓋掉前面相同的物件屬性:

let b = Object.assign({foo: 0}, {foo: 1}, {foo: 2});
ChromeSamples.log(b)
// {foo: 2}

所以,如果我想要複製某一物件的內容到一個全新的物件上的話,只要這麼寫:

let oldObject = {
	a:'a', 
	b:{
		c:'cinsideb'
	}
} 

let newObject = Object.assign({},oldObject)

console.log(newObject) //{a: "a", b: {…}}

另外,如果只是單純要把某個物件內容複製到另外一個物件,可以用 ES6 後的新的、比較簡潔好閱讀的寫法 Spread ,也可以達到一樣的效果:

let newObject = { ...oldObject }

淺拷貝 ( Shallow Copy )

在使用 Object.assign 時有一個要注意的地方,就是他雖然可以複製屬性,但要是物件屬性的內容也是另外一個物件時,從這個屬性複製到新物件上的,也只會是這個內層物件的參考,而不是這個物件的拷貝,這個現象就稱為淺拷貝(可以理解為,只複製最外層屬性,往下被複製的都只有參考)。

let oldObject = {
	a:'a', 
	b:{
		c:'c'
	}
} 

let newObject = Object.assign({},oldObject)

newObject.b.c = 'modified c' 

console.log(oldObject) 
/* {
	a:'a', 
	b:{
		c:'modified c'
	}
} */

由上就可以看出,當我修改新的物件的內層屬性物件時,被複製的物件的內層屬性物件 (b.c),也會跟著一起被改動。

深拷貝 ( Deep Clone )

相對於淺拷貝,深拷貝就是完全的複製整個物件內容了。那麼如果要達到這個效果,我們可能要自己動手處理,檢查要複製物件的某屬性是不是物件,如果是的話,就要再以Object.assgn 複製一次,並且這個檢查要搭配遞迴的概念來檢查,才能確保完全的複製。

function cloneDeep(obj){
            if( typeof obj !== 'object'  ){
                return obj
            }
            let resultData = {}
            return recursion(obj, resultData)
        }

function recursion(obj, data={}){
						//對物件屬性做巡訪
            for(key in obj){
                if( typeof obj[key] === 'object'){
										// 如果是物件就繼續往下遞迴
                    data[key] = recursion(obj[key])
                }else{
										// 如果不是物件的話就直接指派
                    data[key] = obj[key]
                }
            }
            return data
        }
let player = {name:'Anakin',friend:{robot:'R2D2'}}
let player2 = cloneDeep(player)
obj.name = 'Darth Vader!!!'
player2.friend.robot = 'no!!!'
console.log(player) // {name:'Anakin猿',friend:{robot:'R2D2'}}

參考文件

MDN 官方文件的說明

Javascript properties are enumerable, writable and configurable

JS-淺拷貝(Shallow Copy) VS 深拷貝(Deep Copy)


上一篇
JS 原力覺醒 Day26 - 常用 API: setTimeout / setTimeInterval
下一篇
JS 原力覺醒 Day28 - JS 裡的資料結構
系列文
JavaScript 原力覺醒 - 成為絕地武士之路30

尚未有邦友留言

立即登入留言