這些有關函式引數的知識點你都瞭解麼?
相信大家對函式的形參和實參都應該比較熟悉了.今天我們主要是來回顧一下其中的知識點,溫故而知新,可以為師矣.最近的文章基本都是我在整理自己以前的筆記時,看到一些知識點的回顧總結.
形參和實參是什麼
形參是形式引數(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物件中的值保持一致.當形參沒有對應實參的時候,形參和arguments物件中的值並不對應
function fn(a){
console.log(a === arguments[0]) // true
a = 1
console.log(a === arguments[0]) // false
}
fn()
複製程式碼
當然了,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,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個的時候),我們可以將引數變成一個物件,然後通過物件的解構賦值來獲取每個引數.這樣我們就不用去操心每個引數的先後順序了.