參數 (parameters) vs. 引數 (arguments)
先來看看 MDN 的說明:
參數 parameter:A parameter is a named variable passed into a function. Parameter variables are used to import arguments into functions.
引數 argument:An argument is a value (primitive or object) passed as input to a function.
「函式參數(parameters)」是定義函式時所列出的變數,用來將引數導入至函式中;「函式引數(arguments)」則是實際上輸入至函式或是函式收到的值。
// 定義函式,設定「參數」
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
。
// 沒有傳入引數
var print = function (x,y){
console.log(x,y)
}
print() // undefined, undefined
只有傳入一個引數也不會報錯,並且可由此得知 JavaScript 是由左至右讀取參數:
print(3)// 3, undefined
為什麼沒有傳入引數的時候,參數的值會是 undefined
呢?這是由於 Javascript 提升(hoisting)的特性。在函式的提升中,Javascript 在編譯時會先宣告函式的參數 name
和 age
,若參數沒有值則會賦予 undefined
的值。
預設參數 Default Parameters
如果想要避免在呼叫函式時沒傳參數(或是傳入 undefined
)導致出現 undefined
的狀況,ES6 允許我們在定義函式時為參數設定指定的預設值。
透過 =
為參數賦予預設值:
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),保險起見也可以使用另一種寫法:
function greet(name, age) {
name = name || 'Alice' // name 的預設值
age = age || 28 // age 的預設值
console.log(`${name} is ${age} years old.`)
}
由於在沒有帶入預設值的情況下,參數值為 undefined
。這時藉由 ||
運算子來為參數賦值:當 ||
左側 undefined
(falsy value) 被強制轉型成 false
時,會回傳 ||
右側的值,也就是我們希望的預設值(number = Alice
、age = 28
)。
其餘參數 rest parameters
其餘參數(rest parameters)可以讓我們用來表示不確定數量的參數,並將其視為一個陣列。
function(a,b,...args){
// ...
}
每當我們使用 ...args
作為最後一個傳遞給函式的參數時,表示剩餘的引數物件 (arguments object) 會被轉為陣列。...
為「其餘運算子(rest operator)」,args
可以是任何名稱。
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 轉為陣列
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()
將陣列原地反轉,或是排序 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
物件取得呼叫函式時所傳入的引數。
function fn(a,b,c) {
console.log(arguments)
console.log(arguments[0])
}
fn(1,2,3)
這邊提到「arguments
會傳入『所有』傳入函式的值」的意思是,即使傳入的值多於函式所需參數,arguments
還是會接收所有傳入的值;如果傳入的值少於函式所需參數,也不會出現 undefined
的值。
傳入的值多於函式所需參數:
傳入的值少於函式所需參數:
arguments
vs. 陣列
需要注意的是,「類陣列」只是長得很像陣列,實際上並不是真的陣列,因此不能使用 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()
方法,讓我們可以將物件轉為陣列:
function accumulate() {
console.log(arguments)
let numbers = Array.from(arguments)
return numbers.reduce((accum, num) => {
return accum + num
})
}
accumulate(1,2,3)
使用其餘參數將 arguments
轉為陣列
如同上面介紹到關於其餘參數所提到的,如果函式的(最後一個)命名參數是以 ...
開頭,引數物件便會被視為一個陣列,此時便可以使用陣列的所有操作方法。
function accumulate(...args) {
console.log(args)
return args.reduce((accum, num) => {
return accum + num
})
}
accumulate(4,5,6)
使用 call()
將 arguments
轉為陣列
使用 call
函式將 this
指向 arguments
物件:
function accumulate() {
const argsArray = Array.prototype.slice.call(arguments)
return argsArray.sort()
}
accumulate(3,2,1) // [1,2,3]
上面的程式碼中,我們將 slice
函式所指向的 this
透過 call
函式來指向 arguments
物件;也可以看作將 arguments
物件當作 this
來呼叫 slice
函式。
由於 slice
函式沒有輸入引數,將回傳原陣列,所以 argsArray
是陣列,含有 3 個元素。
關於 Javascript 的 this
以及使用 call()
改變 this
指向,可以參考先前整理的筆記:Javascript - this 是誰、指向哪裡,以及 call、apply、bind
引數的傳值(by value)與傳址(by reference)
先來看看這個例子:
// 定義變數 (primitive)
let myName = 'Alice'
function fn(myName) {
// 在函式內改變值
myName = 'Bob'
console.log(myName)
}
// 呼叫函式並帶入變數
fn(myName) // Bob
console.log(myName) // Alice --> 外部變數的值沒有改變
上面這個例子中,我們:
定義了一個原始資料型別(primitive type)的變數
myName
定義了一個函式
fn
及其參數myName
(也可以命名為任意的名字) ,並在函式內改變了參數myName
的值呼叫函式並帶入引數
fn(myName)
並console.log
變數myName
可以看到即使在函式內改變 arguments,也不會改變函式外部的變數的值。
再來看看這個例子:
// 定義變數 (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)」的方式傳遞,傳入函式的是該物件型別的變數的「記憶體參考位址」,因此在函式內改變了物件的值,函式外部的物件也會被改變。
只有當物件被賦予新的值的時候,才會再建立一個新的記憶體位置,此時就不再指向同一個位置:
// 定義變數 (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}