前言
函式是所有程式語言中重要的組成部分,在Es6出現之前 JavaScript的函式語法一直沒有太大的變化,從而遺留了很多問題和隱晦的做法,導致實現一些功能需要編寫很多程式碼。
函式形參預設值JavaScript函式有一個特別的地方,就是無論在函式形參裡定義了多少引數,都可以傳入任意數量的引數,但是有的情況下,我們的引數只是可填,這樣的話我們還在函式體呢寫一堆邏輯從而導致程式碼冗餘,還好Es6版本出現了函式預設值。
我們用Es5和Es6程式碼來比對一下
Es5處理預設引數function person(name, age) { name = typeof(name) != "undefined" ? name : `蛙人${+ new Date()}` age = typeof(age) != "undefined" ? age : 24 }person()
上面example中是Es5這樣處理預設引數值的,假如我們引數多的話,這麼寫程式碼的話會造成非常冗餘的,於是Es6就出現函式引數預設值。<br>
Es6處理預設引數function person(name = "王二", age = 24) { console.log(name, age)}person() // 王二 24person("張三", 30) // 張三 30person(null, null) // null null
上面example是Es6中處理的預設引數,可以看到程式碼非常簡化,上面程式碼可以看到引數傳入了null,對於預設引數null也是一個合法值,這種情況下只有函式引數為undefined時才會使用預設值。
函式引數表示式關於預設引數值,最有趣的特性可能就是非原始值傳參了,也可以把預設引數定義為函式or 變數。
function defaultName() { return "王二"}function person(name = defaultName()) { console.log(name)}person("張三") // 張三person() // 王二
需要注意的是,預設引數的表示式不是一建立函式就立刻執行的,而是當該函式person被呼叫的時候並且沒有傳入引數才會執行。
上面example中,如果不傳參才會呼叫預設值的defaultName函式。
下面來看一下預設引數傳入變數。
let defaultName = "王二"function person(name = defaultName) { console.log(name)}person("張三") // 張三person() // 王二function person(name, nickName = name) { console.log(name, nickName)}person("張三") // 張三 張三person("王二", "李四") // 王二 李四
上面example中,第一個程式碼塊的裡面我們都能看的懂,只不過把之前的函式換成了變數。看第二個程式碼塊裡的程式碼,我們把nickName引數預設值設定成了第一個引數name引數,這是在引用引數預設值的時候,只允許引用前面引數的值,相當於函式引數就是定義的變數,我們後面的變數可以訪問前面變數的,但是隻限制在於當前作用域中,這個函式形參裡就是當前作用域。我們再看一個例子。
function person(name = nickName, nickName) { console.log(name, nickName)}person("張三") // 張三 張三
上面example中,第一個引數預設值是第二個引數,這時執行會丟擲一個錯誤,因為這時在定義第二個變數前去訪問,會造成暫時死區,如果不明白暫時死區的可以去看我的上一篇文章。《一看就懂的var、let、const三者區別》
函式引數預設值對arguments的影響當使用函式預設引數時,arguments物件的行為會與以往不同
Es5非嚴格模式下使用argumentsEs5非嚴格模式下,函式命名引數的變化會體現在arguments物件上,arguments獲取的是當前函式的實參,arguments在非嚴格模式下它跟形參是對映關係,就是形參有變化arguments跟著變。
function test(a, b) { console.log(a == arguments[0]) // true console.log(b == arguments[1]) // true a = "a" b = "b" console.log(arguments) // ["a", "b"]}test(1, 2)
上面example中,在非嚴格模式下,命名引數的變化會同步更新到arguments物件中。當a引數的變化,會對映到arguments[0]物件上。
Es5嚴格模式下使用arguments下面我們再來看一下嚴格模式下的arguments
function test(a, b) { 'use strict'; console.log(arguments) // [1, 2] b = 10 console.log(arguments) // [1, 2]}test(1, 2)
上面example是嚴格模式下的,可以看出當我們改變引數b時,再次列印arguments物件,它還是初始化值。在嚴格模式下JavaScript中取消了arguments物件這個令人困惑的行為,無論引數如何變化,arguments物件不再隨之改變。
Es6中使用預設引數值對arguments的影響在Es6中,如果一個函式使用了預設引數值,那麼arguments物件的行為都將與JavaScript中的嚴格模式下保持一致。
function test(a, b = 2) { a = 12 b = 10 console.log(arguments) // [1]}test(1)
上面example中,arguments物件打印出[1]是因為arguments物件獲取的是實參,我們可以看到實參引數就傳了一個值,所以arguments物件就只有一個值。再看第二點,a和b的引數都改變了值,但是arguments物件還是沒有改變,這就是上面說的,如果一個函式使用了預設引數值,那麼arguments物件的行為都將與JavaScript中的嚴格模式下保持一致。
處理無命名引數在js中函式引數數量是任意的,當傳入更少的數量,預設引數的特性可以有效的簡化函式宣告的程式碼。當傳入更多的數量,Es6也同樣提供了更好的方案。
Es5中獲取無命名引數function test(a, b, c) { console.log(arguments) // [1, 2, 3]}test(1, 2, 3)
上面example中,arguments物件雖然也可以實現獲取所有的引數,但是呢如果我們想獲取第二個引數之後的所有引數,那麼還得迴圈去排除。
Es6中獲取無命名引數function test(...parmas) { console.log(params) // [1, 2, 3, 4]}test(1, 2, 3, 4)function test(a, b, ...params) { console.log(params)}test(1, 2, 3, 4)
上面example中,第一個程式碼塊裡實現了在Es6中獲取全部的引數,可是還不滿足我們的需求。那麼看第二個程式碼塊裡的程式碼就實現了,我們獲取第二個引數後面所有的引數。
Es6獲取無命名引數弊端首先,每一個函式只能宣告一個獲取不定引數,而且只能放在函式的末尾,否則會報錯。
function test(...params, a, b) { }test()
上面example中,會丟擲錯誤,聲明瞭不定引數數之後,就不能繼續在後面宣告引數。<br> 還有一點,不定引數不能定義在物件字面量的setter中,因為setter函式只接收一個函式,寫成不定引數之後就會是一個數組,這樣就會導致程式異常。
let obj = { set name(...params) { }}
函式name屬性在JavaScript中所有的函式都有一個name屬性,該屬性儲存的是該函式名稱的字串。沒有名稱的函式也仍然有name屬性,該name屬性值為空字串。
function person() {}let test = function() {}console.log(person.name) // personconsole.log(test.name) // test
上面example中,person函式name屬性值為"person",對應著宣告時的函式名稱。匿名函式表示式test函式的name名稱,對應著被賦值為匿名函式的變數。
name屬性的特殊情況我原來以為每個函式的name名稱都是對應著當前的函式名,後來發現並不是這麼回事。下面來看一下函式的特殊情況
var person = { get getName() { return "蛙人" }}console.log(Object.getOwnPropertyDescriptor(person, 'getName').get.name) // get getNamefunction test() {}console.log(test.bind().name) // bound test
上面example中,person.getName是一個取值函式getter,所以它的函式名稱get getName,如果是setter函式的話那麼名稱會有帶有字首set。透過bind建立的函式,它的名稱帶有"bound"字首。
箭頭函式Es6中箭頭函式是其中最有趣的特性,箭頭函式是一種使用箭頭=>定義函式的新語法,但是它與傳統的JavaScript函式有些不同,具體看下面幾點。
沒有this、super、arguments不能透過new關鍵字呼叫沒有原型prototype不可以改變this指向不支援重複的命名引數箭頭函式和傳統函式一樣都有一個name屬性,這一點是不變的。
箭頭函式語法let person = () => "王二"// 相當於下程式碼function person() { return "王二"}
上面example中,當箭頭函式右側的表示式求值後會立即返回。
箭頭函式引數let getName = val => val// 相當於下程式碼function getName(val) { return val}
當箭頭函式只有一個引數時,就可以省略括號,直接寫引數名。如果要傳入兩個或多個引數,則就需要帶上括號。看下面例子
let sum = (a, b) => a + b// 相當於下程式碼function sun(a, b) { return a + b}
如果你想返回一個物件字面量,可以這樣寫
let getObj = () => ({name: "王二", age: 24}) // {name: "王二", age: 24}// 相當於下程式碼function getObj() { return { name: "王二", age: 24 }}
箭頭函式沒有this箭頭函式的this值,取決於函式外部非箭頭函式的this值,如果上一層還是箭頭函式,那就繼續往上找,如果找不到那麼this就是window物件
let person = { test: () => { console.log(this) }, fn() { return () => { console.log(this) } }}person.test() // windowperson.fn()() // person物件
上面example中,可以清楚的看到箭頭沒有this,那麼它的this只會去找外層的非箭頭函式的函式。
箭頭函式沒有arguments物件同樣箭頭函式也沒有arguments物件,但是如果它外層還有一層非箭頭函式的話,就會去找外層的函式的arguments物件, 如下
let test1 = () => console.log(arguments) // 執行該函式會丟擲錯誤function test2(a, b, c) { return () => { console.log(arguments) // [1, 2, 3] }}test2(1, 2, 3)()
上面example中,可以清楚的看到當前的箭頭函式沒有arguments物件,然而就去它的外層去找非箭頭函式的函式。注意:箭頭函式找arguments物件只會找外層非箭頭函式的函式,如果外層是一個非箭頭函式的函式如果它也沒有arguments物件也會中斷返回,就不會在往外層去找了。看下面例子
function test(a) { return function() { return () => { console.log(arguments) // [] } }}test(1)()()
上面example中可以看到,裡面的箭頭函式往外層找非箭頭函式的函式,然後不管外層這個函式有沒有arguments物件都會返回。只有它是非箭頭函式就可以,如果外層是箭頭函式還會繼續往外層找。
箭頭函式不能用new關鍵字宣告let test = () => {}new test() // 丟擲錯誤,找不到constructor物件
箭頭函式沒有原型prototype
切記,箭頭函式沒有原型,有可能面試官會問,JavaScript中所有的函式都有prototype屬性嗎
let test = () => {}test.prototype // undefined
箭頭函式不能改變this指向
let person = {}let test = () => console.log(this)test.bind(person)()test.call(person)test.apply(person)
上面example中,改變this指向的方法都不會丟擲錯誤,但是都無效,都不能改變this指向。