函式
參數預設值,以及一些關於arguments物件,如何使用表達式作為參數、參數的暫時性死區。
以前設定預設值總是利用在含有邏輯或運算子(logical OR operator)的表達式中,前一個值是false時,總是返回後面一個的值,但如果我們給參數傳入0時,就有些麻煩。需要去驗證一下型別。
function makeRequest(url,timeout,callback){
timeout = timeout || 2000;
callback = callback || function(){};
// 邏輯
}
// 解決傳0問題
function makeRequest(url,timeout,callback){
timeout = typeof timeout !=='undefined'?timeout: 2000;
callback = typeof callback !=='undefined'?callback : function(){};
// 邏輯
}
預設參數
不傳入或傳入undefined。如果傳入null不會使用預設值,會認為是一個合法的值。預設參數對arguments物件的影響。之前的參數傳遞會體現在arguments物件上。 在ES5非嚴格模式下,命名參數的變化會同步更新到arguments物件上,但在ES5的嚴格模式下,取消了這個行為,無論參數如何變化,arguments物件不再隨之改變。如下:
function mixArgs(first,second){
"use strict"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = 'c';
second = 'd'
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs('a','b')
// true
// true
// false
// false
ES6中,如果一個函式使用了預設參數值,則無論是否明確定義了嚴格模式,arguments物件的行為都將與ES5嚴格模式下保持一致。
預設參數表達式
初次解析函式宣告時不會呼叫getValue()方法,只有當呼叫add()函式且不傳入第二個參數時才會呼叫。
let value = 5;
function getValue(){
return value++;
}
function add(first,second=getValue()){
return first+second;
})
console.log(add(1,1)) // 2
console.log(add(1)) // 6
console.log(add(1)) //7
先定義的參數可以作為後定義參數的預設值,反過來不成立(first=second,second),因為second比first晚定義,因此其不能作為first的預設值。為了幫助你理解背後的原理,我們重溫一下暫時性死區(Temporal Dead Zone, TDZ)的概念。與let宣告類似,定義參數時會為每個參數建立一個新的識別符號繫結,該繫結在初始化之前不可被引用,如果試圖存取會導致程式拋出錯誤。
函式參數有自己的作用域和暫時性死區,其與函式體的作用域是各自獨立的,也就是說參數的預設值不可存取函式體內的變數。
無論是否使用不定參數,arguments物件總是包含所有傳入函式的參數。
處理無命名參數--不定參數
// 函式的length屬性統計的是函式命名參數的數量,不定參數的加入不會影響length屬性的值。
function pick(object, ...keys){
let result = Object.create(null);
for(let i=0,len=keys.length;i<len;i++){
result[keys[i]] = object[keys[i]]
}
return result;
}
- 不定參數(當然在函式參數中使用)
- 每個函式最多只能宣告一個不定參數
- 而且一定要放在所有參數的末尾
- 不允許在setter中使用不定參數,是因為物件字面值(object literal)setter的參數有且只能有一個(在不定參數的定義中,參數的數量可以無限多,所以在當前上下文中允許使用不定參數)。
let object = {
// 語法錯誤 不可以在Setter中使用不定參數
set name(...value){
// ...
}
}
無論是否使用不定參數,arguments物件總是包含所有傳入函式的參數。
增強的Function建構函式
可以在動態建立函式時使用預設值及不定參數,唯一需要做的是在參數名後添加一個等號和一個預設值,定義不定參數,只需在最後一個參數前添加...。如下:
var add = new Function('first','second=first','return first+second')
console.log(add(1,1)) // 2
console.log(add(1)) // 2
var pickFirst = new Function("...args",'return args[0]')
console.log(pickFirst(1,2)) // 1
展開運算子
展開運算子(Spread Operator)可以讓你指定一個陣列,將它們打散後作為各自獨立的參數傳入函式。JavaScript內建的Math.max()方法可以接受任意數量的參數並返回值最大的那個,這是一個簡單的用例。
自己理解,注意,前面那個是定義時用的,這個是呼叫時使用(參數是分開的,而你有一個陣列)。
如下,之前陣列求最大值的做法:
let values = [25,50,75,100]
console.log(Math.max.apply(Math,values)) // 100;
// max方法不允許傳入一個陣列
console.log(Math.max(...values))
// 展開運算子後面可以繼續添加參數
console.log(Math.max(...values,110)) //110
函式的name屬性
宣告函式的.name屬性是它宣告的函式名,函式表達式如果函式是匿名的則就是其變數的名稱,如果不是則是其宣告的名字,
它只是協助偵錯用的額外資訊,所以不能使用name屬性的值來獲取對於函式的引用。
function somethingDoing(){
// 空函式
}
var doSomeThing = function doSomethingElse() {
// 空函式
}
var person = {
get firstName(){
return 'Nicholas';
},
sayName:function(){
console.log(this.name)
}
}
console.log(somethingDoing.name) // somethingDoing
console.log(doSomeThing.name) // doSomethingElse 函式表達式有個名字,這個名字比要賦值的變數權重高
console.log(person.sayName.name) // sayName
console.log(person.firstName.name) // get firstName
// 兩個特例
// 透過bind()建立的函式會帶前綴bind
var doSomthing = function(){}
console.log(doSomthing.bind().name) // "bound doSomething"
console.log(new Function().name) // anonymous
明確函式的多重用途
ES5當使用new時,函式內的this值指向一個新物件,函式最終返回一個新物件(沒有明確返回一個物件時)。
function Person(name){
this.name = name;
}
var person = new Person('Nicholas');
var notPerson = Person('Nicholas');
console.log(person) // '[Object object]'
console.log(notPerson) // undefined
上例中,在ES6中函式有兩個不同的內部存取[[Call]]和[[Construct]]。當透過new關鍵字呼叫函式時,執行的是[[Construct]]函式,它負責建立一個通常被稱作實例的新物件,然後執行函式體,將this繫結到實例上。而不使用new關鍵字時,則呼叫[[Call]]執行函式體,具有[[Construct]]方法的函式統稱為建構函式。切記不是所有的函式都有[[Construct]]方法 如:箭頭函式
ES5判斷函式是否被new呼叫
function Person(name){
if(this instanceof Person){
this.name = name
}else {
throw new Error('必須透過new關鍵字來呼叫Person')
}
}
上面的方式在一定程度上可以避免,但不要忘了還有call和apply呢……
var person = new Person('Nicholas');
var person = Person('Nicholas'); // 報錯
var person = Person.call(person,'Michael'); // 通過
無屬性new.target
為瞭解決判斷函式是否透過new關鍵字呼叫的問題,ES6引入了new.target這個元屬性(meta-property),元屬性是指非物件的屬性,其可以提供非物件目標的補充資訊。當呼叫函式[[Construct]]方法時,new.target上被賦予new的操作目標,通常是新建立的物件實例,也就是函式體內this的建構函式;如果呼叫[[Call]]方法,則new.target值為undefined,有了這個元屬性,可以透過檢查new.target是否被定義過來安全地偵測是否透過new關鍵字呼叫的。
function Person(name){
if(typeof new.target!=='undefined'){
this.name = name;
}else {
throw new Error('必須透過new關鍵字來呼叫Person')
}
}
var person = new Person('Nicholas'); // 通過
var person = Person('Nicholas'); // 報錯
var person = Person.call(person,'Michael'); // 報錯
也可能透過new.target是否被某個特定的建構函式所呼叫。如下:
function Person(name){
// if(typeof new.target!=='undefined'){
if(new.target===Person)
this.name = name;
}else {
throw new Error('必須透過new關鍵字來呼叫Person')
}
}
function AnotherPerson(name){
Person.call(this,name)
}
var person = new Person('Nicholas');
var otherPerson = new AnotherPerson('Michael') // 拋出錯誤
區塊級函式
ES5的嚴格格式下,我們在程式碼區塊中宣告函式是不被允許的,而在ES6中將doSomething()函式視作一個區塊級宣告,從而可以在定義該函式的程式碼內存取和呼叫它:
"use strict"
if(true){
console.log(typeof doSomething) // "function"
function doSomething(){
// 空函式
}
doSomething() //
}
typeof doSomething // undefined
和let的使用差不多,唯一區別是區塊級函式會將宣告提到程式碼區塊的頂部。注意函式表達式宣告的區別。在ES6中,即使處於非嚴格模式下,也可以宣告區塊級函式,但其行為與嚴格模式下稍有不同。這些函式不再提升至程式碼區塊的頂部,而是提升至外圍函式或全域作用域的頂部,如下:
// ECMAScript 6中的行為
if(true){
console.log(typeof doSomething) // "function"
function doSomething(){
// 空函式
}
doSomething() //
}
typeof doSomething // function
箭頭函式
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",function(event){
this.doSomething(event.type); // 拋出錯誤
},false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
上面的程式碼並沒有如預期的正常執行。因為this的繫結是事件目標物件的引用(這裡是指document),而沒有繫結在PageHandler,且由於this.doSomething()在目標document中不存在,所以無法正常執行,之前的做法是透過bind()方法明確地將this繫結到PageHandler函式上來修正這個問題。如下:
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",(function(event){
this.doSomething(event.type);
}).bind(this),false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
呼叫bind(this)後事實上建立了一個新函式,它的this被繫結到目前的this即PageHandler。為了避免建立一個額外的函式,我們可以透過一個更好的方式來修正這段程式碼,使用箭頭函式。箭頭函式中沒有this繫結,必須透過查找作用域鏈來決定其值。
let PageHandler = {
id:"123456",
init: function(){
document.addEventListener("click",(event)=>{
this.doSomething(event.type);
},false)
},
doSomething: function(type){
console.log("Handling "+ type + " for "+this.id)
}
}
- 沒有this、super、arguments和new.target繫結
- 不能透過new關鍵字呼叫 箭頭函式沒有
[[Construct]]方法,所以不能被用作建構函式 - 沒有原型
- 不可以改變this的繫結
- 不支援arguments物件
- 不支援重複的命名參數
箭頭函式也同樣有一個name屬性,這與其他函式的規則相同。
this繫結
如果箭頭函式被非箭頭函式包含,則this繫結的是最近一層非箭頭函式的this;否則,this的值會被設定為undefined。
箭頭函式和陣列
箭頭函式的語法簡潔,非常適用於陣列處理。舉例來說,比如給陣列排序,通常是:
var result = values.sort(function(a,b){
return a - b;
})
// 我們可以將其簡寫至如下:
var result = values.sort((a,b)=>a-b)
諸如
sort()、map()、reduce()這些可以接受回呼函式(callback function)的陣列方法,
尾端呼叫最佳化
函式作為另一個函式的最後一條語句被呼叫。
function doSomething(){
return doSomethingElse() // 尾端呼叫;
}
在ES5引擎中,尾端呼叫的實現與其他函式呼叫的實現類似:建立一個新的堆疊幀(stack frame),將其推入呼叫堆疊(call stack)來表示函式呼叫。也就是說,在循環呼叫中,每一個未用完的堆疊幀都會被保存在記憶體中,當呼叫堆疊變得過大時會造成程式問題。
為了縮減嚴格模式下尾端呼叫堆疊的大小(非嚴格格式不受影響),如果滿足以下條件,尾端呼叫不再建立新的堆疊幀,而是清除並重用目前的堆疊幀:
- 尾端呼叫不存取目前堆疊幀的變數(也就是說函式不是一個閉包)
- 在函式內部,尾端呼叫是最後一條語句
- 尾端呼叫的結果作為函式的返回值
"use strict"
function doSomething(){
return doSomethingElse() // 尾端呼叫最佳化;
}
// 注意以上3個條件
如何利用尾端呼叫最佳化
function factorial(n){
if(n<=1){
return 1
} else {
// 無法最佳化
return n * factorial(n-1)
}
}
// 可以利用傳遞第二個參數來保存每次階乘的結果(並且預設結果是1)
function factorial(n,p=1){
if(n<=1){
return 1*p
}else {
let result = n*p;
//最佳化後
return factorial(n-1,result)
}
}
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4310