對 Function 傳遞 Argument 是天天都會用到功能,ECMAScript 對 Argument 傳遞語法雖然精簡,但有些雷一不小心就會踩到,因此特別提出來討論。
Version
macOS Catalina 10.15.2
VS Code 1.40.2
Quokka 1.0.259
ECMAScript 2015+
Primitive
let data = 1
let fn = val => {
val = 2
return val
}
fn(data) // ?
data // ?
若傳入 argument 為 primitive,則會複製一份新 primitive 給 parameter,因此無論對 parameter 如何修改,也不會影響原本 primitive。
Object
let data = {
title: 'FP in JavaScript',
price: 100
}
let fn = obj => {
obj.title = 'RxJS in Action'
return obj
}
fn(data); // ?
data // ?
若傳入 argument 為 object,則不太一樣。
若為 object,則 variable 存的是 object 的 address,而非整個 object,因此傳入的 argument 會將 address 複製一份給 parameter,而非複製 object。
若對 object 的 propery 修改,因為 address 仍指向同一個 objcet,所以會影響到原本 object。
let data = {
title: 'FP in JavaScript',
price: 100
}
let fn = obj => {
let result = Object.assign({}, obj)
result.title = 'RxJS in Action'
return result
}
fn(data); // ?
data // ?
若要避免修改到原本 object,可用 Object.assign()
clone 出一份新 object,如此對 property 任何修改,都不會影響到原本 object。
let data = {
title: 'FP in JavaScript',
price: 100
}
let fn = obj => {
let result = { ...obj }
result.title = 'RxJS in Action'
return result
}
fn(data); // ?
data // ?
也可使用 ES6 的 spread operator 複製出新 object。
let data = {
title: 'FP in JavaScript',
price: 100
}
let fn = obj => {
obj = {
title: 'RxJS in Action',
price: 200
}
return obj
}
fn(data); // ?
data // ?
若將 parameter 重新指定 object 又不太一樣。
由於 parameter 存的只是 object 的 address,重新定義 object 只是指定新的 address 給 parameter,因此不會修改到原本 object。
Array
let data = [1, 2, 3]
let fn = arr => {
arr[1] = 4
return arr
}
fn(data); // ?
data // ?
Array 也是 object,因此情況與 object 類似。
若為 array,則 variable 存的是 array 的 address,而非整個 array,因此傳入的 argument 會將 address 複製一份給 parameter,而非複製 array。
若使用 array 的 index 修改,因為 address 仍指向同一個 array,所以會影響到原本 array。
let data = [1, 2, 3]
let fn = arr => {
let result = arr.slice()
result[1] = 4
return result
}
fn(data); // ?
data // ?
若要避免修改到原本 array,可用 Array.prototype.slice()
先 clone 出一份新 array,如此對 index 任何修改,都不會影響到原本 array。
let data = [1, 2, 3]
let fn = arr => {
let result = [...arr ]
result[1] = 4
return result
}
fn(data); // ?
data // ?
也可使用 ES6 的 spread operator 複製出新 array。
let data = [1, 2, 3]
let fn = arr => {
arr.push(4)
return arr
}
fn(data); // ?
data // ?
另一個常踩到的雷就是使用 push()
、pop()
、shift()
、unshift()
、sort()
、splice()
這類 destructive 寫法,他會透過 address 直接修改原本 array。
let data = [1, 2, 3]
let fn = arr => {
let result = arr.slice()
result.push(4)
return result
}
fn(data); // ?
data // ?
一樣可用 slice()
先 clone 出一份新 array,如此 push()
就不會影響到原本 array。
let data = [1, 2, 3]
let fn = arr => {
let result = [...arr]
result.push(4)
return result
}
fn(data); // ?
data // ?
也可使用 ES6 的 spread operator 複製出新 array。
let data = [1, 2, 3]
let fn = arr => {
arr = [1, 2, 3, 4]
return arr
}
fn(data); // ?
data // ?
若將 parameter 重新指定 array 又不太一樣。
由於 parameter 存的只是 array 的 address,重新定義 array 只是指定新的 address 給 parameter,因此不會修改到原本 array。
Conclusion
- ECMAScript 的 argument 傳遞其實都是
copy
,只是 primitive 是 copy value,因此沒 side effect;而 object 與 array 是 copy address,因此 copy 完之後仍然指向相同 object 或 array,所以會有 side effect - Object 或 array 要避免 side effect,就要 clone 出一份新的 object 或 array 再繼續處理
- ECMAScript 內建處理 object 或 array 都屬於 shallow copy,若要 deep copy 則要使用 Ramda 的
clone()
,可同時處理 object 或 array