JavaScript 2015 特性概述

LynMcGahan 8年前發布 | 10K 次閱讀 JavaScript開發 JavaScript ECMAScript

本文中將重溫 JavaScript/ECMAScript 2015 的新特性,該版本是對 JavaScript 語言的重大更新。我們會特別重視這些特性是如何有助于更大系統的開發,以及如何與過去的方法比較。還會向你展示如何用 ECMAScript 2015,加上 async/await 支持,來創建一個現代項目。請繼續閱讀!

本文基于 Luke Hoban 的出色工作,以及他的 es6features GitHub repository 。對于想學習更多的人來說,另一個不錯的資源就是 Mozilla 開發者網絡 。當然,致謝詞如果不提到 Rauschmayer 博士的博客 那就是不完整的,在他的博客中可以找到對 ECMAScript 2015 的深入看法。

簡介

在多年的緩慢發展之后,JavaScript 已經看到了重生。Node.js 和較新的前端框架以及庫已經恢復了語言背后的熱情。它在 medium 和大系統上的使用已經讓人們努力思考 JavaScript 需要如何發展。其結果就是 ECMAScript 2015,對該語言的一次重大更新,帶來了很多已經計劃很久的想法。下面我們來看看這些想法是如何有助于讓 JavaScript 成為一門對今天的各種使用都更好的語言。

ECMAScript 2015 特性

Let 和 Const

JavaScript 最初就只有一種聲明變量的方式: var 。不過, var 語句遵從 變量提升 的規則。也就是說, var 聲明表現的好像是變量聲明在當前執行上下文(函數)的頂部一樣。這會導致不直觀的行為:

function test() {
    // 想寫一個全局變量 'foo'
    foo = 2;

    // 大量代碼在這里

    for(var i = 0; i < 5; ++i) {
        // 這個聲明被移到函數頂部,導致第一個 'foo' 成為局部變量而不是全局的。
        var foo = i;
    }
}

test();
console.log(foo); //應該打印 2,但是結果一個異常。

對于大的代碼庫,變量提升會導致不可預期以及有時令人吃驚的行為。特別是,在很多其它流行的語言中,變量聲明是被限制到封閉塊的詞法作用域,所以 JavaScript 新手會完全忽視 var 的語義。

ECMAScript 2015 引入了兩種聲明變量的新方法: let 和 const 。這兩個語句的行為與其它語言更符合。

LET

let 語句的寫法與 var 完全一樣,但是有一個很大的不同之處: let 聲明被限制到包圍作用域,但是只在從語句所在的點向前可用。換句話說,在 for 循環內,或者僅在封閉括號內聲明的變量,只在該塊內有效,并且只在該 let 語句之后有效。這種行為更直觀。用 let 替代 var 在很多情況下是被鼓勵的。

CONST

const 的概念稍微復雜點。在 JavaScript 中,所有聲明都是 可重新綁定的(rebindable) 。變量聲明在 name 和 JavaScript 對象或者基礎類型之間建立了一種連接。這個相同的名稱其后可能被重新綁定到不同的對象或者基礎類型。也就是說:

var foo = 3; //foo 被綁定到基礎類型數據 3。
foo = ["abc", "def"]; // foo 現在被綁定到一個數組對象。

const 語句與 let 和 var 語句相反,它不允許在初始聲明后,將名稱重新綁定到不同對象:

const foo = 3; //foo 被綁定到基礎類型數據 3。
foo = ["abc", "def"]; // TypeError exception!

值得注意的是, const 不會以任何方式影響 可寫性(writability) 。這與諸如 C 和 C++ 這類語言的 const 概念是相反的。可以說,選擇 const 這么個名字可能不是一個好主意。

可寫性可以用 Object.defineProperty() 和 Object.freeze() 控制,并且與 const 語句無關。要記住在非嚴格模式下寫入只讀屬性是會被默默忽略的。嚴格模式下會將這些錯誤報告為 TypeError 異常。

在某種綁定可以被操縱的地方放上較為嚴格的要求可以防止代碼錯誤。在這種情況下, let 和 const 都有很大幫助。

箭頭函數和詞法 this

JavaScript 作為一門多范式語言,可以使用很多函數式特性。在這些特性中,閉包和匿名函數是精華。箭頭函數引入了一種新的、較短的語法來聲明它們。我們來看看:

// ES2015 之前
[1, 2, 3, 4].forEach(function(element, idx) {
    console.log(element);
});

// ES2015 之后:箭頭函數
[1, 2, 3, 4].forEach((element, idx) => {
    console.log(element);
});

起初這可能看起來像小的改進。不過,當箭頭函數遇到 this 、 arguments 、 super 和 new.target 時,表現的就完全不同了。這些都是在函數作用域內局部預定義的聲明。箭頭函數是從外層函數繼承這些元素的值,而不是聲明重新聲明一套。這就防止了出錯,并且整理出某種常見的代碼模式:

function Counter() {
    this.count = 20;

    setInterval(function callback() {
        ++this.count; // BUG! this 指向 Global 對象
                      // 或者是未定義的(在嚴格模式下)
    }, 1000);
}

const counter = new Counter();

很容易像這樣犯錯。過去修復這種錯誤的方式相當麻煩:

function Counter() {
    // 每當在一個局部函數內需要引用 this 時,我們就會用這種方式。
    var that = this;
    this.count = 20;

    setInterval(function callback() {
        ++that.count; 
    }, 1000);
}

const counter = new Counter();

而在 ECMAScript 2015 中事情就更簡單明了:

function Counter() {
    this.count = 20;

    setInterval(() => {
        // this 綁定到外層作用域的 this 值。
        ++this.count; 
    }, 1000);
}

const counter = new Counter();

JavaScript 類

JavaScript 從它最初就已經支持了面向對象編程。不過,JavaScript 實現 OOP 的形式對很多開發者來說卻是完全不熟悉的,特別是哪些來自 Java 和 C++ 語言家族的開發者。這兩類語言以及很多其它語言是本著 Simula 67 的精神來實現對象。而 JavaScript 是本著 Self 的精神來實現對象。這種 OOP 模式被稱為 基于原型編程

基于原型編程對于來自其它對象模式的開發者來說可能是不直觀的。這已經導致了很多 JavaScript 庫推出了它們自己使用對象的方法。這些方法有時候是不兼容的。基于原型編程足夠強大去模擬基于類的編程模型,庫編寫者已經推出了很多這樣做的方法。

在做這事的方法上缺乏一致意見已經導致分裂以及庫之間的耦合問題。ECMAScript 2015 試圖糾正此問題,它提供了一種在原型之上做基于類的編程的常用方法。這已經導致社區中的一些爭論,因為很多人認為基于原型的方式更好。

ECMAScript 2015 中的類是在原型之上模仿類的語法糖:

class Vehicle {
    constructor(maxSpeed) {
        this.maxSpeed = maxSpeed;
    }

    get maxSpeed() {
        return maxSpeed;
    }
}

class Car extends Vehicle {
    constructor(maxSpeed) {
        super(maxSpeed);
        this.wheelCount = 4;
    }
}

在基于原型的方式中看起來是這樣的:

function Vehicle(maxSpeed) {
    this.maxSpeed = maxSpeed;
}

Vehicle.prototype.maxSpeed = function() {
    return this.maxSpeed;
}

function Car(maxSpeed) {
    Vehicle.call(this, maxSpeed);
}

Car.prototype = new Vehicle();

JavaScript 解釋器將類翻譯為原型鏈所采用的確切步驟在 JavaScript 規范 中已經有了。

對于大的項目來說,類的實際用處與傾向于原型相比,是一個討論較多的問題。有些人認為基于類的設計隨著代碼庫的發展會較難擴展,或者說,基于類的設計需要考慮更周全。而另一方面,類的倡導者認為類更容易被來自其它語言的開發者所理解,而且有久經考驗的現成設計證明其有用性。

作為啟發 JavaScript 原型的語言,Self 的設計目標之一,就是要避免 Simula 方式的對象的問題。特別是,類和實例之間的對立被看作是 Simula 方法中很多內在問題的起因。有人認為,因為類為對象實例提供了某種原型,隨著代碼演變和變得更大,它越來越難讓這些基類適應預料不到的新需求。通過為構造新對象的原型創建實例,這種限制就被移除了。因此 原型 的概念是:通過提供它自己的行為,來填補新實例空白的實例。如果一個原型被認為不適合創建新對象,可以就將它克隆,然后修改,不會影響所有其它子實例。這可以說在基于類的方式中(即修改基類)是較難做到的。

不管你在此問題上是什么想法,有一件事情是清楚的:如果你寧愿堅持基于類的方式,那么現在有一個官方批準這樣做的方法。否則,就對核心內容用原型好了。

JavaScript 對象字面量改進

另一個帶來實用性的特性是對象字面量聲明的改進。來看一看:

function getKey() {
    return 'some key';
}

let obj = {

    // 原型可以按這種方式設置
    __proto__: prototypeObject,

    // key === value, someObject: someObject 的縮寫
    someObject,

    // 方法現在可以按這種方式定義
    method() {
        return 3;
    },

    // key 的動態值
    [getKey()]: 'some value'
};

對比一下,過去做這些事情需要像這樣:

let obj = {
    someObject: someObject,
    method: function() {
        return 3;
    }
};

obj.prototype = prototypeObject;
obj[getKey()] = 'some value';

任何有助于可讀性,以及讓應該合成整體的代碼塊盡可能靠近的事情都有助于減少犯錯的機會。

JavaScript 模板字符串字面量

現在幾乎每個項目中你都需要將值插到一個字符串中。在 JavaScript 做這事的標準方式是通過重復的字符串連接:

var str = 'The result of operation ' + op + ' is ' + someNumber;

這樣做不咋漂亮,或者有可維護性的問題。假如有一個很長的字符串帶有很多值,那么事情就會很快失控。

基于此原因,像 sprintf 這種庫被創建了,該庫是受 C 的 sprintf 函數啟發:

var str = sprintf('The result of operation %s is %s', op, someNumber);

這樣做要好一些,但是太像 C 的 sprintf 了,格式字符串和傳遞給 sprintf 所需的值之間是完美相關的。如果從調用中刪除一個參數,就會得到 bug。

ECMAScript 2015 帶來了更好的解決方案:

const str = `The result of operation ${op} is ${someNumber}`;

簡單,并且較難打破!這些新字符串字面量的附加特性是支持多行:

const str = `This is a very long string.
We have broken it into multiple lines to make it easier to read.`;

其它與字符串相關的附加特性是原始字符串和標簽函數。原始字符串有助于防止與轉義符和引號相關的錯誤:

String.raw`Hi\u000A!`; // 不處理 unicode 轉義符

如果還沒有理解字符串標簽的話,語法會看起來有點古怪:

function tag(strings, ...values) {
  console.log(strings[0]); // "Hello "
  console.log(strings[1]); // " world "
  console.log(strings[2]); // ""
  console.log(values[0]);  // 1
  console.log(values[1]);  // 'something'

  return "這是被返回的字符串,它不需要用參數";
}

const foo = 1;
const bar = 'something';

tag`Hello ${a} world ${b}`;

標簽函數本質上是以任意方式轉換字符串字面量的函數。可想而知,它們可能被以損害可讀性的方式濫用,所以要小心使用。

ECMAScript 2015 Promise

ECMAScript 2015 中最大特性之一。Promise 試圖給 JavaScript 的異步本性帶來點理智。如果你是一名經驗豐富的 JavaScript 開發者,就知道回調和閉包是主流。你也知道,它們是很靈活的。這意味著每個人都有權選擇如何去用它們。而在動態語言中,如果出乎意料地將兩個回調約定混合在一起,是沒有人能阻止你的。

如下是沒有用 promise 時 JavaScript 中的樣子:

var updateStatement = '...';

function apiDoSomething(withThis) {
    var url = 'https://some.cool.backend.com/api/justDoIt';
    httpLib.request(url, withThis, function(result) {
        try { 
            database.update(updateStatement, parseResult(result), 
                function(err) {
                    logger.error('HELP! ' + err);
                    apiRollbackSomething(withThis);
                }
            );
        } catch(e) {
            logger.error('EXCEPTION ' + e.toString());
            apiRollbackSomething(withThis);
        }
    }, function(error) {
        logger.error('HELP! ' + error + ' (from: ' + url + ')');
    });
}

這看上去很簡單。為什么說是看上去呢?因為它實際上對以后的程序員(或者你自己!)來說是一個雷區。下面我們一步一步看看它。我們首先看到的是 updateStatement 。這個變量可能是包含一條 SQL 語句或者命令,可能是用某個值做參數去更新數據庫。但是 var 不會阻止之后將 updateStatement 重新綁定到其它地方,所以,如果偶然有人這些寫:

function buggedFunction() {
    // 重新綁定全局的 updateStatement!
    updateStatement = 'some function local update statement';
    // ...
}

而不是:

function buggedFunction() {
    // 遮蔽全局 updateStatement
    var updateStatement = 'some function local update statement';
    // ...
}

那么,你得到的就是...一個 BUG!

但是這與 promise 沒有任何關系,我們繼續看:

httpLib.request(url, withThis, function(result) {
    try { 
        database.update(updateStatement, parseResult(result), 
            function(err) {
                logger.error('HELP! ' + err);
                apiRollbackSomething(withThis);
            }
        );
    } catch(e) {
        logger.error('EXCEPTION ' + e.toString());
        apiRollbackSomething(withThis);
    }
}, function(error) {
    logger.error('HELP! ' + error + ' (from: ' + url + ')');
});

仔細看看這段代碼。可以看到,這里有兩類回調,一個嵌套在另一個中。二者對如何處理錯誤,以及如何傳遞成功調用后的結果,有不同的約定。出現不明錯誤時,不一致性是一個大的因素。不僅如此,它們嵌套的方式會阻止異常處理器成為塊中唯一的故障點,所以 apiRollbackSomething 需要用完全相同的參數調用兩次。這是特別危險的。假如有人在將來修改代碼,添加新的失敗分支,會怎么樣?這人還記得要回滾么?這人甚至能看到它么?最后,logger 也被多次調用,只是為了展示當前的錯誤,并且傳遞給它的參數是用字符串連接構造的,這又是不明錯誤的另一個來源。也就是說,這個函數對很多錯誤打開了大門。下面我們來看看 ECMAScript 是如何能幫助我們防止這些錯誤:

// 這樣將來就不會被重新綁定!加上字符串是常量,所以保證永遠不會修改。
const updateStatement = '...'; 

function apiDoSomething(withThis) {
    const url = 'https://some.cool.backend.com/api/justDoIt';
    httpLib.request(url, withThis).then(result => {
        // database.update 也返回一個 promise
        return database.update(updateStatement,parseResult(result));
    }).catch(error => {
        logger.error(`ERROR: ${error} (from url: ${url})`);
        // 我們的API是如此,這樣萬一最初的請求沒有成功,回滾就被認為是空操作,
        // 所以在這里調用它是 OK 的。
        apiRollbackSomething(withThis);
    });
}

這樣很漂亮。前面描述的所有沖突點都被 ECMAScript 2015 摧毀了。當代碼像這樣呈現時,很難犯錯,而且更容易讀。這是個雙贏的結果。

那么,為什么我們要從 database.update 返回結果呢?這是因為 promise 可以是鏈式的。也就是說,如果成功了,promise 可以把結果傳給鏈中的下一個 promise;如果失敗了,它可以執行正確的行為。下面我們來看看這在上例中是如何工作的。

第一個 promise 是 httpLib.request 創建的那一個。這是最外層的 promise,它將是告訴我們 httpLib.request 執行順利或者失敗的那一個。不管順利還是失敗,如果要做點事情的話,我們可以用 then 或者 catch 函數。這兩個函數并非必須要調用。你可以調用一個,也可以兩個都調用(像上面我們做的那樣),或者是完全不理會結果。現在,在兩個處理器中有兩件事情會發生:

  1. 可以把數據傳遞給函數(要么是結果,要么是錯誤),并返回一個值、一個 promise 或者什么都不返回。
  2. 可以拋出一個異常。

如果拋出了一個異常, then 和 catch 都知道如何處理它:把它當作一個錯誤條件。也就是說,鏈中的下一個 catch 會捕獲該異常。在本例中,最外層的 catch 捕獲所有錯誤,包括由 httpLib.request promise 產生的那些錯誤以及 then 之內產生的那些錯誤。要注意在最外層 catch 內拋出的異常會發生什么:它們被 存儲 在 promise 內,將來給 catch 或者 then 調用。如果沒有調用執行(如同上例中發生的),該異常就被忽視。還好 apiRollbackSomething 不會拋出任何異常。

函數 then 和 catch 總是會返回 promise(即使在鏈中沒有更多 promise)。這意味著在調用兩個函數之后,你還可以再次調用 then 或者 catch 。這就是為什么說 promise 可以是 鏈式 的原因。當所有事情執行完后,任何對 then 或者 catch 的進一步調用會立即執行傳遞給它們的回調。

值得注意的是,鏈式 Promise 通常是對的。在上例中,我們可以省略 database.update 前的 return 語句。如果數據庫操作沒有導致錯誤,那么這段代碼一樣起作用。但是,如果發生了錯誤,那么代碼就有不同的表現了:如果數據庫操作失敗,下面的 catch 塊不會被調用,因為這個 promise 不會鏈到最外層的 promise。

那么如何創建自己的 promise 呢?很簡單:

const p = new Promise((resolve, reject) => {
    try {
        const result = action(data);
        resolve(result);
    } catch(e) {
        logger.error(e.toString());
        reject(e);
    }
});

promise 也可以在promise 構造器內鏈在一起:

const p = new Promise((resolve, reject) => {
    const url = getUrl();
    resolve(
        httpLib.request(url).then(result => {
            const newUrl = parseResult(result);
            return httpLib.request(newUrl); 
        })
    );
});

這里可以看到 promise 的全部威力了:兩個 HTTP 請求被鏈進一個 promise。第一個請求的數據結果被處理,然后用來構造第二個請求。所有的錯誤都在內部被 promise 邏輯處理。

總之,promise 讓異步代碼更具可讀性,減少了犯錯的機會。它們還結束了有關 promise 如何工作的討論,因為在 ECMAScript 2015 之前,有不少有自己 API 的競爭性解決方案。

ECMAScript 2015 Generator、iterator、iterable 和 for...of

生成器(generator)是 ECMAScript 2015 的另一個重大特性。如果你是 Python 程序員,那么就會很快明白 JavaScript 生成器,因為二者是很相似的。我們來看一看:

function* counter() {
    let i = 0;
    while(true) {
        yield i++;
    }
}

如果不是 Python 開發者,那么在分析以上代碼時,腦子里面會拋出 SyntaxError 好幾次。我們來看看是咋回事。第一件看起來有點古怪的事情是 function 旁邊的星號,這是在 ECMAScript 2015 中聲明生成器的新方法。第二個就是函數內的 yield 。 yield 是一個新關鍵詞,用來指示解釋器臨時暫停生成器的執行,并返回傳遞給它的值。在本例中, yield 會返回 i 中的值。重復調用生成器會從最后一個 yield 恢復 執行,從而會保留所有狀態。

const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

如果這些東西似曾相識,可能是因為在計算機科學中有一個很熟悉的稱為 協程(coroutine) 的概念。但是協程與異常對比,有一個特殊功能:它們可以在每次調用 yield 后,從外部接受新數據。實際上,JavaScript 支持協程!所以 JavaScript 生成器實際上是協程。

function* counter() {
    let i = 0;
    while(true) {
        const reset = yield i++;
        if(reset) {
            i = 0;
        }
    }
}

const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next(true).value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

不過,所有這些此時可能看起來是多此一舉。為什么要添加生成器?它們以哪種方式可以幫助讓代碼更干凈以及防止錯誤呢?生成器被添加進來,是為了更容易給 JavaScript 帶來 迭代器(iterator) 的概念。現在,迭代器確實相當多地出現在很多項目中。那么在 ECMAScript 2015 之前,迭代器是怎么實現的呢?每個人都有自己的實現方式:

function arrayIterator(array) {
    var i = 0;
    return {
        next: function() {
            // 可能會拋出
            return array[i++];
        },
        ended: i >= array.length,
        reset: function() {
            i = 0;
        }
    }
}

var data = [0, 1, 2, 3, 4];
var iter = arrayIterator(data);
console.log(iter.next()); // 0
console.log(iter.next()); // 1
console.log(iter.next()); // 2

所以,從某種程度上說,生成器試圖為迭代器的使用帶來一種標準方法。實際上,JavaScript 中的迭代器不過就是一種協議,即,它是一種用來創建對象的被批準的 API,可以用來迭代可迭代的對象。這個協議最好是用一個示例來描述:

function arrayIterator(array) {
    var i = 0;
    return {
        next: function() {
            return i < array.length ? {
                value: array[i++],
                done: false
            } : {
                done: true
            };
        }
    }
}

特別關注一下 arrayIterator 函數返回的對象:它描述了 JavaScript 迭代器所需的協議。也就是說,迭代器是一個滿足如何條件的對象:

  • 包含一個不帶參數的 next 函數。
  • next 函數返回一個包含一個或者兩個成員的對象。如果成員 done 為真,那么就不出現其它成員。 done 標志迭代是否完成。另一個成員將是 value ,代表當前迭代值。

所以,任何遵循該協議的對象都可以被稱為 JavaScript 迭代器。這樣挺好,有官方的方法,意味著混用不同的庫不會導致出現六種不同類型的迭代器(并且必要時不得不在它們之間用適配器!)。約定和協議對于可維護性是很好的,因為這樣混合似是而非的東西的機會就會更少,而在 JavaScript 中是極其容易干出這種事情的。

所以,必須按這種方式寫迭代器,雖然簡單,但是也是比較麻煩的。如果 JavaScript 提供了一種很容易創建這些對象的方法會怎么樣?這就是生成器。生成器函數實際上就是返回迭代器。也就是說,JavaScript 生成器是以更方便的方式創建迭代器的助手。特別是生成器和 yield 關鍵字的使用,有助于更容易理解在迭代器中狀態管理的方式。例如,上面的示例可以簡化為:

function* arrayIterator(array) {
    for(let i = 0; i < array.length; ++i) {
        yield array[i];
    }
}

簡單,而且更容易讀和理解,即使對一個沒有經驗的開發者來說也是如此。代碼清晰對可維護性是至關重要的。

但是我們遺漏了生成器和迭代器謎題中的一個關鍵部分:有很多 可迭代(iterable) 的東西。特別是,集合通常是可以遍歷的。當然,集合中元素被遍歷的方式會根據有關的集合而改變,但是迭代的概念依然適用。所以 ECMAScript 2015 多提供了兩個方式來完成迭代器和生成器謎題: iterable 協議和 for..of 。

iterable 是為方便創建迭代器而提供接口的對象。也就是說,iterable 是提供如下 key 的對象:

const infiniteSequence = {
    value: 0
    [Symbol.iterator]: function* () {
        while(true) {
            yield value++; 
        }
    }
};

Symbol.iterator 和 Symbol 對象是 ECMAScript 2015 中新出現的,所以看起來很古怪。后面我們會復習 Symbol ,但是現在就把它當作是一種創建唯一標識符(Symbol)的方式,這種唯一標識符可以用來索引其它對象。這里另一個古怪的東西是字面量對象語法。我們是在一個對象字面量內使用 [Symbol.iterator] 來設置其 key。在上面我們已經復習了這種對象字面量的擴展。這里出現的示例沒有什么不同:

let obj = {
    // ...
    // key 的動態值
    [getKey()]: 'some value'
}

所以,簡而言之,iterable 是提供一個 Symbol.iterator 鍵的對象,該鍵的值是一個生成器函數。

這樣現在遍歷的對象內部就有一個新的鍵。那么,每次我們想遍歷由這些對象管理的元素時,是否需要顯式從這些對象獲取生成器嗎?答案是不需要!鑒于這是一個很常見的模式(遍歷容器管理的元素),所以 JavaScript 現在提供了一種新版本的 for 控制結構:

for(let num of infiniteSequence) {
    if(num > 1000) {
        break;
    }
    console.log(num);
}

不錯!所以可迭代的對象都可以使用新的 for..of 循環輕松遍歷。并且關于 for..of 的好消息是,已有的集合為了用它,已經被改編了。所以,數組和新的集合( Map 、 Set 、 WeakMap )都可以用這種方式:

const array = [1, 2, 3, 4];
// 本文后面會討論 Map
const map = new Map([['key1', 1], 
                     ['key2', 2], 
                     ['key3', 3]]);

for(let elem of array) {
    console.log(elem);
}

for(let [key, value] of map) {
    console.log(`${key}: ${value}`);
}

注意最后一個 for..of 循環中的古怪語法: let [key, value] 。這種語法稱為解構(desctructuring),是 ECMAScript 2015 的另一個新特性。我們會在后面討論。

一致性和簡化可以對可讀性和可維護性創造奇跡,這正是迭代器、iterable、生成器以及 for..of 循環所帶來的。

函數: 默認參數和 rest 運算符

函數現在支持默認參數,簡化了檢查一個參數是否存在,然后給它設置值的常見模式。

function request(url, method = 'GET') {
    // (...)
}

隨著參數的數目增長,默認參數簡化了在函數開頭需要檢查的流程。簡化在編碼中是沒錯的。

function request(url, method) {
    // 想像一下如果沒有 ES 2015,每個默認參數都要重復這個步驟!
    if(typeof method === 'undefined') {
        method = 'GET';
    }
}

默認參數還可以用 undefined 值。也就是說,當傳遞 undefined 給一個默認參數,那么該參數會采用其默認值替換。

function request(url, method = 'GET', data = {}, contentType = 'application/json') {
    // (...)
}

request('https://my-api.com/endpoint', undefined, { hello: 'world' });

不過,這并不是說就不要恰當的 API 設計了。在上面的示例中,用戶可能會把第三個參數傳為第二個,特別是在用 HTTP GET 時。所以,雖然默認參數可以幫助減少函數內的樣板代碼,但是還是必須注意挑選正確的參數次序及其默認值。

rest 運算符是一個新的運算符,受 C 語言啟發。我們來看看:

function manyArgs(a, b, ...args) {
    // args === ['Hello', 'World', true]
}

manyArgs(1, 2, 'Hello', 'World', true);

當然,JavaScript 確實允許通過 arguments ,訪問在函數的參數列表中沒有聲明的參數。那么為什么要用 rest 運算符呢?有兩個好的理由:

  • 為了去掉必須手動查找在參數列表中沒有命名的第一個參數的需要。這可以防止愚蠢的算錯邊界的錯誤,這個錯誤通常發生在參數被添加或者刪除到函數時。
  • 為了能把包含未聲明參數的變量當作一個真正的 JavaScript 數組。因為從一開始 argument 就一直像數組一樣,但是實際上并不是數組。相反,由 rest 運算符創建的變量是真正的數組,這帶來一致性,一致性總是好的。

因為通過 rest 運算符聲明的變量是真正的數組,所以 arguments 中出現的有些方法,比如 caller 和 callee 現在就不能用了。

擴展語法

快速理解擴展(spread)的一種方式是將它當作與 rest 運算符相反。擴展語法用來自一個數組(或者實際上是任何 iterable)的元素替換參數列表。也就是說:

function manyArgs(a, b, c, d) {
    // (...)
}

let arr = [1, 2, 3, 4];

manyArgs(...arr);

//manyArgs.apply(null, arr); //老方法,可讀性不佳

擴展語法可以用在除函數調用以外的地方。這為有趣的應用程序打開了可能性:

const firstNumbers = [1, 2, 3, 4];
const manyNumbers = [-2, -1, 0, ...firstNumbers, 5, 6, 7];

const arrayCopy = [...firstNumbers];

擴展語法從以前版本的 JavaScript 中刪除了一個煩人的限制: new 運算符不能與 apply 一起用。 apply 帶有一個函數對象為參數, new 是一個運算符。也就是說,不可能像下面這樣做:

const nums = [1, 2, 3, 4, 5];
function NumberList(a, b, c, d, e) {
    this.a = a;
    // (...)
}

//NumberList.apply(new NumberList(), nums); //沒有參數傳遞給NumberList!

現在我們可以這樣做:

const numList = new NumberList(...nums);

擴展語法簡化了很多常用模式。而簡化對可讀性和可維護性總是不錯的。

JavaScript 中的解構

解構(Destructuring)是一種 JavaScript 語法的擴展,它允許采用某種有趣的方式,將一個變量轉換成綁定到其內部的多個變量。我們在上面已經看過一個例子:

for(let [key, value] of map) {
    console.log(`${key}: ${value}`);
}

在本例中,變量 map 被綁定到一個 Map 。這種數據解構遵循 iterable 協議,為每次迭代提供值:一個鍵,及與該鍵關聯的值。這兩個值被返回在一個帶有兩個元素的數組中。鍵是第一個元素,值是第二個元素。

如果沒有解構,那么上面的代碼要像這樣寫:

for(let tuple of map) {
    console.log(`${tuple[0]}: ${tuple[1]}`);
}

這種使用與原始結構一樣的語法,將對象的內部結構映射給變量的能力,使代碼更清晰。下面我們來看另一個例子:

let [a, b, c, d, e] = [1, 2, 3, 4, 5];
console.log(a); // 1
console.log(b); // 2

這是簡單的數組解構。對象解構是什么樣呢?

const obj = {
    hello: 'world',
    arr: [1, 2, 3, 4],
    subObj: {
        a: true,
        b: null
    }
};

let { hello, arr, subObj: { b } } = obj;

console.log(hello); // world
console.log(b); // null

這就變得更有意思了。來看看這個例子:

const items = [
    {
        id: 0,
        name: 'iPhone 7'
    },
    {
        id: 1,
        name: 'Samsung Galaxy S7'
    },
    {
        id: 2,
        name: 'Google Pixel'
    }
];

for(let { name } of items) {
    console.log(name);
}

解構也可以用于函數的參數:

items.forEach(({ name }) => console.log(name));

還可以為解構了的元素挑選不同的名稱:

items.forEach(({ name: phone }) => console.log(phone));

如何不能正確解構對象,會導致變量帶有未定義值。

解構可以用默認參數組合(ECMAScript 2015 中的另一個新特性)。這簡化了某種常見的代碼模式:

function request(url, { method: 'GET', data }) {
    // (...)
}

對于默認參數和解構必須適當小心,因為 ECMAScript 2015 不允許捕獲任何在解構表達式中沒有聲明的鍵。也就是說,如果上例中作為第二個參數傳遞的對象沒有第三個鍵(假如說 contentType 鍵),就不可能訪問它(除了通過 arguments ,而這樣做很煩人,且影響可讀性)。這個疏忽會在 ECMAScript 2016 中修正。

在 ECMAScript 2015 中,數組確實有這種能力:

let arr = [1, 2, 3, 4, 5];
let [a, b, ...rest] = arr; // rest === [3, 4, 5]

數組也允許跳過條目:

let arr = [1, 2, 3, 4, 5];
let [a, , ...rest] = arr; // rest === [3, 4, 5], 數字 2 被跳過

可以說,解構是一種辦事的新方法,而不是更好的方法。我個人建議保持簡單和可讀性。當引用一個內部變量可以簡單地被寫為 let a = obj.subObj.a 時,就不要濫用解構了。解構特定用于當從不同嵌套級別的對象中 挑選 多個元素時。這種情況下,可讀性可以改進。它還可以用于函數參數和 for 循環,以減少所需輔助變量的數量。

JavaScript 模塊

模塊是ECMAScript 2015 最期待的特性之一。為了讓 JavaScript 實現大多數語言已經做到的功能:以方便、便攜和高性能的方式將代碼分離到不同的地方,必須對 JavaScript 進行擴展。模塊的出現終結了與擴展的正確方式有關的無休止的討論。

如果你是編程新手,可能很難理解為什么模塊化對于正確的開發體驗是如此必不可少的需求。可以把模塊當作是一種組織代碼的方式,這種方式下,代碼被組織到自包含的工作單元中。這些單元定義了一種與其它單元進行交互的清晰方法。這種分離提升了可維護性、可讀性,允許更多人同時開發,而不會相互干擾。保持小和簡單在設計和實現過程中也很有幫助。

因為 JavaScript 被構想為一門針對 Web 的語言,所以它已經總是與 HTML 文件關聯在一起。HTML 文件告訴瀏覽器去加載放在其它文件中的或者行內的腳本。先加載的腳本可以創建對于后來的腳本可用的全局對象。一直到 ECMAScript 2015,這都是來自不同 JavaScript 文件的代碼可以相互通訊的唯一基本方式。這導致了大量不同的處理該問題的方法。模塊打包器出于這種情況帶來一些理智而應運而生。其它環境(比如 Node.js)的 JavaScript 解釋器采納了像 Common.js 這樣的解決方案。其它的規范,比如異步模塊定義(AMD),也出現了。社區中缺乏共識也逼得 ECMAScript 工作組去考慮一下現狀。其結果就是 ECMAScript 2015 模塊的出現。

 

// helloworld.js

export function hello() {
    console.log('hello');
}
export function world() {
    console.log('world');
}

export default hello;

console.log('Module helloworld.js');
// main.js

import { hello, world } from 'helloworld.js';

hello();
world();

ECMAScript 2015 給該語言添加了一堆關鍵字: import 和 export 。 import 關鍵字讓你可以把來自其它模塊的元素帶到當前模塊。這些模塊在導入期間可以重新命名,或者可以被批量導入。 export 關鍵字做相反的事情:它將來自當前模塊的元素標記為可用于導入。從其它模塊導入的元素可以被再導出。

// hello 和 world 都可以用
import * from 'helloworld.js';

// HelloWorld 是一個包含 hello 和 world 的對象.
import * as HelloWorld from 'helloworld.js';

// 在這個模塊中,helloFn 就是 hello,worldFn 就是 world.
import { hello as helloFn, world as worldFn } from 'helloworld.js';

// h 是 helloworld.js 中名為 Hello 的默認輸出
import h from 'helloworld.js'; 

// 沒有導入元素,但是 helloworld.js 模塊的副作用運行了
// (模塊中的 console.log 語句就是個副作用)。
import 'helloworld.js';

ECMAScript 2015 模塊的一個有意思的特征是, import 的語義既允許異步加載模塊,又允許同步加載模塊。也就是說,解釋器可以自由選擇更合適的方式。而這在 Common.js(同步)和 AMD 模塊中(異步)是大相徑庭的。

為什么瀏覽器花了這么久來實現模塊?

如果根據上面所描述的原因,模塊是這么重要,那么為什么現在還不能用它們呢?到 2016 年 11 月為止,大多數主流瀏覽器都原生實現了 ECMAScript 的大部分特性,但是模塊依然是漏掉了。這到底是怎么回事呢?

雖然 ECMAScript 2015 確實在語法上定義了模塊,但是規范沒有提到在 Web 上到底該如何實現。也就是說,一個符合標準的實現只需要解析包含 import 和 export 語句的 JavaScript 文件,不需要實際用這做什么事情!這可能看起來像是一個大的疏忽,但是并非如此。正如本節開頭提到的,JavaScript 總是與 Web 中的 HTML 攪在一起。不過,ECMAScript 2015 規范只關注 JavaScript 本身,它與 HTML 以及 JavaScript 如何被訪問無關。也就是說,雖然一條 import 語句說清楚了讓解釋器應該試圖去加載指定名稱的文件,但是它卻完全沒說如何去得到這個文件。在 web 上,這意味著向服務器發送一條向指定 URL 的請求。而且,ECMAScript 對 HTML 和 JavaScript 的關系只字不提。

這個問題預計會被 JavaScript 加載器標準 解決,該標準試圖為瀏覽器提出一個加載器規范以及類似于獨立的解釋器。HTML 預計也會添加所需語法,用來將 JavaScript 模塊與其它常見的腳本區分(為此提議的一個語法是 <script type="module" src="file.js"> )。

import 和 export 的靜態性質

import 和 export 都是靜態性質的。也就是說,使用這兩個關鍵字的效果必須是在腳本執行前完全可計算的。這就為靜態分析器和模塊打包器做點手腳打開了可能性。像 Webpack 這種模塊打包器可以在打包時創建一個完整而明確的依賴樹。也就是說,刪除不需要的依賴以及其它優化是可能的,而且規范是完全支持的。這與 Common.js 和 AMD 是有很大區別的。

但是靜態模塊確實也去掉一些在某些條件下很方便的靈活性。不幸的是,動態加載器提案沒有把它加到 ECMAScript 2015 中。預計會在將來版本中會被加進去。已經有一個提案以 System.import 的形式存在。

我們現在可以用模塊嗎?

是的,而且你應該用!雖然模塊加載在瀏覽器中還沒有實現,但是像 Babel、Webpack 和 System.js 這種打包器、編譯器和庫已經實現了 ECMAScript 2015 模塊。提早采用模塊的好處是,模塊已經是規范的一部分了!你知道不管怎樣,模塊是一成不變的,在將來的 JavaScript 版本中不會看到大的變動。現在還用 Common.js 或者 AMD 意味著退步,采用將來會逐漸淡出的解決方案。

新的 JavaScript 集合

雖然 JavaScript 有必要的能力實現很多數據結構,但是有些數據結構最好是通過優化來實現,而這些優化只能靠解釋器來實現。ECMAScript 2015 工作組決定解決此問題,并提出了 Set 、 Map 、 WeakSet 和 WeakMap 。

Set 存儲唯一的對象。對象要出現在集合中必須通過測試。 Set 用特殊的比較語法(與 === 很相似)來檢查對象的相等性。

Map 對 Set 進行了擴展,讓任意值與唯一鍵關聯在一起。也就是說, Map 允許使用任意唯一的鍵,這與普通 JavaScript 對象形成了對比(普通 JavaScript 對象只允許用字符串作為鍵)。

WeakSet 表現的像 Set ,但是它不會取得存儲在它其中的對象的所有權。也就是說, WeakSet 內的對象在集合外的對象不再引用它們之后,就變成了無效的了。 WeakSet 值只允許對象存儲在其內,不允許原始值。

WeakMap 是在鍵上弱(像 WeakSet ),在它存儲的值上強。

JavaScript 一直在數據結構部分比較弱。Set 和 Map 是用得最多的數據結構之一,所以將它們集成在語言中可以帶來不少好處:

  • 減少在外部庫上的依賴數量
  • 更少的測試代碼(如果 map 或者 set 被語言實現了,就只需要測試其功能了)
  • 為最常見的需求之一提供一致性的 API

不幸的是,基于 hash 的 map 依然不可用。

對象代理

對象代理(proxy)是 ECMAScript 2016 的另一個重大補充。對象代理可以讓我們以有趣的方式自定義對象的行為。JavaScript 作為一門動態語言,在修改對象時是非常靈活的。不過,某些修改通過使用代理會更好表達。例如,下面我們來看看如何修改一個對象的所有屬性的 get 操作,如果屬性是一個數字,就加一。我們先用 ECMAScript 5 來解決這個問題。

var obj = {
    a: 1,
    b: 2,
    c: 'hello',
    d: 3
};

var obj2 = Object.create(obj);

Object.keys(obj).forEach(function(k) {
    if(obj[k] instanceof Number || typeof obj[k] === 'number') {
        Object.defineProperty(obj2, k, {
            get: function() {
                return obj[k] + 1;
            },
            set: function(v) {
                obj[k] = v;
            }
        });
    }
});

console.log(obj2.a); // 2
console.log(obj.a);  // 1
obj2.a = 4;
console.log(obj.a);  // 4
console.log(obj2.a); // 5

這里我們利用 JavaScript 的原型鏈機制,來重寫( 譯者注:shadow,意思是為隱藏、遮蔽、重新 )對象中的變量。重寫的對象有一個自定義的 setter 和 getter,可以從原型對象訪問該對象的變量。這樣做可以起作用,但是按這樣做有點難。下面我們看看 ECMAScript 2015 是如何改進的。

let obj = {
    a: 1,
    b: 2,
    c: 'hello',
    d: 3
};

let obj2 = new Proxy(obj, {
    get: function(object, property) {
        const value = object[property];
        if(value instanceof Number || typeof value === 'number') {
            return value + 1;
        } else {
            return value;
        }
    }
});

console.log(obj2.a); // 2
console.log(obj.a);  // 1
obj2.a = 4;
console.log(obj.a);  // 4
console.log(obj2.a); // 5

這樣更干凈一些:沒有過多的鍵迭代,不需要顯式重寫 setter,不需要玩弄原型鏈。并且我們已經說過:更干凈的代碼就是更好的代碼。

代理的另一個額外好處是,它們可以重寫原本很難(或者不可能)重寫的操作。例如,代理可以修改構造器的行為:

let Obj = new Proxy(function () { return { a: 1 } }, {
    construct: function(target, args, newTarget) {
        target.extension = 'This is an extension!';
        return target;
    }
});

const o = new Obj;
console.log(o.extension); // 'This is an extension';

反射

代理是對 JavaScript 動態能力的一個不錯的補充,而對代理的補充就是反射(reflection)。對于每個可以被代理捕獲和重寫的操作, Reflect 對象允許用同樣一致的 API 訪問該操作。也就是說,如果代理提供了一個重寫訪問屬性的 get 操作,那么 Reflect 也提供一個訪問一個屬性的 get 操作。

let obj = {
    a: 1,
    b: 2,
    c: 'hello',
    d: 3
};

// 等于 obj['a'];
console.log(Reflect.get(obj, 'a')); // 1

function SomeConstructor() {
    return { a: 1 };
}

// 等于 new SomeConstructor
const newObj = Reflect.construct(SomeConstructor, []);
console.log(newObj.a); // 1

// 等于 newObj 中的 'a'
console.log(Reflect.has(newObj, 'a'));

反射 API 的目標是給過去以其它方式執行的某種操作帶來一致性。這些函數的作用可能沒有代理 API 那么重要,但是依然是一個很受歡迎的補充。

Symbol

Symbol 是 JavaScript 中新增加的基礎數據類型。與已有的數據類型相比,Symbol 沒有真實值。其優點在于唯一性。所有 Symbol 都是唯一的,并且是不可修改的。Symbol 主要用作對象的鍵。Symbol 對象鍵不同于字符串鍵,不能被 Object.keys 枚舉,也不能被 JSON.stringify 看到。

Symbol 的主要應用是用來創建對象內的特殊鍵。ECMAScript 2015 用 Symbol 機制定義某種很特定的鍵。例如,可迭代的對象可以用 Symbol.iterator 來定義它們的迭代器:

const obj = {
    [Symbol.iterator]: function* () {
        yield 1;
        yield 2;
    }
}

for(const v of obj) {
    console.log(v);
}

Symbol 可以幫助我們阻止污染帶有不透明鍵的對象。它可以帶有輔助消息來讓調試更輕松。不過,帶有相同消息的兩個 Symbol 依然是可以區分的。

通過讓特殊對象鍵的命名空間與一般鍵分開,ECMAScript 2015 就讓調試變得更容易,對象序列化變得跟簡單,減少了遇到由鍵名沖突導致的 bug 的機會。

類型化數組

JavaScript 的缺陷之一就是缺乏合適的數字數據類型。大部分時間,是可能以某種方法規避掉這種限制的。不過,大量數值的高效存儲是不能實現的。這可以用類型化數組來解決。

const arr = new Uint8Array(1024);
arr[8] = 255;

類型化數組提供了對有符號和無符號 8 位、16 位 和 32 位整數的高效存儲。它還有 32 位和 64 位浮點值的版本。

次要特性

內置子類化

作為添加到 ECMAScript 2015 類中的有爭議的特性,大多數內置對象現在都可以被子類化。

class SpecialArray extends Array {
    constructor(...args) {
        super(...args);
    }

    get lengthWithTerminator() {
        return this.length + 1;
    }
}

const arr = new SpecialArray(1, 2, 3);
console.log(arr.lengthWithTerminator);

子類化應該是優于操作內置對象的原型,而代理應該優先于這兩個選項。最終還是得由你根據自己的使用案例挑選最佳的選項。一般來說,通過組合或者代理來表達行為重用,比通過子類化更好,所以要謹慎使用子類化這個特性。

有保證的尾調用優化

很多函數式編程語言都會執行尾調用優化。尾調用優化負責將遞歸函數調用轉換為循環。這種轉換避免了棧溢出。JavaScript 帶來了很多函數式特性,但是唯一漏掉了尾調用優化,直到 ECMAScript 2015 才添上。有些算法用遞歸比用循環更好表示:

function factorial(n) {
    "use strict";
    function helper(n, result) {
        return n <= 1 ? result : helper(n - 1, result * n);
    }
    return helper(n, 1);
}

尾調用優化需要函數出現在尾調用位置,即,造成下一次調用遞歸函數的分支,必須是該分支的最后一次調用,沒有還沒有處理的操作。這就是上面的示例比下面所示的直接實現要復雜一點的原因。

function factorial(n) {
    return n <= 0 ? 1 : n * factorial(n - 1);
}

在這個示例中,在分支之一中的最后一個操作是 n 被遞歸函數相乘。也就是說,遞歸函數不處于尾位置,于是尾調用優化不能執行。

有些語言的實現會很巧妙地將最后這個示例轉換為前一個,從而啟用尾調用優化。這對 ECMAScript 2015 的實現不是必需的,也不是它所期待的,所以不應該依靠它。

尾調用優化是對 JavaScript 工具箱很有意義的補充。不過,應該只在它提高了代碼條理性時才用它。

UNICODE

雖然 JavaScript 在 ECMAScript 2015 之前就支持 Unicode,但是現在有一些有意思的補充。新的 Unicode 轉義符是其中最突出的:

const str = '\u{10437}'; // ??
str.codePointAt(0) === 0x10437;

在 ECMAScript 2015 之前,要像上面一樣指定一個字符,而不把它按字面放在源代碼中,那就必須放上顯式的替代對:

const str = '\uD801\uDC37'; // ??

正則表達式現在支持在模式中通過從 u 標志嵌入碼點:

'\u{10437}'.match(/./u)[0].length == 2; //替代對

新的數字字面量

現在可以用二進制和八進制字面量:

0b10100001 === 0xA1 === 0o241 === 161;

下一版本 ECMAScript 中會出現什么

下一版本的 ECMAScript 可能不會與 ECMAScript 2015 一樣大,不過預期會有有趣的補充。我們來看看主要的一些。 The next versions of ECMAScript will probably not be as big as ECMAScript 2015, however interesting additions are expected. Let's see some of the major ones.

Async/Await

我們已經看到 ECMAScript 2015 通過 promise 的方式改進了異步編程。不過,對于其所有優點來說,promise 也帶來了相當大的句法負擔。有沒有什么辦法可以改進呢?幸運的是,有!這就是 ECMAScript 2017 中 async/await 試圖要做的事情。

這里是一個來自上面 promise 小節的示例:

const updateStatement = '...'; 

function apiDoSomething(withThis) {
    const url = 'https://some.cool.backend.com/api/justDoIt';
    httpLib.request(url, withThis).then(result => {
        // database.update 也返回一個 promise
        return database.update(updateStatement, parseResult(result));
    }).catch(error => {
        logger.error(`ERROR: ${error} (from url: ${url})`);
        // Our API is such that rollbacks are considered no-ops in case 
        // the original request did not succeed, so it is OK to call it here.
        apiRollbackSomething(withThis);
    });
}

如下是 ECMAScript 2017 中它會是什么樣子:

const updateStatement = '...'; 

async function apiDoSomething(withThis) {
    const url = 'https://some.cool.backend.com/api/justDoIt';
    try {
        const result = await httpLib.request(url, withThis);
        return database.update(updateStatement, parseResult(result));
    } catch(e) {
        logger.error(`ERROR: ${e} (from url: ${url})`);
        // Our API is such that rollbacks are considered no-ops in case 
        // the original request did not succeed, so it is OK to call it here.
        apiRollbackSomething(withThis);
    }
}

這看起來好像也沒有多大改進,所以下面我們來看一個更復雜的例子。

function apiDoSomethingMoreComplex(withThis) {
    const urlA = '...';
    const urlB = '...';

    httpLib.request(urlA, withThis).then(result => {
        const parsed = parseResult(result);
        return new Promise((resolve, reject) => {
            database.update(updateStatement, parsed).then(() => {
                resolve(parsed);
            }, error => {
                reject(error);
            });
        });
    }).then(result => {
        return httpLib.request(urlB, result);
    }).then(result => {
        return worker.processData(result);
    }).then(result => {
        logger.info(`apiDoSomethingMoreComplex success (${result})`);
    }, error => {
        logger.error(error);
    });
}

在這個示例中,有一個異步操作鏈,該鏈依賴于前一個操作的結果。而且,傳遞給下一個操作的結果不一定是前一個操作的結果,所以需要用到 resolve 和 reject 。必須注意的是,盡管這段代碼要照著做有點難,但是比沒有 promise 時需要做的事情好得多。下面我們看看 async/await 如何對此做更多改進:

async function apiDoSomethingMoreComplex(withThis) {
    const urlA = '...';
    const urlB = '...';

    try { 
        let result = await httpLib.request(urlA, withThis);
        const parsed = parseResult(result);
        await database.update(updateStatement, parsed);
        result = await httpLib.request(urlB, parsed);
        result = await worker.processData(result);
        logger.info(`apiDoSomethingMoreComplex success (${result})`);
    } catch(e) {
        logger.error(e);
    }
}

改進是顯著的。可讀性更好了,有人甚至會把它當作同步代碼。 async / await 通過充當語法糖,來隱藏背后工作的 promise 的行為,讓異步函數看起來像同步函數。是的,這是對的, async/awiat 只不過是 promise 的語法糖!所以,只是是能用 prmoise 的代碼現在都已經支持 async/await !要搞清楚 async/await 如何與 promise 關聯,我們將讓這種行為更明確點。

  • await 只能被用在 async 函數內。
  • 一個 async 函數返回一個 promise。從該函數返回的值就是該 promise 的結果。如果函數拋出異常,那么該 promise 就被拒絕。該函數可能返回一個 promise,就會得到一個鏈式的 promise。也就是說,async 函數總是將其結果封裝在一個 promise 中。
  • await 導致當前 async 函數在一個 promise 上等待。當 promise 被成功解決時,來自該 promise 的結果被打開,成為 await 表達式的結果,等著被使用。如果 promise 失敗了,那么帶有該 promise 的被拒絕值的異常就被拋出。如果該異常被捕獲,async 函數可能照樣繼續下去。否則就會被拒絕。

從上面可能看不出來 async 函數是如何與普通函數一起用。下面我們來看看:

function normalFunction() {
    const data = getData();
    // 是的,async 函數只不過就是 promise.
    apiDoSomethingMoreComplex(data).then(result => {
        console.log(`Success! ${result}`);
    }, error => {
        console.log(`Error: ${error}`);
    });
}

一個 async 函數只是一個 promise。在其它 async 函數外,你需要按遵循 Promise API。就是這樣! Async/await 具有 promise 的威力,讓 promise 可讀性更佳。而可讀性總是更好:對代碼更好,對你和將來的程序員更好。

單指令多數據(SIMD)

隨著 JavaScript 被用于越來越多的用途,如何訪問某種硬件操作可以讓事情更有效率就變得很明顯了。單指令多數據(SIMD)指令是同時作用于數據的多個元素上的一連串硬件操作。某些操作如果能訪問這些指令的話,就可以大大加快。它們特別用在圖像、聲頻和加密操作,這些領域已經開始用 JavaScript 了。

const a = SIMD.Float32x4(1, 2, 3, 4);
const b = SIMD.Float32x4(5, 6, 7, 8);
const c = SIMD.Float32x4.add(a, b); // [6,8,10,12]

這個示例來自于 MDN SIMD 頁 ,展示四個浮點值可以用一條操作相加。雖然從 API 看起來不明顯,不過如果一個硬件操作可以用來執行該加法,而且它所需的指令小于四條獨立的加法,它就會被使用。

SIMD 操作為 JavaScript 打開了更多可能性的大門。

異步迭代

異步迭代采用了來自 ECMAScript 2015 和 2017 的三個重大特性,并將其混在一起:iterator、generator 和 async/await。這是一個較早的提案,所以語法還不是一成不變的。它可能看起來像如下這樣:

for await (const line of readLines(filePath)) {
  console.log(line);
}

readLines 是一個在每次迭代中返回一個 promise 的生成器函數。通過 await 關鍵字來擴展 for 的語法,讓它可以處理 promise, By extending the syntax of for to handle promises through the await keyword, uses like the above become possible. It is important to note that the restriction of using await inside async functions remains in place. Here's what an async generator like readLines could look like:

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

悄悄話: 用 ECMAScript 2015 + Async/Await 實現 Auth0 Lock

現在你就可以用 ECMAScript 2015 和 aysnc/await!我們會看到如何用 webpack + Babel 來做這事情。對于本例,我們將改編 Auth0 VanillaJS Lock 示例之一來使用 ECMAScript 2015 和 async/await。

首先,獲取代碼,并注冊一個免費 Auth0 帳號):

$ git clone git@github.com:auth0-samples/auth0-javascript-spa.git

進入 01-Login 目錄,初始化一個新 NPM 項目。

npm init

下面安裝所有開發依賴:

npm install --save-dev http-server webpack babel-loader babel-core babel-preset-es2015 babel-plugin-transform-runtime babel-preset-stage-3 bluebird

簡單 Webpack + Babel 設置,需要兩個簡單的配置文件:

// webpack.config.js
module.exports = {
    entry: "./app.js",
    output: {
        path: __dirname,
        filename: "app.bundle.js"
    },
    module: {
        loaders: [{
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'babel',
            query: {
                presets: ['es2015', 'stage-3']
            }
        }]
    }
};
// .babelrc
{
  "plugins": ["transform-runtime"]
}

現在編輯 HTML 文件,引入新編譯過的打包文件:

<script src="app.bundle.js"></script>

現在,修改 auth0-variables.js 文件,來使用 ECMAScript 2015 的新 export 關鍵字:

export const AUTH0_CLIENT_ID='E799daQPbejDsFx57FecbKLjAvkmjEvo';
export const AUTH0_DOMAIN='speyrott.auth0.com';
export const AUTH0_CALLBACK_URL=location.href;

現在到了主要部分,我們將重構 app.js 文件,讓它使用來自 ECMAScript 2015 的一些特性以及 aysnc/await。但是首先,我們得用 Bluebird 來將 Auth0 Lock 的老式 Node.js 回調轉換為 promise。

import {
  AUTH0_CLIENT_ID,
  AUTH0_DOMAIN,
  AUTH0_CALLBACK_URL
} from './auth0-variables.js';

import Promise from 'bluebird';

var lock = new Auth0Lock(AUTH0_CLIENT_ID, AUTH0_DOMAIN);
const getProfile = Promise.promisify(lock.getProfile, { context: lock });

下面我們來使用 promise 和 async/await:

async function retrieveProfile() {
  var idToken = localStorage.getItem('id_token');
  if (idToken) {
    try {
      const profile = await getProfile(idToken);
      showProfileInfo(profile);
    } catch(err) {
      alert('There was an error getting the profile: ' + err.message);
    }
  }
}

async function afterLoad() {
  // 按鈕
  var btnLogin = document.getElementById('btn-login');
  var btnLogout = document.getElementById('btn-logout');

  btnLogin.addEventListener('click', function () {
    lock.show();
  });

  btnLogout.addEventListener('click', function () {
    logout();
  });

  lock.on("authenticated", function(authResult) {
    getProfile(authResult.idToken).then(profile => {
      localStorage.setItem('id_token', authResult.idToken);
      showProfileInfo(profile); 
    }, error => {
      // 處理錯誤
    });
  });

  return retrieveProfile();
}

window.addEventListener('load', function () {
  afterLoad().then();
});

getProfile 函數是一個 promise。你既可以像這樣用它,也可以在 async 函數內 await 它的結果。

總結

ECMAScript 2015 是對 JavaScript 的一次重大更新。很多討論多年的改進現在都可以用了。這些特性讓 JavaScript 變得更適合于大型開發。某些常用模式被簡化了,代碼更清晰了,表現力增加了。雖然 ECMAScript 2015 對于舊瀏覽器或者環境的支持是個問題,但是 Babel 和 Traceur 這類轉譯器讓你現在就可以獲利。既然現在大多數 JavaScript 項目都利用了打包器,那么轉譯器的使用就簡單和方便了。沒有理由不用 ECMAScript 2015,沒有理由不現在就用它獲利。

 

來自:http://www.zcfy.cc/article/a-rundown-of-javascript-2015-features-1915.html

 

 本文由用戶 LynMcGahan 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!