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