Day24 | JS 關於物件的傳參考、淺層拷貝、深層拷貝
❒ 物件傳參考的特性
JavaScript 賦予一個值到變數上時會有兩個特性
- 傳值 ( Call by Reference )
- Boolean
- Null
- Undefined
- Number
- String
- …
- 傳參考 ( Call by Sharing )
- 物件 ( 陣列、函式 )
➊ 傳值 ( Call by Reference )
純值賦值是透過複製的方式,所以前者與後者各自獨立,當後者修改時不會影響前者。
純值:為基本型別 ( number、string、boolean、null、undefined )。
範例
1 | var person = '小明'; |
var person2 = person;
為把person
的值帶過去person2
就為傳值,純值傳值是一種複製的方式。person2
接收了person
的值後再另外賦予person2
新的值就不會和person
有關聯性。
➋ 傳參考 ( Call by Sharing )
物件賦值是透過傳參考的特性,所以前者與後者都共用同一個記憶體空間的參考路徑,後者修改時前者也會跟著修改。
- 「 物件、陣列、函式 」皆為傳參考特性,會先另外建立一個記憶體空間把值寫入。
- ❗ 注意:物件變數新增一個新的物件
{}
就會產生一個新的記憶體空間,就不會相互影響 ( 如下方範例 2. 3 )。
範例1. 物件傳參考
1 | var person = { |
解析:
程式碼中我們宣告了一個
person
(var person = { name: '小明', money: 1000,};
) 表示在記憶體準備了一個參考路徑,這個參考位置包含了name
和money
。( 如下右圖,0x01 是為了記憶自訂義名稱 )
當定義 person 為一個物件時,person 帶入的會是 0x01 這個記憶體的參考路徑,並不是帶入一個完整的物件內容。當參考路徑指向此物件時 ( 傳參考 ) 就可透過這種方式來取得裡面的
name
與money
屬性。所以person
並不會把name
與money
存到它的記憶體空間,只會傳入一個參考。
var person2 = person;
時會把person
原本的參考位置一樣傳到person2
,所以person
和person2
共用的是同一個參考路徑 ( 右邊格子 0x01 ),所以person2
修改時也會一併修改到person
。- 這段程式碼只有產生一個參考路徑,這個參考路徑對應一個物件,所以只有產生一個物件,
person2
person
兩者皆共用同一物件。
- 這段程式碼只有產生一個參考路徑,這個參考路徑對應一個物件,所以只有產生一個物件,
物件賦予值是以傳參考方式進行。
所以答案為
true
。
範例 2. 物件傳參考
1 | var person = { |
解析:
- 把
person2
指向一個新的空物件{ … }
,它就會產生另一個參考路徑 0x02,這時產生另一個物件也就是產生另一個參考路徑,所以person
與person2
就不會有任何關聯,互不影響。 - 所以答案為
false
。
範例 3. 物件傳參考
1 | var person = { |
person2 = { name: '小明' }
中就算person2
與person
內屬性與屬性值相同,但因分別為獨立的兩個物件兩個參考路徑,所以不會互相影響。- 所以答案為
false
。
❒ 物件傳參考實作範例
❗ 注意:新增一個新的物件就會產生一個新的記憶體空間,前者與後者不會相互影響。
實作 1
1 | var family = { |
解析:
在
var member = family.members;
時,member
與family.members
都還是同個參考路徑。
member
賦予一個新的物件member = { ming: '大明' }
開始,就拆成兩個參考路徑了,因為「 物件變數中看到新的{}
就表示會產生一個新的記憶體空間 」。
但是換成另種寫法 :
member
直接修改屬性而非產生{}
,member
就會和family.members
使用同一個參考路徑,而它們兩個也會相互影響。1
2
3
4
5
6
7
8
9
10
11var family = {
name: '小明家',
members: {
father: '老爸',
mother: '老媽',
ming: '小明',
},
}
var member = family.members;
member.ming = '大明';
console.log(family);
實作 2
1 | var a = { |
解析:
此寫法會造成無限循環,不斷指向自己。
1
2
3
4
5
6
7
8
9
10{
x: 1,
y: {
x: 1,
y: {
x: 1,
y: {.... 無限循環}
}
}
}a
產生一個參考路徑 0x01 屬性為x
→a
指向 0x01 參考路徑 →a
新增一個y
屬性且屬性值為a
,指回原參考路徑 0x01。從a
裡面找y
就會指回原參考路徑一直指向自己造成無限循環。
實作 3
1 | var a = { |
拆解:
var b = a;
這時變數b
與a
都還是同一個參考路徑 0x01。
a.y = a = { x: 2 };
a = { x: 2 };
為運算式,所以x: 2
會賦予到y
上,a.y = a = { x: 2 };
這行是同時執行的沒有執行順序的問題,會等於a = a.y = { x: 2 };
。所以a.y
的參考路徑會是原本的a
參考路徑 0x01 而非a
新的參考路徑 0x02。a
新增一個y
屬性,其屬性值為 a ( 參考物件 0x01 ) → 屬性值a
新增一個物件,只要新增物件就會新增一個參考路徑,這邊a
新增物件的新參考路徑為 0x02 裡面有x
屬性與屬性值 2 (a.y
參考路徑由 0x01 變 0x02 )。
答案:
console.log(a.y);
印出undefined
。- a 參考路徑變 0x02 而裡面並沒有 y 屬性,所以為
undefined
。
- a 參考路徑變 0x02 而裡面並沒有 y 屬性,所以為
console.log(b);
印出{ x:1, y: { x:2 } }
。console.log(a === b.y);
印出true
。
實作 4
1 | var a = { x: 1}; |
解析:
- 到
var b = a;
時,a
與b
都還是同個參考路徑 0x01。 a.x = { x: 2 };
,a.x
被賦予新物件,當有新物件就會產生一個新的參考路徑,所以 0x01 的x
屬性值參考路徑變成 0x02。a.y = a = { y: 1};
為運算式所以它們會同時執行沒有順序問題,而a.y = a
為最原始的 a 參考路徑 0x01,所以 0x01 多一個y
屬性,y
的屬性值為新的參考路徑 0x03 ( 有新物件就會產生一個新的參考路徑 )。
答案:
1 | //a |
❒ 淺層複製與深層複製
由於物件有傳參考的特性,所以當兩物件需要拆開處理,就會產生一些困擾,這時就可使用 for in、淺層複製、深層複製來解決這個問題。
❒ 淺層拷貝 ( shallow Copy )
缺點: for in
方式只能做到第一層的複製,第一層以上的都還是會有傳參考特性。
➊ for in
結構: for ( var key in 原物件名稱 ) {}
,當中的 key
為原物件的屬性。
範例 1.
1 | var family = { |
- 因為淺層拷貝只能移除第一層的傳參考特性,所以
console.log(family.members.ming === newFamily.members.ming);
會為true
。
➋ 使用 jQuery 方式
記得先載入 jQuery CDN
結構: jQuery.extent({}, 原物件名稱)
範例
1 | var family = { |
- 因為淺層拷貝只能移除第一層的傳參考特性,所以
console.log(family.members.ming === newFamily.members.ming);
會為true
。
➌ Object.assign ( ES6 方式 )
結構: Object.assign({}, 原物件名稱)
範例
1 | var family = { |
- 因為淺層拷貝只能移除第一層的傳參考特性,所以
console.log(family.members.ming === newFamily.members.ming);
會為true
。
❹ …
展開的方式 ( 筆者較常用此方式 )
結構: { ...原物件名稱 }
範例
1 | var family = { |
- 因為淺層拷貝只能移除第一層的傳參考特性,所以
console.log(family.members.ming === newFamily.members.ming);
會為true
。
❒ 深層拷貝
不論物件裡面有幾層,都可透過深層拷貝的方式移除物件傳參考的特性。
使用方式: 把原本物件轉為字串再轉回物件,這種方式可以移除傳參考的特性。
結構:
- 使用 JSON 方式把原物件轉為字串 →
JSON.stringify(原物件名稱)
- 透過
JSON.parse
把字串再轉回物件 →JSON.parse(JSON.stringify(原物件名稱))
範例
1 | var family = { |
- 因為深層拷貝可以移除物件內所有層的傳參考特性,所以
console.log(family.members.ming === newFamily.members.ming);
會為false
。
淺層拷貝範例
1 | function changeName(data) { |
- 在
Object.assign
執行淺層拷貝時,先執行了changeName(data)
,物件是傳參考概念而函式也是物件所以也具有傳參考特性。 changeName(data)
中 data 參數換成 family,changeName(family)
裡面執行了family.name = '杰倫家';
會一併修改到family
的參考路徑中name
的屬性值,所以family.name
會印出杰倫家
。- 因為淺層拷貝只會移除第一層的傳參考特性 ( 複製 0x01 第一層到 0x03 ),所以
family2.members.jay = '杰倫';
中members
內為第二層還是保有傳參考特性 ( 0x03 中的 members 參考路徑還是 0 )。
答案:
1 | console.log(`family.name, ${family.name}`); // 杰倫家 |
參考資訊
- 六角學院 - JavaScript 核心篇