1. 函數參數的默認值
[ 基本用法 ]
在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。
上面的代碼檢查函數log的參數y有沒有賦值,如果沒有,則指定默認值為world。這種寫法的缺點在於,如果參數y賦值了,但是對應的布爾值為false,則該賦值不起作用。就像以上代碼的最后一行,參數y等於空字符,結果被改為默認值。
ES6允許為函數的參數設置默認值,即直接寫在參數定義的后面。
[ 與解構賦值默認值結合使用 ]
參數默認值可以與解構賦值的默認值結合起來使用。
[ 參數默認值的位置 ]
通常情況下,定義了默認值的參數應該是函數的尾參數。如果非尾部的參數設置默認值,實際上這個參數是無法省略的。
有默認值的參數都不是尾參數。這時,無法只省略該參數而不省略其后的參數,除非顯式輸入undefined。如果傳入undefined,將觸發該參數等於默認值,null沒有這個結果。
[ 函數的length屬性 ]
指定了默認值之后,函數的length屬性將返回沒有指定默認值的參數個數。
length屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之后,預期傳入的參數個數就不包括這個參數了。
[ 作用域 ]
如果參數默認值是一個變量,則該變量所處的作用域與其他變量的作用域規則是一樣的,即先是當前函數的作用域,然后才是全局作用域。
如果調用時函數作用域內部的變量x沒有生成,結果就會不一樣。
函數調用時y的默認值變量x尚未在函數內部生成,所以x指向全局變量。
[ 應用 ]
利用參數默認值,可以指定某一個參數不得省略,如果省略就拋出一個錯誤。
以上代碼中的foo函數,如果調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。
2. rest參數
ES6引入了rest參數(形式為"...變量名"),用於獲取函數的多余參數,這樣就不需要使用arguments對象了。rest參數搭配的變量是一個數組,該變量將多余的參數放入其中。
以上代碼中的add函數是一個求和函數,利用rest參數可以向該函數傳入任意數目的參數。下面是rest參數代替arguments變量的例子。
// arguments 變量的寫法
// rest 變量的寫法
注意:rest參數之后不能有其他參數,函數的length屬性不包括rest參數。
3. 擴展運算符
[ 含義 ]
擴展運算符是三個點(...)。它好比rest參數的逆運算,將一個數組轉為用逗號分隔的參數序列。
擴展運算符與正常的函數參數可以結合使用。
function f(v,w,x,y,z){} var args = [0,1]; f(-1,...args,2,...[3]);
[ 替代數組的apply方法 ]
由於擴展運算符可以展開數組,所以不再需要apply方法將數組轉為函數的參數了。下面是擴展運算符取代apply方法的一個實際的例子,應用Math.max方法簡化求數組最大元素的寫法:
// ES5的寫法
Math.max.apply(null,[14,3,77]) // ES6的寫法
Math.max(...[14,3,77])
[ 擴展運算符的應用 ]
- 合並數組
擴展運算符提供了數組合並的新寫法。
- 與解構賦值結合
擴展運算符可以與解構賦值結合起來用於生成數組。
如果將擴展運算符用於數組賦值,只能放在參數的最后一位,不然會報錯。
- 函數的返回值
JavaScript的函數只能返回一個值,如果需要返回多個值,只能返回數組或對象。擴展運算符提供了解決這個問題的一種變通方法。
var dateFields = readDateFields(database); var d = new Date(...dateFields);
上面的代碼用於從數據庫中取出一行數據,通過擴展運算符直接將其傳入構造函數Date。
- 字符串
擴展運算符還可以將字符串轉為真正的數組。
[ 類似數組的對象 ]
任何類似數組的對象都可以用擴展運算符轉為真正的數組。
var nodeList = document.querySelectorAll("div"); var array = [...nodeList];
上面的代碼中,querySelectorAll方法返回的是一個nodeList對象,擴展運算符可以將其轉為真正的數組。
[ Map 和 Set 結構,Generator 函數 ]
擴展運算符內部調用的是數據結構的Iterator接口,因此只要具有Iterator接口的對象,都可以使用擴展運算符,比如Map結構。
[ name屬性 ]
函數的name屬性返回該函數的函數名。
如果將一個匿名函數賦值給一個變量,ES5的name屬性會返回空字符串,而ES6的name屬性會返回實際的函數名。
如果將一個具名函數賦值給一個變量,則ES5和ES6的name屬性都返回這個具名函數原本的名字。
5. 箭頭函數
[ 基本用法 ]
ES6允許使用"箭頭"(=>)定義函數。
var f = v => v; // 上面的箭頭函數等同於
var a = function(v){ return v; };
如果箭頭函數不需要參數或需要多個參數,就使用圓括號代表參數部分。
var f = () => 5; // 等同於
var f = function(){ return 5 }; var sum = (num1,num2) => num1 + num2; // 等同於
var sum = function(num1,num2){ return num1+num2; };
如果箭頭函數的代碼塊部分多於一條語句,就要使用大括號將其括起來,並使用return語句返回。
var sum = (num1,num2) => { return num1 + num2 }
由於大括號被解釋為代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號。
var getTempItem = id => ({ id: id, name: "Temp" });
箭頭函數可以與變量解構結合使用。
const full = ({ first, last }) => first + '' + last; // 等同於
function full(person){ return person.first + '' + person.name; }
箭頭函數使得表達更加簡潔。
const isEven = n => n % 2 == 0;
箭頭函數的一個用處是簡化回調函數。
// 正常函數的寫法
[1,2,3].map(function(x){ return x*x; }); // 箭頭函數的寫法
[1,2,3].map(x => x*x);
箭頭函數還能與rest參數結合。
[ 使用注意點 ]
箭頭函數有幾個使用注意點。
● 函數體內的this對象就是定義時所在的對象,而不是使用時所在的對象。
● 不可以當做構造函數。也就是說,不可以使用new命令,否則會拋出一個錯誤。
● 不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用rest參數代替。
● 不可以使用yield命令,因此箭頭函數不能用作Generator函數。
6.函數綁定
箭頭函數沒有自己的this,導致內部的this就是外層代碼塊的this。正因為它沒有this,所以也就不能用作構造函數。當然也不能使用call()、apply()、bind()這些方法去改變this的指向。
函數綁定運算符是並排的雙冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象作為上下文環境綁定到右邊的函數上。
foo::bar // 等同於
bar.bind(foo) foo::bar(...arguments) // 等同於
bar.bind(foo,arguments)
如果雙冒號左邊為空,右邊是一個對象的方法,則等於將該方法綁定在該對象上。
var method = obj::obj.foo // 等同於
var method = ::obj.foo
由於雙冒號運算符返回的還是原對象,因此可以采用鏈式寫法。
import {map,takeWhild,forEach} from "iterlib"; getPlayers() ::map(x => x.character()) ::takeWhile(x => x.strength > 100); ::forEach(x => console.log(x));
7.尾調用優化
[ 尾調用概念 ]
尾調用是函數式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數的最后一步是調用另一個函數。
function f(x){ return g(x) }
以下情況都不屬於尾調用:
// 情況一
function f(x){ let y = g(x); return y; } // 情況二
function f(x){ return g(x)+1; } // 情況三
function f(x){ g(X); }
上面的代碼中,情況一是調用函數g之后還有賦值操作,所以不屬於尾調用,即使語義完全一樣;情況二也屬於調用后還有操作,即使寫在一行內;情況三等同於下面的代碼。
function f(x){ f(x); return undefined; }