iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
2
Modern Web

JavaScript基本功修煉系列 第 6

JavaScript基本功修練:Day6 - 傳址、傳值

  • 分享至 

  • xImage
  •  

在學習資料型別的同時,也需要了解資料如何在記憶體裏被存放。如果要深入探討這課題,其實也可以延伸出一些爭議點,例如到底JavaScript是不是只有傳值?還是也有傳址?但這篇文章不會跳這個深坑,畢竟基本功要先打好!!(很會找藉口

JavaScript存取資料的方法

  • 基本型別: 傳值(pass by value)
  • 物件型別: 傳址(pass by reference)、pass by sharing

以下這張圖簡單說明資料存放在記憶體時的情況。因為物件型別的值可以被修改,如果一併放在棧內存(stack)就會降低效能,所以棧內存只會存放該物件型別值的地址,並引用該地址來指向該物件。

基本型別

當一個變數被賦予基本型別的值時,整個值就會存在記憶體裏。當我們要拷貝基本型別的值到另一個變數,我們只會拷貝它們的,而該兩個變數並不會影響到對方。這個情況稱為傳值(pass by value)

看看以下例子:

var box1 = 10;
var box2 = 'hello';

//拷貝box1,box2的值
var boxA = box1;
var boxB = box2;

boxA = 30;
boxB = 'goodbye';

console.log(box1,box2,boxA,boxB) // 10,"hello",30,"goodbye"

下圖由左至右開始看,解釋了這段程式碼背後的動作。一開始boxAboxB只是各拷貝了box1box2的值,boxAbox1,以及box2boxB之間是沒有關係,所以當我要重新賦予新的值給boxAboxB時,box1box2不會被影響。

物件型別

但物件型別的存取方法就不同了。當變數被賦予的是物件型別的資料,記憶體會存放該物件在記憶體中的地址,並引用該地址來指向該物件

具體來說,請再看看此文第一張圖,如果變數的值是物件型別,棧內存(stack)會存放該物件在堆內存(heap)的地址,並引用該地址來指向該存放在堆內存(heap)的物件

所以,當我們要拷貝一個物件到另一個變數時,我們拷貝的是該物件的地址,換言之,如果物件有被修改,所有引用該物件地址的變數,它們的值都會被修改。

如以下例子:

var user = {
    name: 'Mary',
    age: 30
}

//拷貝user物件的地址
var userCopy = user; 
userCopy.age = 20;

console.log(user);
console.log(userCopy);

console:

比較一下:

具體的圖解:

但如果例子是這樣:

var user = {
    name: 'Mary',
    age: 30
}

var userCopy = user; 

userCopy = {
    name: 'Mary',
    age: 20
};

console.log(user);
console.log(userCopy);

console:

比較一下:

這是因為useCopy已經被重新賦予一個新的物件並非修改該物件,所以userCopy的地址整個變了,並指向另一個新的物件。

把基本型別當作參數傳入函式

當我們把基本型別資料,當成參數傳入函式時,函式的參數會拷貝了那些基本型別的值,所以在函式外的變數並不會被影響。

var box1 = 100;
var box2 = 200;

function add(a,b){
    a = 10;
    b = 20;
}

add(box1,box2);
console.log(box1,box2) //100,200

以上的例子中,像之前提及的傳值概念一樣,ab拷貝了box1box2的值。即使修改ab的值,box1box2都不會被修改。如下圖:

把物件型別當作參數傳入函式

如果把物件型別傳入函式呢?

var user = {
    name: 'Mary',
    age: 30
}

function change(obj){
    obj.name = 'Peter'
}

change(user);
console.log(user) //{name: "Peter", age: 30}

以上例子中,就如同之前提及傳址的概念一樣,這裏的obj拷貝了user的地址,所以當obj被修改了,user也會一併被修改。如下圖:

雖然這個情況與傳址的概念是一樣,但是如果在函式裏的obj被重新賦予一個新的物件時,奇怪的是user並不會被改變。

var user = {
    name: 'Mary',
    age: 30
}

function change(obj){
    obj = {
        name: 'Peter',
        age: 20
    }
}

change(user);
console.log(user) //{name: "Mary", age: 30}

針對這個情況,有些開發者主張用pass by sharing去形容這個過程會更精準。sharing有「共享」的意思,在這個例子中,意思就是objuser共用一個物件,就像我和你一起開一個google document,如果你修改了我寫的東西,我這邊也會看到。回到這個例子,如果在函式裏的obj被修改,user也會被修改。

雖然聽起來與傳址這個概念好像,但如果用pass by sharing去形容,就會更清說明「如果在函式裏的參數被重新賦值,該參數就會指向另一個新的值,函式外的變數不會被改動」的這個情況。

Pure function VS Impure function

從上面提及的概念中,可以看見如果我們把物件當作參數,傳遞到函式裏時,我們可以一併修改在函式內和外的物件,這樣的函式可以被稱為Impure function,意思是這個函式會汙染到函式外的變數。例如之前曾經提及的這個例子:

function impureFunc(person){
    person.age = 20;
    return person;
}

var mary = {
    name: 'Mary',
    age: 30
}

var maryChanged = impureFunc(mary);
console.log(mary); //{name: "Mary", age: 20}
console.log(maryChanged); //{name: "Mary", age: 20}

那麼Pure function呢?就是指即使不會汙染函式外的變數,例如以下例子:

function pureFunc(person){
    var newPerson = JSON.parse(JSON.stringify(person));
    newPerson.age = 20;
    return newPerson
}

var mary = {
    name: 'Mary',
    age: 30
}

var maryChanged = pureFunc(mary);
console.log(mary); //{name: "Mary", age: 30}
console.log(maryChanged); //{name: "Mary", age: 20}

把傳進來的物件轉成字串再轉成物件,從而建立新的物件,並放在newPerson這個變數中,再在那個新建的物件裏把age改成20,因此不會影響到變數mary的物件。但注意這個方法也會有一些限制,例如如果物件裏有函式或undefined,在轉換後它們會消失不見。

有些語法,例如Array.mapArray.filter,它們的原意就是跟Pure function一樣,因為不想修改到原本的陣列,所以這些語法會拿原本的陣列去做運算或處理,並產生出一個新陣列,而原本的陣列是不會被修改的。例如下面Array.map的例子:

var user = [
    {name: 'Mary', age: 30},
    {name: 'Jack', age: 20}
]

var userChanged = user.map(function(person){
    var newPerson = {
        name: person.name,
        age: person.age + 10
    };
    
    return newPerson
})

console.log(userChanged)
console.log(user)

總結

  • 基本型別存在棧內存(stack),物件型別存在堆內存(heap)
  • 基本型別:傳值
  • 物件型別:傳址
  • 基本型別的值當作參數傳入函式時,都是傳值
  • 物件當作參數傳入函式時,是傳址/pass by sharing
  • 如果要避免函式內會修改到在函式外的物件,可以在函式內先轉做字串,再轉成物件,但會有某些限制

參考資料

Explaining Value vs. Reference in Javascript
深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
JS 原力覺醒 Day12- 傳值呼叫、傳址呼叫
https://ithelp.ithome.com.tw/articles/10221506
重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
Professional JavaScript for Web Developers


上一篇
JavaScript基本功修練:Day5 - 宣告變數 - let、const、var
下一篇
JavaScript基本功修練:Day7 - [] == [] 、[]==![]、{} == {}、{} ==!{} 的解釋
系列文
JavaScript基本功修煉31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ChungKaiLu
iT邦新手 5 級 ‧ 2021-12-30 08:39:28

如果改成這樣,就會不一樣了
var user = {
name: 'Mary',
age: 30
}

function change(obj){
obj = {
name: 'Peter',
age: 20
}
return obj;
}

change(user);
console.log(change(user))//{name: 'Peter', age: 20}

我要留言

立即登入留言