# JavaScript - 函式中的參數(parameters)與引數(arguments) ## 函式參數 (parameters) vs. 函式引數 (arguments) 先上 MDN 的說明: > [參數 parameter](https://developer.mozilla.org/en-US/docs/Glossary/Parameter):A parameter is a named variable passed into a function. Parameter variables are used to import arguments into functions. >[引數 argument](https://developer.mozilla.org/en-US/docs/Glossary/Argument):An argument is a value (primitive or object) passed as input to a function. 「函式參數(parameters)」是定義函式時所列出的變數,用來將引數導入至函式中;「函式引數(arguments)」則是實際上輸入至函式或是函式收到的值。 ```javascript // 定義函式,設定「參數」 function greet(name, age){ console.log(`${name} is ${age} years old.`) } // 呼叫函式,傳入「引數」 greet('Alice', 28) // Alice is 28 years old. ``` 上述的例子中 `name` 和 `age` 是參數;`'Alice'` 和 `28` 則是引數。 ### 未傳入引數的參數值為 `undefined` 如果定義函式時設定了參數,卻在呼叫函式時沒有傳入引數,函式仍然可以正常運作,不過引數的值會是 `undefined`。 ```javascript // 沒有傳入引數 var print = function (x,y){ console.log(x,y) } print() // undefined, undefined ``` 只有傳入一個引數也不會報錯,並且可由此得知 JavaScript 是由左至右讀取參數: ```javascript print(3)// 3, undefined ``` 為什麼沒有傳入引數的時候,參數的值會是 `undefined` 呢?這是由於 Javascript [提升(hoisting)]([hoisting](https://hackmd.io/miMzEpGjQh-ck8t0j_4bHw))的特性。在[函式的提升](https://hackmd.io/miMzEpGjQh-ck8t0j_4bHw?view#%E5%87%BD%E5%BC%8F%E7%9A%84%E6%8F%90%E5%8D%87)中,Javascript 在編譯時會先宣告函式的參數 `name` 和 `age` ,若參數沒有值則會賦予 `undefined` 的值。 ## 預設參數 Default Parameters 如果想要避免在呼叫函式時沒傳參數(或是傳入 `undefined`)導致出現 `undefined` 的狀況,ES6 允許我們在定義函式時為參數設定指定的預設值。 透過 `=` 為參數賦予預設值: ```javascript function greet(name = 'Alice', age = '28') { console.log(`${name} is ${age} years old.`) } greet() // Alice is 28 years old. greet('Bob', 18) // Bob is 18 years old. ``` 上面的例子中,`greet()` 沒有傳入任何引數,因此函式的參數值為預設值。 不過這樣的寫法不是所有瀏覽器都有支援(例如:IE),保險起見也可以使用另一種寫法: ```javascript function greet(name, age) { name = name || 'Alice' // name 的預設值 age = age || 28 // age 的預設值 console.log(`${name} is ${age} years old.`) } ``` 由於在沒有帶入預設值的情況下,參數值為 `undefined`。這時藉由 [`||` 運算子](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR)來為參數賦值:當 `||` 左側 `undefined`(falsy value) 被強制轉型成 `false`時,會回傳 `||` 右側的值,也就是我們希望的預設值(`number = Alice`、`age = 28`)。 ## 其餘參數 rest parameters [其餘參數(rest parameters)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters)可以讓我們用來**表示不確定數量的參數**,並將其**視為一個陣列**。 ```javascript function(a,b,...args){ // ... } ``` 每當我們使用 `...args` 作為最後一個傳遞給函式的參數時,表示剩餘的引數物件 (arguments object) 會被轉為陣列。`...`為「其餘運算子(rest operator)」,`args` 可以是任何名稱。 ```javascript function fn(a,b, ...restArgs) { console.log('a', a) // a 1 console.log('b', b) // b 2 console.log('more args:', restArgs) // more args: [3,4,5] } fn(1,2,3,4,5) ``` ### 使用其餘參數將 arguments 轉為陣列 ```javascript function fn(...args) { console.log(args) // [1,2,3] console.log(args[0]) // 1 console.log(args.reverse()) // [3,2,1] } fn(1,2,3) ``` 上面的例子中,我們在定義函式 `fn` 時使用了其餘參數 `...args`。 當我們將 1,2,3 作為引數傳遞給函式 `fn()` 時, `args` 會將傳入的引數蒐集在一個陣列中,讓我們能夠以陣列的形式獲得 arguments 物件。 如此一來,我們就可以在 `fn()` 函式內部取用 `args` 陣列,並對其使用陣列的各種操作方法,像是[`reverse()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse) 將陣列原地反轉,或是排序 `sort()`、過濾 `filter()` ⋯等。 ## 引數物件 arguments > `arguments` is an array-like object accessible inside functions that contains the values of the arguments passed to that function. `arguments` 引數物件是傳入函式的一種**類陣列物件(Array-like object)**,包含著「所有」傳入函式的值。 有時候在定義函式時我們並不確定需要設定幾個參數, `arguments` 讓我們在定義函式時就算未設定參數,仍然可以透過 `arguments` 物件取得呼叫函式時所傳入的引數。 ```javascript function fn(a,b,c) { console.log(arguments) console.log(arguments[0]) } fn(1,2,3) ``` ![](https://hackmd.io/_uploads/H1gCCGiT3.png) 這邊提到「`arguments` 會傳入『所有』傳入函式的值」的意思是,即使傳入的值多於函式所需參數,`arguments` 還是會接收所有傳入的值;如果傳入的值少於函式所需參數,也不會出現 `undefined` 的值。 傳入的值多於函式所需參數: ![傳入的值多於函式所需參數](https://hackmd.io/_uploads/HyUopGo62.png) 傳入的值少於函式所需參數: ![傳入的值少於函式所需參數](https://hackmd.io/_uploads/r1avRMoah.png) ### `arguments` vs. 陣列 需要注意的是,「類陣列」只是長得很像陣列,實際上並不是真的陣列,因此不能使用 Javascript 陣列內建的操作方法。 ```javascript function fn(a,b,c){ return arguments.sort() } fn(1,2,3) // Uncaught TypeError: arguments.sort is not a function ``` `sort` 是陣列的一個方法,但 `arguments` 並非陣列,因此使用 `arguments.sort` 會報錯。 總結 `arguments` 和 `array` 的相同/相異之處: - 都有以 `0` 為開始的索引值 - 都有 `length` 屬性可以用 - `agruments` 比陣列多了一個 `callee` 屬性:表示目前執行在哪個函式內 - `arguments` 並不是陣列,不能使用陣列的操作方法(像是 `forEach`, `map` ...等) ### 將 `arguments` 轉為陣列 雖然 `arguments` 並不是陣列,我們仍然可以透過一些方法將 `arguments` 轉為陣列來使用陣列的操作方法。 #### 使用 `Array.from()` 將 `arguments` 轉為陣列 ES6 提供了 `Array.from()` 方法,讓我們可以將物件轉為陣列: ```javascript function accumulate() { console.log(arguments) let numbers = Array.from(arguments) return numbers.reduce((accum, num) => { return accum + num }) } accumulate(1,2,3) ``` ![](https://hackmd.io/_uploads/H1IXXmj62.png) #### 使用其餘參數將 `arguments` 轉為陣列 如同上面介紹到關於其餘參數所提到的,如果函式的(最後一個)命名參數是以 `...` 開頭,引數物件便會被視為一個陣列,此時便可以使用陣列的所有操作方法。 ```javascript function accumulate(...args) { console.log(args) return args.reduce((accum, num) => { return accum + num }) } accumulate(4,5,6) ``` ![](https://hackmd.io/_uploads/HkMHn7o63.png) #### 使用 `call()` 將 `arguments` 轉為陣列 使用 `call` 函式將 `this` 指向 `arguments` 物件: ```javascript function accumulate() { const argsArray = Array.prototype.slice.call(arguments) return argsArray.sort() } accumulate(3,2,1) // [1,2,3] ``` 上面的程式碼中,我們將 [`slice`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) 函式所指向的 `this` 透過 [`call`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/call) 函式來指向 `arguments` 物件;也可以看作將 `arguments` 物件當作 `this` 來呼叫 `slice` 函式。 由於 `slice` 函式沒有輸入引數,將回傳原陣列,所以 `argsArray` 是陣列,含有 3 個元素。 :::info 關於 Javascript 的 `this` 以及使用 `call()` 改變 `this` 指向,可以參考先前整理的筆記:[Javascript - this 是誰、指向哪裡,以及 call、apply、bind](https://hackmd.io/KLkyl-z_SCeukUzO4R16oQ) ::: ### 引數的傳值(by value)與傳址(by reference) 先來看看這個例子: ```javascript // 定義變數 (primitive) let myName = 'Alice' function fn(myName) { // 在函式內改變值 myName = 'Bob' console.log(myName) } // 呼叫函式並帶入變數 fn(myName) // Bob console.log(myName) // Alice --> 外部變數的值沒有改變 ``` 上面這個例子中,我們: 1. 定義了一個原始資料型別(primitive type)的變數 `myName` 2. 定義了一個函式 `fn` 及其參數 `myName`(也可以命名為任意的名字) ,並在函式內改變了參數 `myName` 的值 3. 呼叫函式並帶入引數 `fn(myName)` 並 `console.log` 變數 `myName` 可以看到即使在函式內改變 arguments,也不會改變函式外部的變數的值。 再來看看這個例子: ```javascript // 定義變數 (object) let myObj = { name: 'Alice', age: 18, isMale: false } function fn(myObj) { // 在函式內改變值 myObj.name = 'Bob' console.log(myObj.name) } // 呼叫函式並帶入變數 fn(myObj) // 'Bob' console.log(myObj.name) // 'Bob' --> 外部變數的值改變了! ``` 但在這個例子中,外部變數 `myObj` 的值卻被函式內部改變了。 當我們在傳入引數時,需要注意函式傳入的引數是傳值(passed by value)還是傳址(passed by reference)。 #### 基本型別傳值 (Arguments are Passed by Value) 如果傳入的 arguments 的值為基本型別的值(Primitive type),像是 `null`, `undefined`, `boolean`, `number`, `string`...,則為「傳值(pass by value)」,那麼在函式中改變 arguments 的值並不會影響函式外的變數的值。 #### 物件型別傳址(Objects are Passed by Reference) 如果傳入的 arguments 的值為物件型別的值(Object type),像是 `object`、`array`、`function`⋯,因為物件型別是以「傳址(pass by reference)」的方式傳遞,傳入函式的是該物件型別的變數的「記憶體參考位址」,因此在函式內改變了物件的值,函式外部的物件也會被改變。 只有當物件被**賦予新的值**的時候,才會再建立一個新的記憶體位置,此時就不再指向同一個位置: ```javascript // 定義變數 (object) let myObj = { name: 'Alice', age: 18, } function fn(myObj) { // 賦予新值,引數 `myObj` 將指向新的記憶體位置,不再影響外部變數 `myObj` myObj = { name: 'Bob' } console.log(myObj) } // 呼叫函式並帶入變數 fn(myObj) // { name: 'Bob'} console.log(myObj) // {name: 'Alice', age: 18} ``` :::info [Javascript 中的傳值 by value 與傳址 by reference](https://medium.com/itsems-frontend/javascript-pass-by-value-reference-sharing-5d6095ae030b) ::: ## Ref - [[JavaScript] 函式中的參數 (parameters), 引數 (arguments), 預設值 (default parameters), 和其餘運算子 (rest operator)](https://medium.com/itsems-frontend/javascript-parameters-and-arguments-5844261fe6cd) - [call函式 & arguments物件](https://ithelp.ithome.com.tw/articles/10207798)