相信大家對函數的形參和實參都應該比較熟悉了.今天我們主要是來回顧一下其中的知識點,溫故而知新,可以爲師矣.最近的文章基本都是我在整理自己以前的筆記時,看到一些知識點的回顧總結.
形參和實參是什麼
形參是形式參數(parameter),是指在函數定義時,預先定義用來在函數內部使用的參數
實參是實際參數(arguments),是指在函數調用時,傳入函數中用來運算的實際值
function fn(a){
console.log(a)
}
fn('str')
上面代碼中,fn
後面的這個定義在()
中的a就是形參,類似於一個佔位符,佔了第一個坑,代表了第一個傳進來的參數.而下面函數調用時的'str'
則是我們傳進去的實際參數,是一個真正有意義的值.
形參和實參數量不相等的情況
在js中,形參和實參的數量往往可以是不相等的
function add(a,b){
console.log(a+b)
}
add(1,2) // 3
add(1) // NaN
add(1,2,3) // 3
第一次調用的時候,形參和實參的數量相等,完美計算出結果3
第二次調用的時候,實參比形參少了一個,結果爲NaN,這是因爲此時的形參b
因爲沒有值而變成了undefined
,在做加法運算的時候,undefined
轉爲數值類型後爲NaN
,而NaN
再加1,結果還是NaN
.
第三次調用的時候,實參比形參多了一個,結果爲3.這是因爲當實參比形參多的時候,多餘的實參會被忽略掉.類似於我這原來只有兩個坑位,你們三個人一起來,那最後那個人只能沒有坑位了.
arguments對象
說到arguments
對象,大家應該都比較熟悉了.因爲平時的日常開發中也會經常使用到這個對象.arguments
的樣子有點像一個數組,但又不是真正的數組.所以我們叫它是一個類數組對象.除了具有length
屬性和索引信息外,arguments
對象不具有其他數組的特性.像數組的push
,splice
等,它通通都沒有.
function fn(){
let len = arguments.length
for(let i = 0; i < len; i++){
console.log(arguments[i])
}
}
fn(1,2) // 1 2
fn(2,3,4) // 2 3 4
arguments
代表了傳進函數的實參集合.我們可以通過遍歷它來獲取所有的實參.
其中,這個對象中的第一個元素和函數的第1個形參是對應的,以此類推.
function fn(a,b){
console.log(a === arguments[0])
console.log(b === arguments[1])
}
fn(1,2) // true true
當我們改了形參的值,arguments
對應的值也會發生改變.同樣,改變了arguments
的值,函數內的形參表示的值也發生了改變.
function fn(a){
a = 2
console.log(arguments[0]) // 2
}
fn(1)
function fn(a){
arguments[0] = 2
console.log(a) // 2
}
fn(1)
當然了,arguments
對象既然作爲一個類數組對象,也是可以轉換爲數組形式的.
function fn(){
console.log(Array.from(arguments))
}
fn(1,2) // [1,2]
如何讓函數的形參和實參保持相等
其實在JS中函數參數的接受還是比較鬆散的.可以多傳遞一個值,也可以少傳遞一個值.這樣難免有時候會引起不必要的BUG,那麼我們該如何解決呢?
首先我們要介紹兩個長度length
屬性.其中一個是函數的length
,另外一個是arguments
的length
.它們分別表示函數形參的個數和實參的個數.
function fn(a){
console.log(arguments.length)
}
fn(1,2) // 2
console.log(fn.length) // 1
既然我們能知道函數的形參個數和實參個數,那問題就好解決了
function fn(a){
if(fn.length !== arguments.length){
throw new Error('參數個數不對')
}
console.log('參數個數對的')
}
try{
fn(1,2)
}catch(e){
console.log(e)
}
fn(1) // 參數個數對的
只要我們在函數的開頭判斷一下形參的個數和實參的個數是不是相等就ok了.
函數重載?
我們知道,JS中是沒有傳統意義上的函數重載的,後面定義的同名函數會覆蓋前面的同名函數.
function fn(a){
console.log(1)
}
function fn(a,b){
console.log(2)
}
fn(1) // 2
fn(1,2) // 2
但是我們還是有辦法可以通過arguments
對象來模擬實現一個函數重載的功能
function fn(a){
if(arguments.length === 1){
console.log(1)
}else{
console.log(2)
}
}
fn(1) // 1
fn(1,2) // 2
改變形參會不會對實參產生影響?
function fn(a){
arguments[0] = 2
console.log(a)
}
let a = 1
fn(1) // 2
console.log(a) // 1
上面的示例中,我們傳入的是基本類型值,發現無論我們怎麼修改形參,都不會影響到實參.
下面我們要傳入一個引用類型的值,看它是否會受影響
function fn(obj){
obj.name = 'lisi'
obj = {
name:'zhangsan',
age:12
}
console.log(obj)
}
let obj = {}
fn(obj) // {name: "zhangsan", age: 12}
console.log(obj) // {name: "lisi"}
可以看出來,在函數裏面操作形參影響了實參.
那麼到底形參會不會影響到實參呢,這個答案我們可以從紅寶書的傳遞參數一節找到答案.
這是因爲,JavaScript所有函數參數都是按值傳遞.這就是答案,可是我擦,這話該怎麼理解,也太抽象了吧!當我的參數是對象的時候,我怎麼看起來更像是引用傳遞啊.想當初,看見這句話的時候,我是百思不得其解啊,它也沒個形象點的解釋,後面翻閱了一些資料,總算是有點理解了.具體的內容就不展開了,只要記住這裏的按值傳遞,基本類型的值指的是本身,而引用類型的值指的是內存中的地址.我們來看下面的幾個例子,證明一下剛剛的結論.
let a = 1
function fn(a){
a = 2
}
fn(a)
console.log(a) // 1
let obj = {}
function fn2(obj){
obj.name = 'zhangsan'
}
fn2(obj)
console.log(obj) // {name: "zhangsan"}
let obj2 = {}
function fn3(obj){
obj = {
name:'lisi'
}
}
fn3(obj2)
console.log(obj2) // {}
這裏的fn3
函數,我們可以理解爲實際上是這樣的一段代碼
function fn3(obj){
var obj = obj2 // 多了這裏一步
obj = {
name:'lisi'
}
}
上面多出來的一步,其實就是引用賦值的過程,拷貝的是對象的內存地址.對象的實際值保存在堆中,而棧中保存的是堆的內存地址.所以這裏是將obj2
的內存地址(類似 0x001234abcd)這一字符串賦值給了obj
,但是obj
又重新賦值了一個對象,所以在內存中開闢了一塊新的內存地址.所以在執行完函數以後,再次打印obj2
的值還是{}
.結合上面的第二段代碼來看,因爲函數內的obj
和函數外的obj
指向的是同一內存地址.因此在函數內部給obj
添加了name
屬性,會反映到函數外部的obj
對象中.好了,我們再來唸一遍這個結論:JavaScript所有函數參數都是按值傳遞,基本類型的值指的是本身,而引用類型的值指的是內存中的地址
函數默認參數
有時候,我們可以給函數設置一個默認參數.這樣當外部沒有實參傳遞進來的時候,我們就可以使用默認參數來進行運算.
function fn(a,b){
b = b || 'zhangsan'
console.log(a + ' ' +b)
}
fn('hello') // hello zhangsan
fn('hello', false) // hello zhangsan
不對啊,這裏我明明在第二次調用的時候,傳入了兩個參數,爲啥還是取用了默認值呢?這是因爲 ||
運算符當前面的值爲假值的時候,會取後面的值作爲結果計算.而當我們的實參傳入類似undefined
,null
,NaN
等假值時,上面設置默認參數的弊端就顯示出來了.
因此我們最好用ES6中設置默認參數的方式來設置
function fn(a,b = 'zhangsan'){
console.log(a + ' ' +b)
}
fn('hello') // hello zhangsan
fn('hello', false) // hello false
函數剩餘參數(rest參數)
rest參數也是ES6新增加的,語法形式爲...變量名
,用於獲取函數多餘的參數.注意剩餘參數後面不能再跟其他的參數了,否則會報錯.
function fn(a,...rest){
console.log(a)
console.log(rest)
}
fn(1) // 1 []
fn(1,2) // 1 [2]
fn(1,2,3) // 1 [2,3]
console.log(fn.length) // 1
並且我們可以看到,方法fn
的length
並不包括rest參數
.
rest參數
是一個真正的數組,可以使用數組原型上的所有方法,這點和arguments
是不同的.此外arguments
上還有callee
.這個就是我們接下來要講的內容了.
arguments.callee
arguments.callee
屬性包含當前正在執行的函數.
下面是一個典型的算階乘的函數
function fn(n){
if(n < 2){
return 1
}else{
return n * arguments.callee(n - 1)
}
}
console.log(fn(1)) // 1
console.log(fn(4)) // 24
上面的arguments.callee
在函數名稱是未知的時候,是很有用的.但是ES5中規定了,在嚴格模式下是不準使用arguments.callee
的,可以在這裏找到原因.當這個函數必須調用自身的時候,可以使用函數聲明或者命名一個函數表達式.
function fn(n){
'use strict'
if(n < 2){
return 1
}else{
return n * fn(n - 1)
}
}
最後提一嘴
function fn(obj){
const {name, age, gender ,hobby} = obj
console.log(name)
console.log(age)
console.log(gender)
console.log(hobby)
}
let p = {
name:'zhangsan',
age:12,
gender:'male',
hobby:'王者榮耀'
}
fn(p)
當我們的函數中需要傳入的參數較多的時候(大於3個的時候),我們可以將參數變成一個對象,然後通過對象的解構賦值來獲取每個參數.這樣我們就不用去操心每個參數的先後順序了.