點燈坊

失くすものさえない今が強くなるチャンスよ

深入探討 Function 傳遞 Argument

Sam Xiao's Avatar 2019-12-13

對 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。

arg000

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。

arg001

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。

arg002

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。

arg003

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。

arg004

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。

arg007

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。

arg008

let data = [1, 2, 3]

let fn = arr => {
  let result = [...arr ]
  result[1] = 4

  return result
}

fn(data); // ?
data // ?

也可使用 ES6 的 spread operator 複製出新 array。

arg009

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。

arg005

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。

arg006

let data = [1, 2, 3]

let fn = arr => {
  let result = [...arr]
  result.push(4)

  return result
}

fn(data); // ?
data // ?

也可使用 ES6 的 spread operator 複製出新 array。

arg010

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。

arg011

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

Reference

Mooji, JS 原力覺醒 Day 12 - 傳值呼叫、傳址呼叫