Day24 | JS 關於物件的傳參考、淺層拷貝、深層拷貝

❒ 物件傳參考的特性

JavaScript 賦予一個值到變數上時會有兩個特性

  1. 傳值 ( Call by Reference )
    • Boolean
    • Null
    • Undefined
    • Number
    • String
  2. 傳參考 ( Call by Sharing )
    • 物件 ( 陣列、函式 )

➊ 傳值 ( Call by Reference )

純值賦值是透過複製的方式,所以前者與後者各自獨立,當後者修改時不會影響前者。

純值:為基本型別 ( number、string、boolean、null、undefined )。

範例

1
2
3
var person = '小明';
var person2 = person;
person2 = '杰倫';
  • var person2 = person; 為把 person 的值帶過去 person2 就為傳值,純值傳值是一種複製的方式。
  • person2 接收了 person 的值後再另外賦予 person2 新的值就不會和 person 有關聯性。

➋ 傳參考 ( Call by Sharing )

物件賦值是透過傳參考的特性,所以前者與後者都共用同一個記憶體空間的參考路徑,後者修改時前者也會跟著修改。

  • 「 物件、陣列、函式 」皆為傳參考特性,會先另外建立一個記憶體空間把值寫入。
  • ❗ 注意:物件變數新增一個新的物件 {} 就會產生一個新的記憶體空間,就不會相互影響 ( 如下方範例 2. 3 )。

範例1. 物件傳參考

1
2
3
4
5
6
7
8
var person = {
name: '小明',
money: 1000,
};
var person2 = person;
person2.name = '杰倫';

console.log( person === person2 );

解析:

  • 程式碼中我們宣告了一個 person ( var person = { name: '小明', money: 1000,}; ) 表示在記憶體準備了一個參考路徑,這個參考位置包含了 namemoney。( 如下右圖,0x01 是為了記憶自訂義名稱 )
    示意圖

  • 當定義 person 為一個物件時,person 帶入的會是 0x01 這個記憶體的參考路徑,並不是帶入一個完整的物件內容。當參考路徑指向此物件時 ( 傳參考 ) 就可透過這種方式來取得裡面的 namemoney 屬性。所以 person 並不會把 namemoney 存到它的記憶體空間,只會傳入一個參考。
    示意圖

  • var person2 = person; 時會把 person 原本的參考位置一樣傳到 person2,所以 personperson2 共用的是同一個參考路徑 ( 右邊格子 0x01 ),所以 person2 修改時也會一併修改到 person

    • 這段程式碼只有產生一個參考路徑,這個參考路徑對應一個物件,所以只有產生一個物件,person2 person 兩者皆共用同一物件。
      示意圖
  • 物件賦予值是以傳參考方式進行。

  • 所以答案為 true

範例 2. 物件傳參考

1
2
3
4
5
6
7
8
var person = {
name: '小明',
money: 1000,
}
****var person2 = person;
person2 = { ... };

console.log( person === person2 );

示意圖

解析:

  • person2 指向一個新的空物件 { … },它就會產生另一個參考路徑 0x02,這時產生另一個物件也就是產生另一個參考路徑,所以 personperson2 就不會有任何關聯,互不影響。
  • 所以答案為 false

範例 3. 物件傳參考

1
2
3
4
5
6
7
8
var person = {
name: '小明',
}
****var person2 = person;
person2 = {
name: '小明',
}
console.log( person === person2 );
  • person2 = { name: '小明' } 中就算 person2person 內屬性與屬性值相同,但因分別為獨立的兩個物件兩個參考路徑,所以不會互相影響。
  • 所以答案為 false

❒ 物件傳參考實作範例

❗ 注意:新增一個新的物件就會產生一個新的記憶體空間,前者與後者不會相互影響。

實作 1

1
2
3
4
5
6
7
8
9
10
11
12
13
var family = {
name: '小明家',
members: {
father: '老爸',
mother: '老媽',
ming: '小明',
},
}
var member = family.members;
member = {
ming: '大明',
};
console.log(family);

解析:

  • var member = family.members; 時,memberfamily.members 都還是同個參考路徑。
    示意圖

  • member 賦予一個新的物件 member = { ming: '大明' } 開始,就拆成兩個參考路徑了,因為「 物件變數中看到新的 {} 就表示會產生一個新的記憶體空間 」。
    示意圖

  • 但是換成另種寫法 :member 直接修改屬性而非產生 {}member 就會和 family.members 使用同一個參考路徑,而它們兩個也會相互影響。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var family = {
    name: '小明家',
    members: {
    father: '老爸',
    mother: '老媽',
    ming: '小明',
    },
    }
    var member = family.members;
    member.ming = '大明';
    console.log(family);

實作 2

1
2
3
4
5
var a = {
x: 1,
}
a.y = a;
console.log(a);

解析:

  • 此寫法會造成無限循環,不斷指向自己。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    x: 1,
    y: {
    x: 1,
    y: {
    x: 1,
    y: {.... 無限循環}
    }
    }
    }
  • a 產生一個參考路徑 0x01 屬性為 xa 指向 0x01 參考路徑 → a 新增一個 y 屬性且屬性值為 a,指回原參考路徑 0x01。從 a 裡面找 y 就會指回原參考路徑一直指向自己造成無限循環。
    示意圖

實作 3

1
2
3
4
5
6
7
8
9
10
var a = {
x: 1,
}
var b = a;
a.y = a = {
x: 2,
}
console.log(a.y);
console.log(b);
console.log(a === b.y);

拆解:

  • var b = a; 這時變數 ba 都還是同一個參考路徑 0x01。
    物件傳參考拆解01

  • 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 )。
      物件傳參考拆解02

答案:

  • console.log(a.y); 印出 undefined
    • a 參考路徑變 0x02 而裡面並沒有 y 屬性,所以為 undefined
  • console.log(b); 印出 { x:1, y: { x:2 } }
  • console.log(a === b.y); 印出 true

實作 4

1
2
3
4
5
6
var a = { x: 1};
var b = a;
a.x = { x: 2 };
a.y = a = { y: 1};
console.log(a); // 結果?
console.log(b); // 結果?

解析:
物件傳參考範例四拆解

  • var b = a; 時,ab 都還是同個參考路徑 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
2
3
4
5
6
7
8
9
10
//a
{ y: 1 }

//b
x: {
x: 2
}
y: {
y:1
}

❒ 淺層複製與深層複製

由於物件有傳參考的特性,所以當兩物件需要拆開處理,就會產生一些困擾,這時就可使用 for in、淺層複製、深層複製來解決這個問題。

❒ 淺層拷貝 ( shallow Copy )

缺點: for in 方式只能做到第一層的複製,第一層以上的都還是會有傳參考特性。

➊ for in

結構: for ( var key in 原物件名稱 ) {},當中的 key 為原物件的屬性。

範例 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var family = {
name: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
},
}

var newFamily = {};
for (var key in family) {
newFamily[key] = family[key];
}
// 物件內第一層成功拷貝,family與newFamily各自獨立
newFamily.name = '大明家';
console.log(family, newFamily);

// 物件內第二層還是有傳參考特性,所以會一併修改到 family.members.ming
newFamily.members.ming = '大明';
console.log(family.members.ming === newFamily.members.ming);

CodePen 範例

  • 因為淺層拷貝只能移除第一層的傳參考特性,所以 console.log(family.members.ming === newFamily.members.ming); 會為 true

➋ 使用 jQuery 方式

記得先載入 jQuery CDN

結構: jQuery.extent({}, 原物件名稱)

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var family = {
name: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
},
}

var newFamily = jQuery.extend({}, family);
newFamily.name = '大明';
console.log(family, newFamily);
newFamily.members.ming = '大明';
console.log(family.members.ming === newFamily.members.ming);
  • 因為淺層拷貝只能移除第一層的傳參考特性,所以 console.log(family.members.ming === newFamily.members.ming); 會為 true

➌ Object.assign ( ES6 方式 )

結構: Object.assign({}, 原物件名稱)

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var family = {
name: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
},
}

var newFamily = Object.assign({}, family);
newFamily.name = '大明';
console.log(family, newFamily);
newFamily.members.ming = '大明';
console.log(family.members.ming === newFamily.members.ming);
  • 因為淺層拷貝只能移除第一層的傳參考特性,所以 console.log(family.members.ming === newFamily.members.ming); 會為 true

展開的方式 ( 筆者較常用此方式 )

結構: { ...原物件名稱 }

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var family = {
name: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
},
}

var newFamily = { ...family };
newFamily.name = '大明';
console.log(family, newFamily);
newFamily.members.ming = '大明';
console.log(family.members.ming === newFamily.members.ming);
  • 因為淺層拷貝只能移除第一層的傳參考特性,所以 console.log(family.members.ming === newFamily.members.ming); 會為 true

❒ 深層拷貝

不論物件裡面有幾層,都可透過深層拷貝的方式移除物件傳參考的特性。

使用方式: 把原本物件轉為字串再轉回物件,這種方式可以移除傳參考的特性。

結構:

  • 使用 JSON 方式把原物件轉為字串 → JSON.stringify(原物件名稱)
  • 透過 JSON.parse 把字串再轉回物件 → JSON.parse(JSON.stringify(原物件名稱))

範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var family = {
name: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
},
}
var newFamily = JSON.parse(JSON.stringify(family));

newFamily.name = '大明';
console.log(family, newFamily);

// 深層拷貝後,改了原物件第二層內的值也不會影響原物值,兩物件 family 與 newFamily各自獨立
newFamily.members.ming = '大明';
console.log(family.members.ming === newFamily.members.ming);

CodePen 範例

  • 因為深層拷貝可以移除物件內所有層的傳參考特性,所以 console.log(family.members.ming === newFamily.members.ming); 會為 false

淺層拷貝範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function changeName(data) {
data.name = '杰倫家';
return data;
}
var family = {
name: '小明',
home: '小明家',
members: {
father: '老爸',
mom: '老媽',
ming: '小明',
}
}

var family2 = Object.assign({}, changeName(family));
family2.members.jay = '杰倫';

console.log(`family.name, ${family.name}`);
console.log(`family.members.jay, ${family.members.jay}`);
console.log(family === family2);
console.log(family.members === family2.members);

示意圖

  • 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
2
3
4
console.log(`family.name, ${family.name}`); // 杰倫家
console.log(`family.members.jay, ${family.members.jay}`); //杰倫
console.log(family === family2); // false
console.log(family.members === family2.members); // true

參考資訊

  • 六角學院 - JavaScript 核心篇