# JavaScript 的「傳值」與「傳址」 >[竹白記事本](https://chupai.github.io/),筆記紀錄。 ###### tags: `JavaScript 竹白記事本` 這部分在 JavaScript 是相當重要的觀念,正確了解這個觀念能避免發生不必要的錯誤。 ## ▶ 概述 在 Javascript 中,值的傳遞分為兩種: - **傳值**,Call by value 或是 Pass by value。 - **傳址(傳參考)**,Call by reference 或是 Pass by reference。 在其他程式語言有語法可以決定要「傳值」還是「傳址」,但在 JavaScript 中沒有選擇,**基本型別** 就是「傳值」行為,**物件型別** 就是「傳址」行為。 ## ▶ 傳值 當值的型別為 **基本型別**,那麼永遠都是藉由值的複製來賦值,所以稱作 **傳值**。 來看程式碼: ```javascript var a = 10; var b = 10; a === b ; // console.log: true ``` - 由於 `10` 的型別為 **基本型別**; - 當我們比較 `a` 與 `b` 時,是互相比較彼此的值,所以回傳 `true`。 繼續來看另一段程式碼: ```javascript var a = 2; var b = a; b += 1; a; // console.log: 2 b; // console.log: 3 ``` - 當 `b` 指定為 `a` 時,由於 `a` 的值是 **基本型別**; - 所以會 `b` 得到的是 `a` 的值而不是 `a` 的記憶體位址; - 因此 `b` 就算改變了,`a` 也不會受到影響,兩者是獨立的。 ## ▶ 傳址 接下來,我們來看 **物件型別** 的情況。 ```javascript var obj1 = { a: 1 }; var obj2 = { a: 1 }; obj1 === obj2; // console.log: false ``` 你可以觀察到,就算 `obj1` 和 `obj2` 的屬性名稱與值都相同,但互相比較的結果卻是 `false`。 這是因為每個物件都是獨立存在的實體,兩者的記憶體位置並不相同。在比較 **物件型別** 時,比較的是記憶體位置,而非值。 繼續來看另一段程式碼: ```javascript var obj1 = { a: 1 }; var obj2 = obj1; obj1.a = 0; obj2.a; // console.log: 0 obj2.b = 2; obj1.b; // console.log: 2 obj1 === obj2; // console.log: true obj1 = {}; obj1 === obj2; // console.log: false ``` - 將 `obj2` 透過 `obj2 = obj1` 的方式賦值; - 當我們修改任意物件屬性時,另一邊的屬性也會更動; - 這是因為兩個變數指向相同的記憶體位置,並沒有新的物件被複製出來; - 但當我們將變數 `ob1` 賦值新的物件時,`obj1` 會指向新的記憶體位置; - 而 `obj2` 依然保持原物件記憶體位置,這時 `obj1` 和 `obj2` 彼此之間就沒有關係了,所以比較的結果為 `false`。 **物件型別** 之間的傳遞是記憶體位置指向,所以稱作 **傳址**。 ## ▶ 函式參數 如果一個 **物件型別** 作為函式參數傳遞,一樣是傳址行為: ```javascript function foo(obj) { obj.a = 10; } var obj1 = {}; foo(obj1); obj1; // console.log: { a: 10 } ``` 直接更改屬性,會影響原物件。 如果我們將參數重新指向新物件,那麼不影響原物件: ```javascript function foo(obj) { obj = { a: 10 }; } var obj1 = {}; foo(obj1); obj1; // console.log: {} ``` ## ▶ 複製物件 物件型別是傳參考,那我們該如何複製物件呢? 複製物件的方式分為兩類: - 淺拷貝(Shallow Copy) - 深拷貝(Deep Copy) ### 1.淺拷貝操作 常見淺拷貝物件的方式: - 使用 [`Object.assign()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 將原本的 `obj` 內容複製到一個空物件中。 - 在一個空物件內,使用 `...` 展開運算子展開物件。 ```javascript var obj = { foo: 1, bar: 2}; var obj2 = Object.assign({}, obj); var obj3 = { ... obj }; obj === obj2; // console.log: false obj2; // console.log: { foo: 1, bar: 2 } obj === obj3; // console.log: false obj3; // console.log: { foo: 1, bar: 2 } ``` 常見淺拷貝陣列的方式: - 利用會回傳新陣列的方法,例如 [`slice()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)、[`concat()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) [`map()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map)等等。 - 在一個空陣列內,使用 `...` 展開運算子展開陣列。 ```javascript var arr = [1, 2, 3, 4]; var arr2 = arr.slice(0); var arr3 = [].concat(arr); var arr4 = arr.map((item)=> item); var arr5 = [...arr]; arr === arr2; // console.log: false arr2; // console.log: [1, 2, 3, 4] arr === arr3; // console.log: false arr3; // console.log: [1, 2, 3, 4] arr === arr4; // console.log: false arr4; // console.log: [1, 2, 3, 4] arr === arr5; // console.log: false arr5; // console.log: [1, 2, 3, 4] ``` ::: success **總結**,淺拷貝使用 `...` 展開運算子就對了,簡單易懂。 ::: ### 2. 什麼是淺拷貝 以上拷貝方式有一個問題,請考慮下面程式碼: ```javascript var obj1 = { a: 10, b: { foo: 20, }, }; var obj2 = { ...obj1 }; obj1 === obj2; // console.log: false obj1.b === obj2.b; // console.log: true ``` 假如物件內還有物件,就算我們使用 `...` 展開運算子複製物件,第二層物件還是指向相同的記憶體位置。 因為我們的複製操作只改變外層容器的地址,但沒動到內層,所以這種複製操作稱為「淺拷貝」。也就是說,當我們的物件或陣列是巢狀或多維的,要多注意。 ### 3. 深拷貝 深拷貝就是完全複製一份,不會有共用記憶體的問題。 常見深拷貝物件方法有: - 利用 JSON 方法:先轉 JSON 格式,再轉回來。 - 使用第三方函式庫: - jQuery 的 [`$.extend()`](http://www.html.cn/jqapi-1.9/jQuery.extend/) - Lodash 的 [`_.cloneDeep()`](https://www.lodashjs.com/docs/lodash.cloneDeep) #### 3.1 JSON 先用 `JSON.parse()` 轉再 JSON 格式,再用 `JSON.stringify()` 轉回 JS 物件: ```javascript var obj1 = { a: 10, b: { foo: 20, }, }; var obj2 = JSON.parse(JSON.stringify(obj1)); obj1 === obj2; // console.log: false obj1.b === obj2.b; // console.log: false ``` 但有幾點要注意,當物件轉 JSON 格式時, - 函式、`undefined`、`Symbol` 會被忽略; - 而 `NaN`、`Infinity`、`-Infinity` 會被轉成 `null`。 所以利用 JSON 方法深拷貝物件,只能用在單純只有資料的物件。 #### 3.2 jQuery jQuery 是常見的 JS 函式庫之一。 `jQuery.extend()`,是用來將兩個或更多物件的內容合併到第一個物件,我們可以用它來複製一個全新的物件。 ```javascript var obj1 = { a: 10, b: { foo: 20, }, }; var obj2 = $.extend(true, {}, obj1); obj1 === obj2; // console.log: false obj1.b === obj2.b; // console.log: false ``` 第一個參數預設為 `fasle`(可省略),如果是 `false` 就是淺拷貝,`true` 為深拷貝。 #### 3.3 Lodash Lodash 是熱門 JS 工具函式庫之一。 `_.cloneDeep(value)`,能深拷貝一個物件: ```javascript var obj1 = { a: 10, b: { foo: 20, }, }; var obj2 = _.cloneDeep(obj1); obj1 === obj2; // console.log: false obj1.b === obj2.b; // console.log: false ```