寫在前面
(這篇本來是上週的內容,昨天忘記寫啦,趕緊偷偷補上)
最近事情有點多,雖然 sort 過了,還是略顯忙亂。但無論怎樣,計劃擺在那裡,最終都會一件一件完成
廢話適可而止,願老媽身體趕緊好起來~
一。箭頭函數簡介
箭頭函數(arrow function),就是 C# 中的 lambda 表達式,據說 Java8 也把它加入了豪華午餐。但不管怎样,JS 正在從其它語言吸納優秀的特性(比如 yield, class, 預設參數等等),且不論這些特性好壞,這件事本身就是極好的(至少我們正在使用的是一個充滿活力的工具)
只是 Java 用 -> 箭頭,C# 用的箭頭與 JS 一樣:=>,這個箭頭叫「lambda 運算符」,行話讀作 "goes to"
lambda 表達式(箭頭函數)據說是定義函數最簡潔的方法,語法上幾乎沒有冗餘成分了。因為 JS 弱類型的特點,JS 中的 lambda 表達式要比 C# 和 Java 中的更簡潔(少了參數類型聲明)
一句話,箭頭函數就是 lambda 表達式,提供了更簡潔的 function 定義方式
二。語法
arg => returnVal 語法是創建函數最簡潔的方式,定義了一個形參為 arg,返回值為 returnVal 的 function
其它語法如下表:
| 語法 | 等價代碼 | 含義 |
|---|---|---|
x => f(x) | function(x) {
return f(x);
} | y=f(x) |
(x, y)=>x + y; | function(x, y) {
return x + y;
} | y=f(x,y)=x+y |
(x, y)=>{g(x); g(y); return h(x, y);}; | function(x, y) {
g(x);
g(y);
return h(x, y);
} | g(x), g(y) y=f(x,y)==============h(x,y) |
()=>({}); | function() {
return {};
} | y={}
|
P.S. 第三列的「含義」指的是數學函數含義,lambda 表達式本來就是數學家弄出來的
簡單示例如下:
// 簡單例子,簡化匿名函數的定義
var arr = [1, 3, 21, 12];
console.log(arr.map(x => 2 * x)); // [2, 6, 42, 24]
console.log(arr.sort((a, b) => a - b)); // [1, 3, 12, 21]
arr.forEach((item, index, arr) => {
if (index %2 == 0) {
console.log(item);
}
if (index == arr.length - 1) {
console.log(`last item is ${item}`);
}
});
複雜一點的示例:
// 複雜例子
var app = {
cache: {},
ajax: function(url, callback) {
var self = this;
function req(url) {
var res = `data from ${url}`;
console.log(`ajax request ${url}`);
// cache res
self.cache[url] = res;
return res;
}
var data = req(url);
callback(data);
}
}
app.ajax('http://www.xxx.xx', function(data) {
console.log(`receive: ${data}`);
});
console.log(app.cache);
用箭頭函數改寫上例:
// 用箭頭函數改寫
var es6App = {
cache: {},
ajax(url, callback) {
var req = url => {
var res = `data from ${url}`;
console.log(`ajax request ${url}`);
// cache res
this.cache[url] = res;
return res;
}
var data = req(url);
callback(data);
}
}
es6App.ajax('http://www.q.xx', function(data) {
console.log(`receive: ${data}`);
});
console.log(es6App.cache);
消除了 that = this 這種必要的廢話,其實只要遵守一項原則就可以消除所有的 that = this,見下文注意事項中的 3. 關於 this
三。特點及注意事項
###1. 參數列表與返回值的語法
1 個參數時,左邊直接寫參數名,0 個或者多個參數時,參數列表要用 () 包裹起來
函數體只有 1 條語句時,右邊值自動成為函數返回值,函數體不止 1 條語句時,函數體需要用 {} 包裹起來,並且需要手動 return
P.S. 當然,可能很容易想到不分青紅皂白,把 () => {} 作為箭頭函數的起手式,但不建議這樣做,因為下一條說了 { 是有歧義的,可能會帶來麻煩
###2. 有歧義的字符
{ 是唯一1 個有歧義的字符,所以返回對象字面量時需要用 () 包裹,否則會被當作塊語句解析
例如:
var f1 = () => {};
f1(); // 返回 undefined
// 等價於
// var f1 = function() {};
var f2 = () => ({});
f2(); // 返回空對象 {}
// 等價於
// var f2 = function() {return {};};
###3. 關於 this
箭頭函數會從外圍作用域繼承 this,為了避免 that = this,需要遵守:除了對象上的直接函數屬性值用 function 語法外,其它函數都用箭頭函數
這個規則很容易理解,示例如下:
// 場景 1
function MyType() {}
MyType.prototype.fn = function() {/*定義箭頭函數*/}; // 箭頭函數中 this 指向 MyType 類型實例
// 場景 2
var obj = {};
obj.fn = function() {/*定義箭頭函數*/}; // 箭頭函數中 this 指向 obj
區別在於 function 關鍵字定義的函數屬性中,該函數的 this 指向這個函數屬性所屬的對象(匿名函數的 this 指向 global 對象或者 undefined)。說白了,function 能定義一個新 this,而箭頭函數不能,它只能從外層借一個 this。所以,需要新 this 出現的時候用 function 定義函數,想沿用外層的 this 時就用箭頭函數
###4. 關於 arguments 對象
箭頭函數沒有 arguments 對象,因為標準鼓勵使用 [預設參數、可變參數](/articles/預設參數和不定參數-es6 筆記 4/)、[參數解構](/articles/destructuring(解構賦值)-es6 筆記 5/)
例如:
// 一般函數
(function(a) {console.log(`a = ${a}, arguments[0] = ${arguments[0]}`)})(1);
// log print: a = 1, arguments[0] = 1
// 與上面等價的箭頭函數
(a => console.log(`a = ${a}, arguments[0] = ${arguments[0]}`))(1);
// log print: Uncaught ReferenceError: arguments is not defined
這與函數匿名不匿名無關,規則就是箭頭函數中無法訪問 arguments 對象(undefined),如下:
// 非匿名函數
var f = a => console.log(`a = ${a}, arguments[0] = ${arguments[0]}`);
f(2);
// log print: Uncaught ReferenceError: arguments is not defined
四。題外話
就 ES6 箭頭函數而言,上面的內容足以隨心所欲地駕馭它了,下面我們扯點別的(有意思的)
###1.JS 中支持的所有箭頭
| 箭頭 | 含義 |
|---|---|
| <!-- | 單行註釋 |
| --> | 在行首表示單行註釋,在其它位置表示「趨向於」(n --> 0 等價於 n-- > 0) |
| <= | 比較運算符,小於等於 |
| => | 箭頭函數 |
看到兩個單行註釋語法不要大驚小怪,歷史原因,但目前所有瀏覽器都支持。沒什麼用,冷知識吧
###2.lambda 演算與邱奇數
lambda 演算中唯一基礎數據類型是函數,邱奇數(church numerals)就是用高階函數表示常見的基礎數據類型(整數、布爾值、鍵值對、列表等等)
自然數都是數字,邱奇數都是函數,邱奇數的 n 是 n 階函數,f^n(inc, base) === f(inc, f(inc, ...f(inc, base))),所有邱奇數都是有 2 個參數的函數
如何用函數表示自然數?內容比較多,這裡給一個自然數集小例子:
// 定義自然數集合
var number = (function*(inc, base) {
var n = zero;
while(true) {
yield n(inc, base);
n = succ(n);
}
})(inc, 0);
for (var n of number) {
console.log(n); // 0, 1, 2, 3, 4
if (n > 3) {
break;
}
}
用邱奇數表示的自然數集如下:
// 0, 1, 2
var zero = (inc, base) => base;
var one = (inc, base) => inc(base);
var two = (inc, base) => inc(inc(base));
// 定義後繼函數 f^n -> f^(n+1)
// succ = ln.lf.lx.f (n f x)
var succ = n => (inc, base) => inc(n(inc, base));
// 定義邱奇數集合
var church = (function*() {
var fn = zero;
while(true) {
yield fn;
fn = succ(fn);
}
})();
// test
var [, , , three, four, five, six, seven] = church;
console.log(three(inc, 0)); // 3
console.log(four(inc, 0)); // 4
console.log(five(inc, 0)); // 5
console.log(six(inc, 0)); // 6
console.log(seven(inc, 0)); // 7
仔細想想的話會發現世界真奇妙,這樣也行??感興趣的話請查看筆者的 Demo(實現了減法、乘法和減法)
###3.Y 組合子與函數式編程
Y 組合子能實現匿名遞歸函數
Y 組合子就是一個函數,如下:
var Y = F => G(G), var G = slef => F(self(self))
其中有個不動點的概念。不動點:若 F(f) = f,則 f 是不動點,在 Y 組合子中,G(G) 是不動點
假設現有一個用來求階乘的遞歸函數:
var fact = n => n === 0 ? 1 : n * fact(n - 1);
這顯然不是一個匿名遞歸,fact 是函數名,遞歸調用它實現計算階乘。那麼如何實現一個匿名的遞歸函數?這有可能嗎?
用 Y 組合子來一發就好了,如下:
// 定義 Y 組合子
// var Y = F => G(G);
// var G = self => F(self(self));
var Y = F =>
((g => g(g))
(g =>
(F((...x) =>
g(g)(...x)))));
// 實現匿名遞歸求階乘
var yFact = Y(f =>
n => n === 0 ? 1 : n * f(n - 1));
console.log(yFact(5)); // 120
奇妙吧?函數式編程中有各種類似的奇妙變換,且不說 FP 的理解成本,執行效率,這些變換本身就是一些有意思的值得研究的東西,給思維多一點空間,讓 cpu 跑起來
五。總結
lambda 表達式的極致簡潔很誘人,定義函數就像寫數學公式一樣,支持函數式編程的語言本該如此
參考資料
-
APIO 講稿——函數式編程:byvoid 前輩的講稿,汗顏
-
《ES6 in Depth》:InfoQ 中文站提供的免費電子書
暫無評論,快來發表你的看法吧