JS30 全攻略 第14天

前言

JS 30 是由加拿大的全端工程師 Wes Bos 免費提供的 JavaScript 簡單應用課程,課程主打 No Frameworks、No Compilers、No Libraries、No Boilerplate 在30天的30部教學影片裡,建立30個JavaScript的有趣小東西。

另外,Wes Bos 也很無私地在 Github 上公開了所有 JS 30 課程的程式碼,有興趣的話可以去 fork 或下載。


本日目標

瞭解 JavaScript 的 Passed by ValuePassed by Reference 以及 Object 的 shallow copydeep copy


解析程式碼

JS 部分

Passed by Value

在 JavaScript 裡,如果變數的資料型態屬於 Primitive(原生型別),則在傳遞變數時,採用的是 Passed by Value (傳遞值)。

常見的 Primitive:
  • String
  • Number
  • BigInt
  • Boolean
  • Symbol

在下面的兩個例子,我們分別將agename傳遞給age2name2,之後改變age2name2的數值。結果原本的agename均不會受到影響,因為在傳遞過程是 Passed by Value,將原本存於agename的值複製一份給age2name2

1
2
3
4
5
6
let age = 100;
let age2 = age;
console.log(age,age2);

age = 200;
console.log(age,age2);
1
2
3
4
5
6
let name = 'Mes';
let name2 = name;
console.log(name,name2);

name = 'wesley';
console.log(name,name2);

  • 簡易概念圖

Passed by Reference

在 JavaScript 裡,如果變數的資料型態屬於 Object(物件型別),則在傳遞變數時,採用的是 Passed by Reference (傳遞位址)。

常見的 Object:

  • Array
  • Object

下面的例子,我們將陣列 players 傳遞給常數 team,之後將team[3](Poppy)改成Lux,再把兩個陣列都印到 console,會發現到兩個陣列的第四個位置都被修改成新的值(Lux)。

這是因為物件在傳遞的過程中採取的是 Passed by Reference,也就是將物件在記憶體上的位置傳遞給另一個物件(兩者共享同一個記憶體位置),所以只要更改其中一方,另一方也會受到影響。

1
2
3
4
5
6
const players = ['Wes','Sarah','Ryan','Poppy'];
const team = players;
console.log(players,team);

team[3] = 'Lux';
console.log(players,team);

如果想修改陣列的值又不影響另一個陣列的話,我們可以使用slice()來複製陣列並回傳新陣列(擁有自己的記憶體空間)。

下面的例子,我們呼叫players.slice()將陣列players完整的複製一份到新陣列中並傳遞給team2,之後再修改team2的第四個位置,這次我們發現到原來的陣列players並沒有被修改到。

注意:slice()進行的是shallow copy(淺複製)!!!

1
2
3
4
const team2 = players.slice()
team2[3] = 'Lux';

console.log(players,team2)

其他修改陣列的值又不影響另一個陣列的方法有:

  • concat() : 將多個陣列進行串聯和slice()一樣會回傳串聯後的新陣列。
  • Spread : 將陣列中的元素展開並逐個放到新陣列中。
  • Array.from() : 建立一個新的 Array 實體。
1
2
3
4
5
6
const team3 = [].concat(players);

//use the new ES6 Spread
const team4 = [...players];

const team5 = Array.from(players);

另一個例子,我們宣告物件person並把它傳遞給captain,在captain上新增number: 90後將兩個物件都印到 consle,此時會發現在captain新增的number: 90,也會被加到person上。會這樣是因為物件在傳遞過程是 Passed by Reference,兩物件同時指向一個記憶體空間。

1
2
3
4
5
6
7
8
9
const person = {
name: 'Wes Bos',
age: 80
};

const captain = person;
captain.number = 99;

console.log(person,captain);

如果不想修改到另一個物件的話,我們可以使用Object.assign(target, ...sources)來複製一個或多個物件的屬性到另一個目標物件,最後回傳目標物件。

下面我們利用Object.assign({},person,{number: 99,age:12});,將物件person{number:99,age:12}的屬性複製到目標物件({ })。

如果複製的多個物件的屬性有重複,以後面物件的屬性為準進行合併。舉例來說age是重複出現的屬性,最後複製屬性值時,要以後面出現的age:12為準合併物件。

注意 : Object.assign()做的是 shallow copy(淺複製),如果要複製的物件屬性包含子物件,就會複製到子物件的參照(reference)!!!。

1
2
3
4
5
6
7
const person = {
name: 'Wes Bos',
age: 80
};

const cap2 = Object.assign({},person,{number: 99,age:12});
console.log(person,cap2);

原來的person並沒有被改動到。

  • 簡單概念圖

Shallow Copy & Deep Copy

前面我們有提到,Object.assign()做的是 shallow copy(淺複製),若要複製的物件屬性包含子物件,就會複製到子物件的參照。

所以只要去改動新物件的子物件屬性值,就會連帶影響原本複製的物件子屬性值。

下面我們用Object.assign()複製來源物件(wes)的屬性到目標物件({}),再將目標物件傳遞給dev物件,要留意wes的屬性包含子物件屬性(social)。

例子中我們修改複製來的物件屬性(name)和子物件屬性(social.twitter),之後把wesdev物件印到 console 會發現複製的來源物件wes的屬性(name)不受影響,但wes的子物件屬性(social.twitter)卻被影響。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const wes = {
name: 'Wes',
age: 100,
social: {
twitter: '@wesbos',
facebook: 'wesbos.developer'
}
}

//shallow copy
const dev = Object.assign({},wes);
dev.name = 'Wesley';
console.log(wes,dev);

dev.social.twitter = '@coolman';
console.log(wes.social,dev.social);

如果不想改動複製來源物件的子屬性值(social.twitter),我們可以將要複製的物件用JSON.stringify()先轉換成 JSON String(因為是Primitive,所以是 Passed by Value),再用JSON.parse()把它還原成物件再回傳給dev2

下面我們一樣修改子物件的屬性值,但這次的原物件子屬性值並沒有受到影響。

1
2
3
4
5
6
7
8
9
10
11
12
13
const wes = {
name: 'Wes',
age: 100,
social: {
twitter: '@wesbos',
facebook: 'wesbos.developer'
}
}

// deep copy
const dev2 = JSON.parse(JSON.stringify(wes));//先轉成string再換回object
dev2.social.twitter = '@coolman';
console.log(wes.social,dev2.social);

補充資料:

JavaScript: var, let, const 差異
Array.prototype.concat()
Spread syntax (…)
Array.from()
Object.assign()
JSON.stringify()
JSON.parse()

分享到