0%

物件

物件結構

建立物件有兩個方式 :

  • 物件實字
  • 建構式物件

物件實字比起建構式更容易閱讀,也不需要額外繼承 Object() ,多數人推薦以此方式建立物件,而建構式物件則是學習建構式或者原型時再來使用。

1
2
3
4
5
6
7
8
let objA = {
a: 1,
b: 2
}

let objB = new Object()
objB.a = 1
objB.b = 2

物件操作(取值、新增、刪除)

取值

可以使用兩種方式來取得物件的屬性值 : 

  1. 使用點記號來取值 .
    • 必須使用字串去取得屬性名稱
    • 無法使用 .數值 取得屬性值,例如 obj.1
  2. 使用中括號與字串來取值 ['屬性名稱']
    • 可以利用變數名稱帶入
1
2
3
4
5
6
7
8
9
10
11
12
13
let b = 'a'
let obj = {
a: 1,
1: '1',
fn(){
return '我是函式'
}
}
console.log(obj.1) // 報錯 Unexpected number
console.log(obj['1']) // 1,使用中括號與字串就能正確取值
console.log(obj.fn()) // '我是函式'
console.log(obj['fn']()) // '我是函式'
console.log(obj[b]) // 1

新增

一樣可以透過點記號與中括號新增值。

1
2
3
let obj = {}
obj.cat = '阿美'
obj['dog'] = '小黑'

刪除

  • 物件屬性可以經由 delete 刪除
  • 但已宣告的變數無法透過 delete 刪除
1
2
3
4
5
6
7
8
9
10
11
let zoo = {}
zoo.cat = '阿美'
zoo['dog'] = '小黑'
delete zoo.cat
delete zoo['dog']
console.log(zoo) // { }

// 物件屬性可以被刪除、已宣告的變數不行
console.log(delete zoo.cat) // true
console.log(delete zoo['dog']) // true
console.log(delete zoo) // false

物件與純值

物件可以新增純值,但純值無法新增屬性

1
2
3
4
5
6
let cat = '阿美'
let zoo = {}
zoo.cat = cat
cat.dog = '小黑'
console.log(zoo) // { cat:'阿美' }
console.log(cat) // '阿美'

物件與預設值

所有的變數在建立的時候,JS均會給予一個 undefined 的值,當我們賦予這個變數值時,才會是正常看到的。

1
2
3
4
5
6
7
8
9
10
let a 
let obj = {
b: 1
}
console.log(e) // e is not defined
// 在呼叫變數 e 時,就會因為 e is not defined而中斷執行
// 可以透過物件取值的方式, window.e 讓執行可以繼續
console.log(a) // 當第一行為 window.e,會得到 undefined
console.log(obj.c) // 當第一行為 window.e, 會得到 undefined

物件傳參考

只要看到大括號 {} ,就會與原來的物件區隔開來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 賦予純值
let objA = {
x: 1,
}
let b = objA
b.x = 2
console.log(objA.x) // 2,objA.a的值會被修改
console.log(b === objA) // true

// 賦予物件,參考位置已被區隔
let objB = {
x: 1,
}
let c = objB
c = {
x: 2
}
console.log(objB.x) // 1,objB.a的值不會被修改
console.log(c === objB) // false

物件複製

Call by Reference 與 Call by Sharing

在 JS中,物件是 Call by Reference 還是 Call by Sharing ?
在維基百科的求值策略中提到,JS的物件是屬於 Call by Sharing,但在取值與賦予值的過程中反而會讓人認為是其他概念。那麼,根據目的而分類它是那一種效果,而非一定是屬於那一種效果,或許比較容易理解這些概念。

Call by Reference

將原物件賦予至新變數上,修改其值後,原物件也一併被修改,因為它們參照同一個位址上的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const refit = (car, color) => {
let newColor = car
newColor.color = color
return newColor
}
let carA = {
age: 20,
color: 'white'
}
let carB = refit(carA, 'red')
console.log(carA, carB)
// Object {
// age: 20,
// color: "white"
// } Object {
// age: 20,
// color: "white"
// }
console.log(carA === carB) // true

Call by Sharing

將物件複製後,修改其值,不會將原來的物件一併修改,因為使用了新物件做區隔。但這屬於淺層複製,後面會有淺層與深層複製的介紹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const refit = (car, color) => {
// Object.assign({}, 物件)
// 複製目標物件的所有屬性至空物件
// 也可以將多個物件屬性合併
let newColor = Object.assign({}, car)
newColor.color = color
return newColor
}
let carA = {
age: 20,
color: 'white'
}
let carB = refit(carA, 'red')

console.log(carA, carB)
// Object {
// age: 20,
// color: "white"
// } Object {
// age: 20,
// color: "red"
// }
console.log(carA === carB) // false

Call by Value

複製 x的值給 y,y 重新賦予值不會修改 x的值

1
2
3
4
let x = 1
let y = x
y = 2
console.log(x, y) // 1 2

淺層與深層複製

在上面的範例中,有使用到 Object.assign() 來複製物件內容,但這樣只有複製了第一層,若其中某個屬性值也是物件,也就是原物件的子物件,當修改這個子物件時,還是會修改到原物件的子內容。

淺層複製 shallow copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let carA = {
age: 20,
color: 'white',
other: {
door: 4,
SRS: 2
}
}
// 第一種方式 for in
let newCar = {}
for (let key in carA){
newCar[key] = carA[key]
}

// 第二種方式 jQuery
// let newCar = jQuery.extend(true, {}, carA)

// 第三種方式 ES6
// let newCar = Objecct.assign({}, carA)

newCar.other.SRS = 1
console.log(carA, newCar)
// 原物件的子物件也一併被修改
// Object {
// age: 20,
// color: 'white'
// other: {
// door: 4,
// SRS: 1
// }
// }
// Object{
// age: 20,
// color: 'white'
// other: {
// door: 4,
// SRS: 1
// }
// }
console.log(carA === newCar) // false

深層複製 deep copy

藉由 JSON轉字串( JSON.stringify(物件) )再轉回物件( JSON.parse(JSON字串) ),就能完全複製出一個新物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let carA = {
age: 20,
color: 'white',
other: {
door: 4,
SRS: 2
}
}
let newCar = JSON.parse(JSON.stringify(carA))

newCar.other.SRS = 1
console.log(carA, newCar)
// Object {
// age: 20,
// color: 'white'
// other: {
// door: 4,
// SRS: 2
// }
// }
// Object{
// age: 20,
// color: 'white'
// other: {
// door: 4,
// SRS: 1
// }
// }
console.log(carA === newCar) // false

不過,使用 JSON互轉的方式會遇到一些問題,像是屬性值若為函式、undefined會消失,Nan則為 null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let motorcycle = {
name: 'Z900RS',
equipped: {
headlight: 'round'
},
fn(){
console.log('123');
},
un: undefined,
nan: NaN,
}
let newMotorcycle = JSON.parse(JSON.stringify(motorcycle))
console.log(newMotorcycle)
// Object{
// name: 'Z900RS',
// equipped: {
// headlight: 'round'
// },
// nan: null
// },

第三方插件的深層複製

能深層複製且沒有上述問題的方式,目前找到的有以下,經過 codePen測試(在 JS設定中匯入 CDN),可以深層複製也沒有假值的問題。

  • jQuery
    • jQuery.extend(true, {}, 目標物件)
    • https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
  • lodash
    • _.cloneDeep(objects)
    • https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js

非第三方插件之深層複製

若不依靠第三方插件,想自己動手試著複製,需要考慮到以下幾點,或者更多 :

  • 判斷型別,typeof
  • 循環引用,callback
  • 建構式
  • 假值判斷
  • 物件相依關係

這邊附上測試後的確可運作的實例做為參考,Gary - 14. [JS] 深拷貝是什麼?如何實現?

物件與陣列

  • 物件、函式、陣列的型別都是屬於 object
  • 陣列使用中括號 [ ]
  • 每個索引位址都能賦予純值、物件、陣列、函式、布林值等
  • 可以新增屬性,但不影響原有陣列長度
  • 可以跳過索引賦予值,但是會有空的索引位置,預設為 undefined
1
2
3
let fn = () => {return 1}
let arr = [1, , '3', fn, true, , [], {}]
console.log(arr[1]) // undefined

物件與JSON

  • 轉成字串時,屬性會使用雙引號 "屬性名稱"
  • 撰寫 JSON格式時,無法使用物件的形式撰寫,也就是說屬性名稱必須要有單或雙引號

參考來源

  1. Charles Huang - JS基本觀念:call by value 還是reference 又或是 sharing?
  2. Ray - JavaScript 核心觀念(29)-物件-Call by Reference 還是 Call by Sharing
  3. huli - 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
  4. wikipedia - 求值策略
  5. MDN - Object.assign()
  6. stackoverflow - Deep copy in ES6 using the spread syntax
  7. jQuery - jQuery.extend()
  8. lodash - _.cloneDeep(value)
  9. Gary - 14. [JS] 深拷貝是什麼?如何實現?