JavaScript 的參數傳遞

1. 傳值 & 傳址

call by value 傳值 (原始型別的特性)

  • 每個宣告的變數的值(value)都會儲存在不同的記憶體空間
  • 使用容量較小,原始型別的資料儲存在 Stack 內
  • 複製變數時,會完全複製一份新的「值 (value)」
let a = 6
let b = a
a = 9
console.log(b) // 6

Stack

宣告的變數儲存的值記憶體空間
a6空間 A
b6空間 B
  • 使用 == , === 比較時,是比較值是否相同
let a = 6
let b = a

console.log(a == b) //true
console.log(a === b) //true
  • 函式接收到的參數值是原始值的一個複製
  • 當函式修改參數的值時,原始值不會被改變
let age = 18
function increase(age) {
  age++
}

increase(age)
console.log(age) // 18;

call by reference 傳址 (物件型別的特性)

  • 儲存資料值所在的記憶體位置的地址 (address)
  • 物件類別的資料通常較為複雜,佔用記憶體容量較大,會儲存在 Heap 內
  • 複製變數時,是複製一份新的「地址 (address)」,並非複製值 (value)
  • 注意重點: 原物件是否會被修改
//因為是儲存記憶體位置,所以用const宣告的陣列可以被修改
const arr = [55, 66]
arr.push(77)

//值會被變更
console.log(arr) // [55,66,77]

Stack

宣告的變數儲存的值
arr記憶體位址 0x00

Heap

記憶體位址
0x0055,66,77
  • 相同的地址會指向同樣的值
// object example
//x有一個記憶體位址 0x01 指向儲存的值 {value:10}
let x = { value: 10 }

//y有同一個記憶體位址 0x01 指向相同的值
let y = x
x.value = 20
console.log(x) // {value: 20}
console.log(y) // {value: 20}

//array example
let animals = ['zebra', 'bear', 'deer']
let animals2 = animals
animals2.push('lion')
console.log(animals) // ['zebra','bear','deer','lion']

Stack

宣告的變數儲存的值
x記憶體位址 0x01
y記憶體位址 0x01
animals記憶體位址 0x02
animals2記憶體位址 0x02

Heap

記憶體位址
0x01{ value: 20}
0x02'zebra' ,'bear', 'deer', 'lion'
  • 使用 == , === 比較時,是比較記憶體位址是否相同
//記憶體位址不同
console.log([] == []) //false
console.log([10] == [10]) //false
console.log(['hello', 'bye'] === ['hello', 'bye']) // false

//有相同的值和記憶體位址
let arr1 = [1, 2, 3] //0x01
let arr2 = arr1 //0x01

console.log(arr1 == arr2) //true
console.log(arr1 === arr2) //true

//有相同的值,但記憶體位址不同
let arr3 = [4, 5, 6] //0x01
let arr4 = [4, 5, 6] //0x02

console.log(arr3 == arr4) //false
console.log(arr3 === arr4) //false

2. 淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)

  • 因為物件類別有 call by reference 特性,如果不想修改原物件,可以使用一些方法來複製一份
  • 例如想要新增物件屬性,但希望保留原本的物件資料時

淺拷貝

  • 只複製物件第一層屬性 (primitive type)
  • 修改物件第一層屬性的值不會影響原物件
  • 若有第二層以上結構時 (reference type),位址仍然相同,指向同樣的值
  • 修改物件第二層屬性的值會影響原物件
  1. 方法 1:展開運算子 (Spread Operator)
//範例:複製物件
const jacket = { brand: 'UNIQLO', color: 'navy' }
const Jacket2 = { ...jacket }

console.log(newJacket) // { brand: 'UNIQLO', color: 'navy' }

//範例:複製陣列
const dinner = ['pizza', 'hamburger', 'sandwich']
const dinner2 = [...dinner]

console.log(dinner2) // ['pizza','hamburger','sandwich']
  1. 方法 2: Object.assign()
//語法
Object.assign(target, ...sources)

//範例:複製物件
const obj = { a: 1 }
const copy = Object.assign({}, obj)
console.log(copy) // { a: 1 }
  1. 方法 3: Array.prototype.slice()
  • 會回傳一個新陣列物件
  • 為原陣列選擇之 begin 至 end(不含 end)部分的淺拷貝(shallow copy)
  • 原本的陣列不會被修改
//語法
arr.slice([begin[, end]])

const drinks = ['tea','coke','milk']

//寫法1: 不給參數,會全部複製一份
const mydrinks = drinks.slice()

console.log(mydrinks) // ['tea', 'coke', 'milk']

//寫法2:小括號內放0,結果同上
const mydrinks = drinks.slice(0)

console.log(mydrinks) // ['tea', 'coke', 'milk']
  1. 範例
  • 如果物件內有 reference type(Object,Array...),是複製其位址,仍指向相同的值
//example1
//改變第一層屬性的值,不影響原物件
const car1 = {
  brand: 'tesla',
  color: 'white',
  detail: {
    model: 'ModelX',
    year: '2023'
  }
}

const car2 = Object.assign({}, car1)

car2.color = 'black'

console.log(car1)
/*{
  brand: 'tesla',
  color: 'white',
  detail: { model: 'ModelX', year: '2023' }
}*/

console.log(car2)
/*{
  brand: 'tesla',
  color: 'black',
  detail: { model: 'ModelX', year: '2023' }
}*/

//example2
//改變第二層物件的值,連原物件也一起改變
let car2 = Object.assign({}, car1)

car2.detail.model = 'Model3'

//因為car1和car2內的detail共享同個位址
//當改變car2的detail內model的值時,car1也會同樣被改變

console.log(car1)
/*{
  brand: 'tesla',
  color: 'white',
  detail: { model: 'Model3', year: '2023' }
}*/

console.log(car2)
/*{
  brand: 'tesla',
  color: 'white',
  detail: { model: 'Model3', year: '2023' }
}*/

深拷貝

  • 深度複製指定物件,操作新物件不影響原物件
  • 兩者指向不同記憶體位址

方法 1: 使用JSON.stringify/parse

//深拷貝方法 JSON.stringify/parse

const myBand = {
  name: 'The Strokes',
  members: {
    vocal: 'Julian',
    guitarist1: 'Albert',
    guitarist2: 'Nick',
    bassist: 'Nikolai',
    drummer: 'Fabrizio'
  }
}

//使用JSON.stringify將myBand轉成字串
//再透過JSON.parse將字串轉回物件

const newBand = JSON.parse(JSON.stringify(myBand))
console.log(myBand === newBand) // false

newBand.members.vocal = 'Kurt'

// myBand不會被改變
console.log(myBand)

/*{
  name: 'The Strokes',
  members: {
    vocal: 'Julian',
    guitarist1: 'Albert',
    guitarist2: 'Nick',
    bassist: 'Nikolai',
    drummer: 'Fabrizio'
  }
}*/

console.log(newBand)
/*{
  name: 'The Strokes',
  members: {
    vocal: 'Kurt',
    guitarist1: 'Albert',
    guitarist2: 'Nick',
    bassist: 'Nikolai',
    drummer: 'Fabrizio'
  }
}*/

方法 2: 使用structuredClone

  • JavaScript 的內建方法
const myBand = {
  name: 'The Strokes',
  members: {
    vocal: 'Julian',
    guitarist1: 'Albert',
    guitarist2: 'Nick',
    bassist: 'Nikolai',
    drummer: 'Fabrizio'
  }
}

const yourBand = structuredClone(myBand)
console.log(yourBand === myBand) // false
yourBand.members.drummer = 'Ringo'

console.log(myBand)
/*{
  name: 'The Strokes',
  members: {
    vocal: 'Julian',
    guitarist1: 'Albert',
    guitarist2: 'Nick',
    bassist: 'Nikolai',
    drummer: 'Fabrizio'
  }
}*/

console.log(yourBand)
// {
//   "name": "The Strokes",
//   "members": {
//     "vocal": "Julian",
//     "guitarist1": "Albert",
//     "guitarist2": "Nick",
//     "bassist": "Nikolai",
//     "drummer": "Ringo"
//   }
// }

參考資料

# JavaScript