1. 程式人生 > >深入淺出ES6(一):你真的瞭解箭頭函式嗎

深入淺出ES6(一):你真的瞭解箭頭函式嗎

前言
這個系列主要是說明ES6的新特性,從2015年到現在,es6出來也有挺長一段時間了,在專案中也在普遍使用這些特性.,網上的寫es6的文章也大把, 但我感覺可能還是停留在會用的階段,,至於為什麼要這麼用, 又為什麼會出現這個特性,解決了什麼樣的問題,這些都有些一知半解. 所以,打算抽時間去了解es6未知的一面.
這篇文章,就先從用的最多的箭頭函式開始->

箭頭函式的由來
為什麼叫箭頭函式?
因為,它的定義用的是一個箭頭.
那為什麼要用一個箭頭?
先來看一段程式碼:

  <script language="javascript">
    <!--
      document.bgColor = "brown"
; // red // -->
</script>

有沒覺得很奇怪?在註釋裡邊的程式碼也有效?當然,如果知道註釋風格的程式碼的人,應該可以理解.

箭頭符號在JavaScript誕生時就已經存在,當初第一個JavaScript教程曾建議在HTML註釋內包裹行內指令碼,這樣可以避免不支援JS的瀏覽器誤將JS程式碼顯示為文字。

老式瀏覽器會將這段程式碼解析為兩個不支援的標籤和一條註釋,只有新式瀏覽器才能識別出其中的JS程式碼。

為了支援這種奇怪的hack方式,瀏覽器中的JavaScript引擎將<!--這四個字元解析為單行註釋的起始部分,我沒開玩笑,這自始至終就是語言的一部分,直到現在仍然有效, 這種註釋符號不僅出現<script>

標籤後的首行,在JS程式碼的每個角落你都有可能見到它,甚至在Node中也是如此。

碰巧,這種註釋風格首次在ES6中被標準化了,但在新標準中箭頭被用來做其它事情。

箭頭序列–>同樣是單行註釋的一部分。古怪的是,在HTML中–>之前的字元是註釋的一部分,而在JS中–>之後的部分才是註釋。

你一定感到陌生的是,只有當箭頭在行首時才會註釋當前行。這是因為在其它上下文中,–>是一個JS運算子:“趨向於”運算子!

function countdown(n) {
while (n –> 0) // “n goes to zero”
alert(n);
blastoff();
}

上面這段程式碼可以正常執行,迴圈會一直重複直到n趨於0,這當然不是ES6中的新特性,它只不過是將兩個你早已熟悉的特性通過一些誤導性的手段結合在一起。你能理解麼?通常來說,類似這種謎團都可以在Stack Overflow上找到答案。

當然,同樣地,小於等於操作符<=也形似箭頭,你可以在JS程式碼、隱藏的圖片樣式中找到更多類似的箭頭,但是我們就不繼續尋找了,你應該注意到我們漏掉了一種特殊的箭頭。
當然,同樣地,小於等於操作符<=也形似箭頭,你可以在JS程式碼、隱藏的圖片樣式中找到更多類似的箭頭,但是我們就不繼續尋找了,你應該注意到我們漏掉了一種特殊的箭頭。

<!-- 單行註釋
--> “趨向於”操作符
<= 小於等於
=> 這又是什麼?
=> 到底是什麼?我們今天就來一探究竟。

首先,我們談論一些有關函式的事情。

函式表示式無處不在
JavaScript中有一個有趣的特性,無論何時,當你需要一個函式時,你都可以在想新增的地方輸入這個函式。

舉個例子,假設你嘗試告訴瀏覽器使用者點選一個特定按鈕後的行為,你會這樣寫:

$("#confetti-btn").click(

jQuery的.click()方法接受一個引數:一個函式。沒問題,你可以在這裡輸入一個函式:

$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

對 於現在的我們來說,寫出這樣的程式碼相當自然,而回憶起在這種程式設計方式流行之前,這種寫法相對陌生一些,許多語言中都沒有這種特性。1958年,Lisp首 先支援函式表示式,也支援呼叫lambda函式,而C++,Python、C#以及Java在隨後的多年中一直不支援這樣的特性。

現在截然不同,所有的四種語言都已支援lambda函式,更新出現的語言普遍都支援內建的lambda函式。我們必須要感謝JavaScript和早期的JavaScript程式設計師,他們勇敢地構建了重度依賴lambda函式的庫,讓這種特性被廣泛接受。

令人傷感的是,隨後在所有我提及的語言中,只有JavaScript的lambda的語法最終變得冗長乏味。

// 六種語言中的簡單函式示例
function (a) { return a > 0; } // JS
[](int a) { return a > 0; }  // C++
(lambda (a) (> a 0))  ;; Lisp
lambda a: a > 0  # Python
a => a > 0  // C#
a -> a > 0  // Java


箭袋中的新羽
ES6中引入了一種編寫函式的新語法

// ES5
var selected = allJobs.filter(function (job) {
  return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());

當你只需要一個只有一個引數的簡單函式時,可以使用新標準中的箭頭函式,它的語法非常簡單:識別符號=>表示式。你無需輸入function和return,一些小括號、大括號以及分號也可以省略。

(我個人對於這個特性非常感激,不再需要輸入function這幾個字元對我而言至關重要,因為我總是不可避免地錯誤寫成functoin,然後我就不得不回過頭改正它。)

如果要寫一個接受多重引數(也可能沒有引數,或者是不定引數、預設引數、引數解構)的函式,你需要用小括號包裹引數list。

// ES5
var total = values.reduce(function (a, b) {
  return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);

我認為這看起來酷斃了。

正如你使用類似Underscore.js和Immutable.js這樣的庫提供的函式工具,箭頭函式執行起來同樣美不可言。事實上,Immutable的文件中的示例全都由ES6寫成,其中的許多特性已經用上了箭頭函式。

那麼不是非常函式化的情況又如何呢?除表示式外,箭頭函式還可以包含一個塊語句。回想一下我們之前的示例:

// ES5
$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

這是它們在ES6中看起來的樣子:

// ES6
$("#confetti-btn").click(event => {
  playTrumpet();
  fireConfettiCannon();
});

這是一個微小的改進,對於使用了Promises的程式碼來說箭頭函式的效果可以變得更加戲劇性,}).then(function (result) { 這樣的一行程式碼可以堆積起來。

注意,使用了塊語句的箭頭函式不會自動返回值,你需要使用return語句將所需值返回。

小提示:當使用箭頭函式建立普通物件時,你總是需要將物件包裹在小括號裡。

// 為與你玩耍的每一個小狗建立一個新的空物件
var chewToys = puppies.map(puppy => {});   // 這樣寫會報Bug!
var chewToys = puppies.map(puppy => ({})); //

用小括號包裹空物件就可以了。

不幸的是,一個空物件{}和一個空的塊{}看起來完全一樣。ES6中的規則是,緊隨箭頭的{被解析為塊的開始,而不是物件的開始。因此,puppy => {}這段程式碼就被解析為沒有任何行為並返回undefined的箭頭函式。

更令人困惑的是,你的JavaScript引擎會將類似{key: value}的物件字面量解析為一個包含標記語句的塊。幸運的是,{是唯一一個有歧義的字元,所以用小括號包裹物件字面量是唯一一個你需要牢記的小竅門。


箭頭函式與this

既然要說箭頭函式, 自然避不開this了
瞭解es5的人,應該知道, 在不同地方使用this,它的指向也有所不同, 但總歸來講, 實際上this總是指向最後呼叫它的物件.

舉個例子:

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window        }
    }
}
var j = o.b.fn;
j();

上面這個例子中,fn函式始終沒有執行,直到執行j();而j()又是通過window來呼叫,所以最後輸出的this是window.
總的來說, this的呼叫方式和指向有以下幾種:

函式呼叫方式和this指向:
  (1)直接呼叫:函式內部this指向全域性window2) 通過物件使用點來呼叫:函式內部this指向呼叫物件
  (3) 觸發事件呼叫函式:函式內部this指向呼叫觸發事件的物件
  (4) 以new的方式來呼叫:函式內部this指向本次函式執行時對應的一個匿名物件。
  (5) 通過call的方法來間接呼叫方法:函式內部this指向call方法的第一個引數(自己指定this)。

這篇文章的目的不是要解析this, 如果對this不清楚的童鞋,可以參考這篇文件: this 指向詳細解析(箭頭函式)

看到this在這麼多呼叫方式下的指向都不同, 有沒有被繞暈? 相信肯定也有很多童鞋跟我一樣,踩過this的坑. 而箭頭函式的存在, 就避免了這種指向問題,在箭頭函式中, this總是指向詞法作用域, 也就是外層的呼叫者.

看個例子:

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};

上面這個例子中,fn()並沒有得到我們預期的日期, 而是出錯,因為在fn()函式中,this.birth並不是指向當前obj,而是指向函式的呼叫者window, 因此birth是undefined,

現在讓我們用箭頭函式來改造這個函式:

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj物件
        return fn();
    }
};
obj.getAge(); // 27

在箭頭函式中, this指向的是外層呼叫者obj,因此最後拿到了birth這個引數.

另外,在箭頭函式中,我們不再需要這種hack寫法:

var that = this;

關於箭頭函式的案例
上面應該大致說明白了箭頭函式吧?
那麼現在,就來說說它的使用.

改造開始

首先,我們從一個例子開始,在 ES5 中,我們一般是這麼書寫的。

var sum1 = function(num1, num2) { return num1 + num2; };

那麼,改造成箭頭函式,它是什麼樣子呢?

var sum2 = (num1, num2) => { return num1 + num2;};

小括號內的引數列表和花括號內的程式碼被 => 分隔開了。這個就是箭頭函式的魅力,箭頭函式使得表達更加簡潔,從而簡化了我們的程式碼。

如果一個表示式的程式碼塊, 只是 return 後面跟一個表示式,那麼還可以進一步簡化。

var sum3 = (num1, num2) => num1 + num2;

如果某個方法只含有一個引數。

console.info(“=> ES5 寫法”);
var curf1 = function(v) {
return v;
};

我們甚至可以省略小括號。

console.info(“=> ES6 寫法”);
var curf2 = v => v;

如果某個方法沒有引數。

console.info(“=> ES5 寫法”);
var f1 = function() {
return “樑桂釗”;
};

我們仍可以提供一對空的小括號,如同不含引數的

console.info(“=> ES6 寫法”);
var f2 = () => “樑桂釗”;

補充一個例外,如果箭頭函式直接返回一個物件,必須在物件外面加上括號。

console.info("=> ES5 寫法");
var f3 = function() {
return {
real_name: "樑桂釗",
nick_name: "LiangGzone"
}
};
console.log(f3());

console.info("=> ES6 寫法");
var f4 = () => ({real_name: "樑桂釗",nick_name: "LiangGzone"});
console.log(f4());


關於解構
我們還可以使用到 ES6 解構賦值特性。ES5 寫法,之前是這樣的。

var f5 = function(person) {
return person.first + ' ' + person.last;
}

使用到 ES6 解構賦值特性後,就更加好理解了。

const f6 = ({ first, last }) => first + ' ' + last;

關於回撥函式
我們經常使用回撥函式,之前的常規的做法。

console.info("=> ES5 寫法");
var x1 = [1,2,3].map(function (x) {
return x * x;
});
console.info(x1);

那麼,現在我們可以進行改造。

console.info("=> ES6 寫法");
var x2 = [1,2,3].map(x => x * x);
console.info(x2);

rest引數結合

沒有使用箭頭函式,之前,我們的程式碼可能長這樣子。

console.info("=> ES5 寫法");
var x3 = function(...nums){
return nums;
}
console.info(x3(512, 1024));

那麼,現在我們可以進行改造。

console.info("=> ES6 寫法");
var x4 = (...nums) => nums;
console.info(x4(512, 1024));

那麼, 我們什麼時候會用到箭頭函式呢?
ES6 的箭頭函式在微軟的新版本中有使用, 他們也在 Babel,Traceur,和 TypeScript 得到實現, 等等

以上就是我對箭頭函式的一點點見解, 如果有不對的地方, 歡迎指正!