函數(shù)參數(shù)是函數(shù)內(nèi)部跟函數(shù)外部溝通的橋梁。下面本篇文章就來帶大家了解一下JavaScript函數(shù)中的參數(shù),希望對大家有所幫助!
一、函數(shù)的形參和實參
函數(shù)的參數(shù)會出現(xiàn)在兩個地方,分別是函數(shù)定義處和函數(shù)調(diào)用處,這兩個地方的參數(shù)是有區(qū)別的。
-
形參(形式參數(shù))
在函數(shù)定義中出現(xiàn)的參數(shù)可以看做是一個占位符,它沒有數(shù)據(jù),只能等到函數(shù)被調(diào)用時接收傳遞進來的數(shù)據(jù),所以稱為形式參數(shù),簡稱形參。
-
實參(實際參數(shù))
函數(shù)被調(diào)用時給出的參數(shù)包含了實實在在的數(shù)據(jù),會被函數(shù)內(nèi)部的代碼使用,所以稱為實際參數(shù),簡稱實參。
形參和實參的區(qū)別和聯(lián)系
-
1) 形參變量只有在函數(shù)被調(diào)用時才會分配內(nèi)存,調(diào)用結(jié)束后,立刻釋放內(nèi)存,所以形參變量只有在函數(shù)內(nèi)部有效,不能在函數(shù)外部使用。
-
2) 實參可以是常量、變量、表達式、函數(shù)等,無論實參是何種類型的數(shù)據(jù),在進行函數(shù)調(diào)用時,它們都必須有確定的值,以便把這些值傳送給形參,所以應該提前用賦值、輸入等辦法使實參獲得確定值。
-
3) 實參和形參在數(shù)量上、類型上、順序上必須嚴格一致,否則會發(fā)生“類型不匹配”的錯誤。當然,如果能夠進行自動類型轉(zhuǎn)換,或者進行了強制類型轉(zhuǎn)換,那么實參類型也可以不同于形參類型。
-
4) 函數(shù)調(diào)用中發(fā)生的數(shù)據(jù)傳遞是單向的,只能把實參的值傳遞給形參,而不能把形參的值反向地傳遞給實參;換句話說,一旦完成數(shù)據(jù)的傳遞,實參和形參就再也沒有瓜葛了,所以,在函數(shù)調(diào)用過程中,形參的值發(fā)生改變并不會影響實參。
-
5) 形參和實參雖然可以同名,但它們之間是相互獨立的,互不影響,因為實參在函數(shù)外部有效,而形參在函數(shù)內(nèi)部有效。
形參和實參的功能是傳遞數(shù)據(jù),發(fā)生函數(shù)調(diào)用時,實參的值會傳遞給形參。
二、參數(shù)傳遞
函數(shù)允許我們將數(shù)據(jù)傳遞進去,通過傳遞的數(shù)據(jù)從而影響函數(shù)執(zhí)行結(jié)果,使函數(shù)更靈活、復用性更強。
function foo(a, b) { console.log([a, b]); } foo(1, 2); // 輸出 [1, 2]
這個例子中,a
和 b
屬于函數(shù)中的局部變量,只能在函數(shù)中訪問。調(diào)用函數(shù)時,傳遞的數(shù)據(jù)會根據(jù)位置來匹配對應,分別賦值給 a
和 b
。
創(chuàng)建函數(shù)時,function 函數(shù)名
后面括號中設定的參數(shù)被稱為形參;調(diào)用函數(shù)時,函數(shù)名后面括號中傳入的參數(shù)被稱為實參。上面例子中,a
和 b
是形參,傳入的 1
和 2
是實參。
因為形參是已聲明的變量,所以不能再用 let
和 const
重復聲明。
function foo(a, b) { let a = 1; // 報錯,a 已聲明 const b = 1; // 報錯,b 已聲明 }
JavaScript 中所有函數(shù)傳遞都是按值傳遞的,不會按引用傳遞。所謂的值,就是指直接保存在變量上的值,如果把對象作為參數(shù)傳遞,那么這個值就是這個對象的引用,而不是對象本身。這里實際上是一個隱式的賦值過程,所以給函數(shù)傳遞參數(shù)時,相當于從一個變量賦值到另一個變量。
原始值:
function add(num) { return num + 1; } let count = 5; let result = add(count); // 此處參數(shù)傳遞的過程可以看作是 num = count console.log(count); // 5 console.log(result); // 6
引用值:
function setName(obj) { obj.name = "小明"; } let person = {}; setName(person); // 此處參數(shù)傳遞的過程可以看作是 obj = person; console.log(person); // {name: "小明"}
三、理解參數(shù)
JavaScript 中的函數(shù)既不會檢測參數(shù)的類型,也不會檢測傳入?yún)?shù)的個數(shù)。定義函數(shù)時設置兩個形參,不意味著調(diào)用時必須傳入兩個參數(shù)。實際調(diào)用時不管是傳了一個還是三個,甚至不傳參數(shù)也不會報錯。
所有函數(shù)(非箭頭)中都有一個名為 arguments
的特殊的類數(shù)組對象(不是 Array
的實例),它保存著所有實參的副本,我們可以通過它按照數(shù)組的索引訪問方式獲取所有實參的值,也可以訪問它的 arguments.length
屬性來確定函數(shù)實際調(diào)用時傳入的參數(shù)個數(shù)。
例如:
function foo(a, b) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments.length); } foo(10, 20); // 依次輸出 10、20、2
上面例子中,foo() 函數(shù)的第一個參數(shù)是 a,第二個參數(shù)是b ,可以通過 arguments[x] 的方式來分別獲取同樣的值 。因此,你甚至可以在聲明函數(shù)時不設置形參。
function foo() { console.log(arguments[0]); console.log(arguments[1]); } foo(10, 20); // 依次輸出 10、20
由此可見,JavaScript 函數(shù)的形參只是方便使用才寫出來的。想傳多少個參數(shù)都不會產(chǎn)生錯誤。
還有一個要注意的是,arguments
可以跟形參一起使用,并且 arguments
對象中的值會和對應的形參保持同步。例如:
function foo(a) { arguments[0] ++; console.log(a); } foo(10); // 輸出 11 //------------------------------------ function foo2(a) { a++; console.log(arguments[0]); } foo2(10); // 輸出 11
當修改 arguments[0] 或 a 的值時,另一個也被改變了。這并不意味著它們訪問同一個內(nèi)存地址,畢竟我們傳入的是一個原始值。它們在內(nèi)存中還是分開的,只是由于內(nèi)部的機制使它們的值保持了同步。
另外,如果缺少傳參,那這個形參的值就不會和 arguments
對象中的對應值進行同步。例如下面這個例子,只傳了一個參數(shù),那么arguments
中只有一個實參值,這時候在函數(shù)中把 arguments[1] 設置為某個值,這個值并不會同步給第二個形參,例如:
function foo(a,b) { arguments[1] = 2; console.log(b); } foo(1); // 輸出 undefined
這個例子中,形參 b 沒有傳入實參,它的值會默認為 undefined
。但如果:
foo(1, undefined); // 輸出 2
手動傳入 undefined
時, arguments
數(shù)組中會出現(xiàn)一個值為 undefined
的元素,依然能和 b 的值進行同步。
嚴格模式下,arguments
對象中的值和形參不會再同步,當然,如果傳入的是引用值,它們依然會互相影響,但這只是引用值的特性而已。因此,在開發(fā)中最好不要依賴這種同步機制,也就是說不要同時使用形參和它在arguments
對象中的對應值。
箭頭函數(shù)中沒有 arguments
如果函數(shù)是使用箭頭語法定義的,那么函數(shù)中是沒有 arguments 對象的,只能通過定義的形參來訪問。
let foo = () => { console.log(arguments[0]); }foo(); // 報錯,arguments 未定義
在某些情況可能會訪問到 arguments
:
function fn1(){ let fn2 = () => { console.log(arguments[0]); } fn2(); }fn1(5);
但這個 arguments
,并不是箭頭函數(shù)的,而是屬于外部普通函數(shù)的,當箭頭函數(shù)中訪問 arguments
時,順著作用域鏈找到了外部函數(shù)的arguments
。
四、將對象屬性用作實參
當一個函數(shù)包含的形參有多個時,調(diào)用函數(shù)就成了一種麻煩,因為你總是要保證傳入的參數(shù)放在正確的位置上,有沒有辦法解決傳參順序的限制呢?
由于對象屬性是無序的,通過屬性名來確定對應的值。因此可以通過傳入對象的方式,以對象中的屬性作為真正的實參,這樣參數(shù)的順序就無關(guān)緊要了。
function foo(obj) { console.log(obj.name, obj.sex, obj.age); } foo({ sex: '男', age: 18, name: '小明' }); // 小明 男 18
五、參數(shù)默認值
如果調(diào)用函數(shù)時缺少提供實參,那么形參默認值為 undefined
。
有時候我們想要設置特定的默認值,在 ES6 之前還不支持顯式地設置默認值的時候,只能采用變通的方式:
function sayHi(name) { name = name || 'everyone'; console.log( 'Hello ' + name + '!'); } sayHi(); // 輸出 'Hello everyone!'
通過檢查參數(shù)值的方式判斷有沒有賦值,上面的做法雖然簡便,但缺點在于如果傳入的實參對應布爾值為 false
,實參就不起作用了。需要更精確的話可以用 if
語句或者三元表達式,判斷參數(shù)是否等于 undefined
,如果是則說明這個參數(shù)缺失 :
// if 語句判斷 function sayHi(name) { if (name === undefined) { name = 'everyone'; } console.log( 'Hello ' + name + '!'); } // 三元表達式判斷 function sayHi(name) { name = (name !== undefined) ? name : 'everyone'; console.log( 'Hello ' + name + '!'); }
ES6 就方便了許多,因為它支持了顯式的設置默認值的方式,就像這樣:
function sayHi(name = 'everyone') { // 定義函數(shù)時,直接給形參賦值 console.log( 'Hello ' + name + '!'); } sayHi(); // 輸出 'Hello everyone!' sayHi('Tony'); // 輸出 'Hello Tony!' sayHi(undefined); // 輸出 'Hello everyone!'
這些結(jié)果表明了,它也是通過參數(shù)是否等于 undefined
來判定參數(shù)是否缺失的。
默認值不但可以是一個值,它還可以是任意合法的表達式,甚至是函數(shù)調(diào)用:
function sayHi(name = 'every'+'one') { console.log( 'Hello ' + name + '!'); } sayHi(); // 輸出 'Hello everyone!' //-------------------------------------- function foo() { console.log('調(diào)用foo'); return 'Tony'; } function sayHi(name = foo()) { console.log( 'Hello ' + name + '!'); } sayHi(); // 輸出 '調(diào)用foo' // 輸出 'Hello Tony!' sayHi(undefined); // 輸出 '調(diào)用foo' // 輸出 'Hello Tony!' sayHi('John'); // 輸出 'Hello John!'
可以看到,函數(shù)參數(shù)的默認值只有在函數(shù)調(diào)用時,參數(shù)的值缺失或者是 undefined
才會求值,不會在函數(shù)定義時求值。
參數(shù)默認值的位置
通常我們給參數(shù)設置默認值,是為了調(diào)用函數(shù)時可以適當省略參數(shù)的傳入,這里要注意的是,有多個參數(shù)時,設置了默認值的參數(shù)如果不是放在尾部,實際上它是無法省略的。
function fn(x = 1, y) { console.log([x, y]); } fn(); // 輸出 [1, undefined] fn(2); // 輸出 [2, undefined] fn(, 2); // 報錯,語法錯誤(這里不支持像數(shù)組那樣的空槽) fn(undefined, 2); // 輸出 [1, 2] (那還不如傳個 1 方便呢!)
上面例子中,給形參 x 設置的默認值就顯得沒有任何意義了。因此,設置默認值的參數(shù)放在尾部是最好的做法:
function fn(x, y = 2) { console.log([x, y]); } fn(); // 輸出 [undefined, 2] fn(1); // 輸出 [1, 2] fn(1, 1) // 輸出 [1, 1]
參數(shù)的省略問題
在多個參數(shù)設置了默認值的情況下,那么問題又來了,你并不能省略比較靠前的參數(shù),而只給最后的一個參數(shù)傳入實參。
function fn(x, y = 2, z = 3) { console.log([x, y, z]); } fn(1, , 10) // 報錯
前面我們知道,可以通過傳入對象的這種方式去避免參數(shù)順序的限制。那參數(shù)默認值如何實現(xiàn)呢?用 ||
、 if
語句或者三元表達式去判斷也是解決辦法,但這樣就顯得有些落后了。接下來要討論的是另外兩種 ES6 中的全新方式。
參數(shù)默認值和 Object.assign() 結(jié)合使用
function fn(obj = {}) { let defaultObj = { x: undefined, y: 2, z: 3 } let result = Object.assign(defaultObj, obj); console.log([result.x, result.y, result.z]); } fn(); // 輸出 [undefined, 2, 3] fn({ x: 1, z: 10 }); // 輸出 [1, 2, 10]
上面的例子中,在函數(shù)中定義了一個對象 defaultObj
,變通地利用其中的屬性作為參數(shù)的默認值,然后利用 Object.assagin() 把傳入的對象和默認對象進行合并,defaultObj 中的屬性會被 obj 的相同屬性覆蓋,obj 中如果有其他屬性會分配給 defaultObj 。這里用一個變量接收返回的合并對象。
同時形參 obj
也設置了默認值為一個空對象,防止函數(shù)調(diào)用時不傳任何參數(shù),因為這會導致 Object.assign() 接收的第二個參數(shù)是 undefined
,從而產(chǎn)生報錯。
參數(shù)默認值和解構(gòu)賦值結(jié)合使用
函數(shù)調(diào)用時,實參和形參的匹配實際上是一個隱式的賦值過程,所以,參數(shù)傳遞也可以進行解構(gòu)賦值:
function fn({ x, y = 2, z = 3 }) { console.log([x, y, z]); } fn({}); // 輸出 [undefined, 2, 3] fn({ x: 1, z: 10 }); // 輸出 [1, 2, 10]
在這個例子中,使用的只是對象的解構(gòu)賦值默認值,還沒有使用函數(shù)參數(shù)的默認值。如果函數(shù)調(diào)用時不傳任何參數(shù),也會產(chǎn)生報錯,因為這導致了參數(shù)初始化時解構(gòu)賦值失敗,相當于執(zhí)行了 {x, y = 2, z = 3} = undefined
這樣的代碼。
同樣的,你可以利用參數(shù)默認值的語法,給 {x, y = 2, z = 3}
設置一個默認的解構(gòu)對象,使得不傳參函數(shù)也能夠順利執(zhí)行:
function fn({ x, y = 2, z = 3 } = {}) { console.log([x, y, z]); } fn(); // 輸出 [undefined, 2, 3]
這里出現(xiàn)了雙重的默認值,可能有些繞,那么用一段偽代碼來解釋以上的參數(shù)初始化過程就是:
if( 實參 === {...} ) { // 當 fn({...}); { x, y = 2, z = 3 } = {...}; } else if ( 實參 === undefined ){ // 當 fn(); { x, y = 2, z = 3 } = {}; }
雙重默認值有一點細節(jié)需要特別注意,就是解構(gòu)賦值默認值和函數(shù)參數(shù)默認值的差別,看下面例子:
function fn ({ x = 1 } = {}, { y } = { y: 2 }){ console.log(x, y); } fn(); // 輸出 1 2 fn({ x: 10 }, { y: 20 }); // 輸出 10 20 fn({},{}); // 1 undefined
這個函數(shù)中,有兩組參數(shù)采用了解構(gòu)賦值的方式,看似 x 和 y 都設置了默認值,雖然是不同的兩種形式,但顯然不是任何情況下結(jié)果都相同的。當傳入的參數(shù)是{}
時,y 并沒有獲取到默認值 2 ,為什么會這樣呢?結(jié)合前面的偽代碼例子來看:
fn({ x: 10 }, { y: 20 }); // 初始化時: { x = 1 } = { x: 10 }, { y } = { y: 20 } fn({},{}); // 初始化時: { x = 1 } = {}, { y } = {}
當傳入的參數(shù)是{}
時,函數(shù)參數(shù)沒有缺失也不是 undefined
,所以函數(shù)參數(shù)默認值是不起作用的。同時 {}
里面也沒有 x 和 y 的對應值,x 得到的 1 是解構(gòu)賦值默認值,而 y 由于沒有設置解構(gòu)賦值默認值,所以它默認是 undefined
。
參數(shù)默認值的作用域與暫時性死區(qū)
還有一個小細節(jié),一旦有參數(shù)設置了默認值,那么它們會形成自己的作用域(包裹在(...)
中),因此不能引用函數(shù)體中的變量:
function foo(a = b) { let b = 1; } foo(); // 報錯,b 未定義
但這個作用域只是臨時的,參數(shù)初始化完畢后,這個作用域就不存在了。
它也符合普通作用域的規(guī)則:
let b = 2; function foo(a = b) { let b = 1; return a; } foo(); // 2
上面例子中,存在一個全局變量 b,那么形參 a 會獲取到全局變量 b 的值。
當然,如果形參作用域中存在一個形參 b 的話,它優(yōu)先獲取到的是當前作用域的:
let b = 2; function foo(b = 3 ,a = b) { return a; } foo(); // 3
給多個參數(shù)設置默認值,它們會按順序初始化的,遵循“暫時性死區(qū)”的規(guī)則,即前面的參數(shù)不能引用后面的參數(shù):
function foo(a = b, b = 2) { return a + b; } foo(); // 報錯,b 在初始化之前不能訪問
六、參數(shù)的收集與展開
剩余參數(shù)
ES6 提供了**剩余參數(shù)(rest)**的語法(...變量名
),它可以收集函數(shù)多余的實參(即沒有對應形參的實參),這樣就不再需要使用 arguments
對象來獲取了。形參使用了 ...
操作符會變成一個數(shù)組,多余的實參都會被放進這個數(shù)組中。
剩余參數(shù)基本用法:
function sum(a, ...values) { for (let val of values) { a += val; } return a; } sum(0, 1, 2, 3); // 6
上面例子中,在參數(shù)初始化時,首先根據(jù)參數(shù)位置進行匹配,把 0 賦值給 a ,然后剩余的參數(shù) 1、2、3 都會被放進數(shù)組 values 中。
下面是分別用 arguments
對象和剩余參數(shù)來獲取參數(shù)的對比例子:
// arguments 的寫法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // 剩余參數(shù)的寫法 const sortNumbers = (...numbers) => { return numbers.sort(); }
可以看出剩余參數(shù)的寫法更加簡潔。盡管 arguments
是一個類數(shù)組,也是可迭代對象,但它終究不是數(shù)組。它不支持數(shù)組方法,當我們使用 arguments
時,如果想要調(diào)用數(shù)組方法,就必須使用Array.prototype.slice.call
先將其轉(zhuǎn)為數(shù)組。
而剩余參數(shù)它不同于 arguments
對象,它是真正的 Array
實例,能夠很方便地使用數(shù)組方法。并且箭頭函數(shù)也支持剩余參數(shù)。
另外,使用剩余參數(shù)不會影響 arguments
對象的功能,它仍然能夠反映調(diào)用函數(shù)時傳入的參數(shù)。
-
剩余參數(shù)的位置
剩余參數(shù)必須是最后一個形參,否則會報錯。
// 報錯 function fn1(a, ...rest, b) { console.log([a, b, rest]); } // 正確寫法 function fn2(a, b, ...rest) { console.log([a, b, rest]); } fn2(1, 2, 3, 4) // 輸出 [1, 2, [3, 4]]
展開語法
前面我們知道了如何把多余的參數(shù)收集為一個數(shù)組,但有時候我們需要做一些相反的事,例如要把一個數(shù)組中的元素分別傳入給某個函數(shù),而不是傳入一個數(shù)組,像這樣:
function sum(...values) { let sum = 0; for (let val of values) { sum += val; } return sum; } let arr = [1, 2, 3, 4]; sum(arr); // "01,2,3,4"
上面例子的函數(shù)會把所有傳進來的數(shù)值累加,如果直接傳入一個數(shù)組,就得不到我們想要的結(jié)果。
例子中傳入一個數(shù)組, values 的值會變成 [[1, 2, 3, 4]]
,導致數(shù)組 values 中只有一個元素,而這個元素的類型是數(shù)組。那么函數(shù)返回值就是數(shù)值 0
和數(shù)組 [1, 2, 3, 4]
相加的結(jié)果了,兩者各自進行了類型的隱式轉(zhuǎn)換變成字符串,然后再相加,是一個字符串拼接的效果。
要實現(xiàn)把數(shù)組拆解傳入給函數(shù),首先不可能一個個傳入?yún)?shù)——sum(arr[0], arr[1], arr[2], arr[3]);
,因為不是任何時候都知道數(shù)組中有多少個元素的,而且數(shù)組中可能會非常多的元素,手動傳是不明智的。
比較可行的是借助 apply() 方法:
sum.apply(null, arr); // 10
但這還不是最優(yōu)解,那么重點來了!
ES6 新增的**展開語法(spread)**可以幫助我們面對這種情況。它也是使用 ...變量名
的語法,雖然跟剩余參數(shù)語法一樣,但是用途完全相反,它能夠把一個可迭代對象拆分成逗號分隔的參數(shù)序列。
在函數(shù)調(diào)用時,它的應用是這樣子的:
sum(...arr); // 10 // 相當于 sum(1,2,3,4);
它甚至可以隨意搭配常規(guī)值使用,沒有前后位置限制,還可以同時傳入多個可迭代對象:
sum(-1, ...arr); // 9 sum(...arr, 5); // 15 sum(-1, ...arr, 5); // 14 sum(-1, ...arr, ...[5, 6, 7]); // 27
展開操作符 ...
相當于替我們完成了手動分別傳參的操作,函數(shù)只知道接收的實參是單獨的一個個值,不會因為展開操作符的存在而產(chǎn)生其他影響。
上面的示例雖然都是針對于數(shù)組的,但展開語法能做的還不止這些,其他可迭代對象例如字符串、字面量對象都可以展開,深入了解請參見 → 展開語法
總結(jié)
-
形參是函數(shù)中已聲明的局部變量,傳遞給函數(shù)的實參會被賦值給形參,函數(shù)參數(shù)傳遞實際上是一個隱式的賦值過程。
-
形參和實參的數(shù)量可以不相等:
● 缺失實參的形參會得到默認值
undefined
。● 額外的實參,可以通過
arguments
對象訪問,箭頭函數(shù)除外。 -
可以通過傳入對象的方式讓傳參順序不再重要,讓對象中的屬性作為真正的實參。
-
ES6 的參數(shù)默認值——函數(shù)調(diào)用時參數(shù)的值缺失或者是
undefined
,才會獲取默認值。● 設置默認值的形參只有放在最后一位才可以省略傳參。
● 形參設置默認值不能引用函數(shù)體中的變量,但可以引用前面的形參和外部變量。
● 通過 Object.assign() 或者解構(gòu)賦值實現(xiàn)默認值,能讓傳參的方式更加靈活。
-
剩余參數(shù)和
arguments
的主要區(qū)別:● 剩余參數(shù)只包含那些沒有對應形參的實參,而
arguments
對象包含了傳給函數(shù)的所有實參。● 剩余參數(shù)是真正的
Array
實例,而arguments
只是類數(shù)組對象。 -
剩余參數(shù)和展開語法都采用
...
操作符,在函數(shù)的相關(guān)場景中:● 出現(xiàn)在函數(shù)形參列表的最后,它是剩余參數(shù)。
● 出現(xiàn)在函數(shù)調(diào)用時,它是展開語法。
【