[翻譯] 理解 ECMASCript 6 第一章: 基礎知識

zzNucker發表於2014-04-12

翻譯自 https://github.com/nzakas/understandinges6 by Nicholas C. Zakas

基礎知識

ECMAScript 6在ECMAScript 5標準的基礎上做出了很多變化。其中有些變化是巨大的,比如新增了新型別或語法,而其他的一些則相當小,在原有的語言之上做出了不斷的改進。本章主要介紹這些增量改進,這些改進可能並不會得到很多關注,但所提供的一些重要的功能可能會使某些型別的問題變得更容易解決一些。

更好地支援Unicode

在ECMAScript 6之前,JavaScript的字串實現完全基於16位字元編碼的想法。所有的字串屬性和方法,如lengthcharAt(),都是根據“每16位序列表示一個字元的”的想法來實現的。ECMAScript 5標準允許JavaScript引擎來決定使用哪兩種編碼,是UCS-2還是UTF-16(兩個編碼都採用16位為編碼單位,使得所有可以被觀察到的操作都有相同的結果)。雖然在曾經某個時間內,世界上所有的字元都能夠使用16位來表示,但是現在,這個情況已經改變了。

繼續使用16位的話將不可能達到Unicode的“為世界上每一個字元建立一個唯一的標示符”的目標。這些被稱為碼點的全域性唯一識別符號僅僅是從0開始的數字(你可能會認為這些是字元編碼,但其實是有細微的差別的)。一種字元編碼負責將一個碼點轉換成內部一致的編碼單元。而UCS-2對於碼點到編碼單元的對映是一對一對映,UTF-16則有更多可能性。

在UTF-16裡,最開始的2^16個碼點表現為一個16位的編碼單元。這就是所謂的基本多文種平面(BMP)。超出該範圍內的一切被認為是處在一個補充平面中,在這裡,碼點不再能夠用僅僅16位來表示。UTF-16通過引進代理編碼對解決了這個問題,在這裡,一個單一的碼點是由兩個16位編碼單元來表示的。這意味著在一個字串中的任何單個字元可以是一個編碼單元(支援BMP,共16位)或者兩個(輔助平面的字元,總共有32位)。

ECMAScript 5將所有的操作都保持在16位編碼單元中,這意味著你也許會得到意想不到的結果,如果你操作的字串包含代理編碼對的話。例如:

var text = "?";

console.log(text.length); // 2
console.log(/^.$./.test(text));  //false
console.log(text.charAt(0)) // '"'
console.log(text.charAt(1)) // '"'
console.log(text.charCodeAt(0)) // 55362
console.log(text.charCodeAt(1)) // 57271

在這個例子中,一個單一的Unicode字元是使用代理編碼對來表示的,也就是說,JavaScript的字串操作把字串作為兩個16位字元來操作。這意味著 字串的length 為2,正規表示式試圖匹配單個字元失敗,並且charAt()無法返回一個有效的字串。charCodeAt()方法對每個編碼單元返回對應的16位數字編碼,這已經是你可以在ECMAScript 5中得到的最接近真實值的東西了。

ECMAScript 6強制使用UTF-16字串的編碼。字元編碼的標準化意味著該語言現在可以支援那些被設計用來對付代理編碼對的方法了。

codePointAt() 方法

完全支援UTF-16的第一個例子是codePointAt()方法,它可以用於獲取對映到一個給定字元的Unicode碼點。這個方法接受一個字元位置(而不是編碼單元的位置),並返回一個整數值:

var text = "?a";

console.log(text.codePointAt(0));   // 134071
console.log(text.codePointAt(1));   // 97

返回的值是Unicode碼點的值。對於BMP字元來說,這個結果將和使用 charCodeAt() 的結果是一致的,因此 "a"將會返回97。這種方法是確定一個給定的字元是由一個還是兩個碼點來表示的最簡單的方法:

function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
}

console.log(is32Bit("?"));         // true
console.log(is32Bit("a"));          // false

16位字元表示的上界為十六進位制的'FFFF',所以任何在此數字之上的碼點都必須由兩個編碼單元來表示。

String.fromCodePoint()

當ECMAScript中提供了一種方法來做一些事情,一般它也會提供一種方法做相反的事。您可以使用codePointAt()來在一個字串中獲取字元的碼點,而`String.fromCodePoint()' 則會通過一個指定碼點來產生一個單字元字串。例如:

console.log(String.fromCodePoint(134071));  // "?"

你可以把String.fromCodePoint()想成String.fromCharCode()的一個更完整版本。對於在BMP中的所有字元,每種方法都有相同的結果。唯一的區別是對於在該範圍之外的字元。

編碼 Non-BMP 字元

ECMAScript 5允許字串包含由轉義序列來表示的16位Unicode字元。轉義序列是\U後面跟著四個十六進位制值。例如,轉義序列'\u0061'表示字母"a":

console.log("\u0061");      // "a"

如果您嘗試使用超過'FFFF',即BMP的上界,的轉義序列,那麼你就可以得到一些令人吃驚的結果:

console.log("\u20BB7");     // "₻7"

由於Unicode轉義序列被定義為總是嚴格的四個十六進位制字元,所以ECMAScript將\u20BB7作為兩個字元看待:\u20BB和'"7"'。第一個字元是不可列印的,第二個是數字7。

ECMAScript 6通過引入一個擴充套件的Unicode轉義序列來解決這個問題,在這種序列中十六進位制數字被包含在大括號中。這允許最多8個十六進位制字元來指定單個字元:

console.log("\u{20BB7}");     // "?"   

使用擴充套件轉義序列,正確的字元會被包含在字串中。

請確保您只在一個支援ECMAScript 6的環境中使用這個新的轉義序列。在所有其他環境中,這樣做會導致一個語法錯誤。你可能需要檢查環境是否支援擴充套件轉義序列功能,可以使用以下函式來檢測:

function supportsExtendedEscape() {
 try {
     "\u{00FF1}";
     return true;
 } catch (ex) {
     return false;
 }
}

normalize() 方法

Unicode的另一個有趣的方面是,不同的字元在進行排序或者或其他基於比較的操作時,有可能被視為相同的。有兩種方法來定義這些關係。首先,規範等價表示兩個碼點的序列在各方面都被認為是通用的。這甚至意味著兩個字元的組合,可以標準地等同於一個字元。第二關係是相容性,這意味兩個碼點的序列具有不同的外觀,但在某些情況下可以互相通用。

需要了解的一件重要的事情是,由於這些關係,可能會存在兩個字串,它們從根本上來說表示的是同一文字,卻含有不同的碼點序列。例如, 字元 "æ" 和字元 "ae" 也許能夠互相通用,雖然它們是不同的碼點。因此,這兩個字串在JavaScript中就是不相等的,除非它們被以某種方式進行標準化。

ECMAScript 6中通過一個新的normalize()方法來支援以下四種對字串的Unicode標準化形式。該方法選擇性地接受一個引數,"NFC"(預設值),"NFD""NFKC""NFKD"。解釋這四種形式之間的差異超出了本書的範圍。請記住,為了正常使用,你必須將兩個字串都以同樣的形式進行標準化。例如:

var normalized = values.map(text => text.normalize());
normalized.sort(function(first, second) {
    if (first < second) {
        return -1;
    } else if (first === second) {
        return 0;
    } else {
        return 1;
    }
});

在這段程式碼中,在一個values 陣列中的字串被轉換成一個標準化的形式以使該陣列可以被適當地排序。你可以通過在原始陣列上呼叫normalize()作為比較器的一部分來完成排序:

values.sort(function(first, second) {
    var firstNormalized = first.normalize(),
        secondNormalized = second.normalize();

    if (firstNormalized < secondNormalized) {
        return -1;
    } else if (firstNormalized === secondNormalized) {
        return 0;
    } else {
        return 1;
    }
});

再次,要記住最重要的一點是,這兩個值都必須以相同的方式進行標準化。這些例子都使用預設值NFC,但你可以很容易地指定它們其中的另一個:

values.sort(function(first, second) {
    var firstNormalized = first.normalize("NFD"),
        secondNormalized = second.normalize("NFD");

    if (firstNormalized < secondNormalized) {
        return -1;
    } else if (firstNormalized === secondNormalized) {
        return 0;
    } else {
        return 1;
    }
});

如果你在此前從來沒有擔心過Unicode的標準化,那麼這個方法可能對你來說用處不大。然而,知道它是可用的將會幫助你在一個國際化的應用程式中工作得更好。

正規表示式的 u 標誌

許多常見的字串操作是通過使用正規表示式來完成的。然而,正如前面所提到的,正規表示式的工作也建立在16位的編碼單元,每個單元代表一個字元的基礎上。這就是為什麼在前面的示例中,單字元匹配沒有像我們所預想的一樣工作。為了解決這個問題,ECMAScript 6定義了正規表示式中的一個新的標誌 u 來代表 Unicode

當一個正規表示式設定了標誌u時,它會把工作模式從編碼單元切換到字元。這意味著正規表示式再也不會對字串中的代理編碼感到困惑,它將像預期一樣正常地工作。例如:

var text = "ð ®·";

console.log(text.length);           // 2
console.log(/^.$/.test(text));      // false
console.log(/^.$/u.test(text));     // true

新增u標誌允許正規表示式按照字元來進行正常的字串匹配。不幸的是,ECMAScript 6還沒有一種方法來確定一個字串中含有多少編碼點的方式,但是幸運的是,正規表示式可以做到:

function codePointLength(text) {
    var result = text.match(/[\s\S]/gu);
    return result ? result.length : 0;
}

console.log(codePointLength("abc"));    // 3
console.log(codePointLength("ð ®·bc"));   // 3

在這個例子中,正規表示式匹配空格和非空白字元,這適用於通用的所有Unicode字串。在至少有一個結果匹配時,result會包含一個匹配結果的陣列,因此陣列的長度就是這個字串中的編碼點的數量。

雖然這種方法有效,但它不是很快,尤其是當你應用到長字串時,因此請儘可能減少編碼點的計數操作。希望未來的ECMAScript 7將帶來一種更高效能的計算方法。

更多的String方法

JavaScript的字串在類似的功能上一直落後於其它語言。比如,直到ECMAScript 5中字串才終於獲得了trim()方法,而ECMAScript 6將會繼續擴充套件字串的新功能。

contains(), startsWith(), endsWith()

自從有了JavaScript以來,開發人員一直使用indexOf()來確定某個字串是否被包含在另一個字串中。在ECMAScript 6中,新增加了三個新的方法,用以判斷一個字串是否包含其它的字串。

  • contains() - 如果給定的文字在字串中任意地方被發現,則會返回true。 否則會返回false。
  • startsWith() - 如果給定的文字在字串的開始處被發現,則返回true。否則返回false。
  • endsWith() - 如果給定的文字在字串的結尾處被發現,則返回true。否則返回false。

所有這些方法會接受兩個引數:需要搜尋的文字,(可選的)從字串中開始搜尋的位置。如果省略了第二個引數,contains()startsWith()將從字串的開頭開始搜尋,而endsWith()則從結尾搜尋。實際上,第二個引數會減少被搜尋的字串部分。下面是一些例子:

var msg = "Hello world!";

console.log(msg.startsWith("Hello"));       // true
console.log(msg.endsWith("!"));             // true
console.log(msg.contains("o"));             // true

console.log(msg.startsWith("o"));           // false
console.log(msg.endsWith("world!"));        // true
console.log(msg.contains("x"));             // false

console.log(msg.startsWith("o", 4));        // true
console.log(msg.endsWith("o", 8));          // true
console.log(msg.contains("o", 8));          // false

這三種方法使我們能夠更容易判斷字元子串,而無需擔心它們精確位置的識別。

所有的這些方法都會返回一個布林值,如果你需要獲得一個字串在另一個字串中的位置,請使用indexOf()lastIndexOf()方法。

repeat()

ECMAScript 6還為字串增加了一個repeat()方法,這個方法接受一個引數,為重複該字串的次數,並返回原始字串重複了指定次數後的一個新的字串。例如:

console.log("x".repeat(3));         // "xxx"
console.log("hello".repeat(2));     // "hellohello"
console.log("abc".repeat(4));       // "abcabcabcabc"

無論怎樣,這確實是一個非常方便的函式,尤其是在文字處理中。看其中一個例子,這裡我們需要為程式碼格式化工具建立給定的縮排級數:

// indent using a specified number of spaces
var indent = " ".repeat(size),
    indentLevel = 0;

// whenever you increase the indent
var newIndent = indent.repeat(++indentLevel);

Object.is()

當你想比較兩個值時,你可能習慣於使用或者等號運算子(==)或恆等於操作符(===) 。許多人喜歡使用後者,以避免在比較期間進行強制型別轉換。然而,即使是恆等於操作符也不是完全準確。例如,值+0和-0在===操作符下被認為是相等的。即使它們在不同的JavaScript引擎下會表現得不一樣。同樣的,NaN === NaN 會返回 false,這迫使我們使用isNaN()來正確地檢測NaN

ECMAScript 6引入了Object.is() 來彌補恆等於操作符所遺留的一些詭異之處。這個方法接受兩個引數,並在兩個值是相等的時返回true。兩個值只有在它們擁有同樣的值並且同樣的型別時,才會被認為是相等的。在在許多情況下,Object.is() 的工作方式與===相同。唯一的區別是+0和-0會被認為是不等價的,而且NaN會被認為等同於NaN。下面是一些例子:

console.log(+0 == -0);              // true
console.log(+0 === -0);             // true
console.log(Object.is(+0, -0));     // false

console.log(NaN == NaN);            // false
console.log(NaN === NaN);           // false
console.log(Object.is(NaN, NaN));   // true

console.log(5 == 5);                // true
console.log(5 == "5");              // true
console.log(5 === 5);               // true
console.log(5 === "5");             // false
console.log(Object.is(5, 5));       // true
console.log(Object.is(5, "5"));     // false

在大多數情況下,您可能仍然想使用=====來用於比較,因為Object.is()所涵蓋的特殊情況可能並不會對你造成影響。

Block bindings

傳統上,JavaScript的棘手的部分之一,一直被認為是var變數宣告的工作方式。在大多數基於C的語言中,變數在哪裡宣告就在那裡被建立。然而,在JavaScript中,卻並非如此。使用var宣告的變數會被懸掛到函式(或全域性空間)的頂部,而不管實際上宣告是在哪裡產生的。例如:

function getValue(condition) {

    if (condition) {
        var value = "blue";

        // other code

        return value;
    } else {

        return null;
    }
}

如果你不熟悉JavaScript,您可能會認為該變數value只會在condition值為true的時候被宣告和定義。而事實上,變數value無論如何都是會被宣告的。JavaScript引擎中,程式碼會被轉換成這樣:

function getValue(condition) {

    var value;

    if (condition) {
        value = "blue";

        // other code

        return value;
    } else {

        return null;
    }
}

value的宣告被移到了頂部(懸掛),而初始化卻留在了原有的地方。這意味著變數value的值其實在else子句中還是能夠被訪問的,它只是具有undefined的值,因為它此時並沒有被初始化。

這種特性往往需要新的JavaScript開發者花費一些時間來習慣變數懸掛,而且,這種獨特的行為有可能最終會導致一些錯誤。 出於這個原因,ECMAScript 6中引入了塊級作用域選項,使得對於變數生命週期的控制能夠更加有力。

Let declarations

let宣告語句的格式與var是完全一樣的。基本上來說,你可以用let代替var來宣告一個變數,但保留其範圍到當前的程式碼塊。例如:

function getValue(condition) {

    if (condition) {
        let value = "blue";

        // other code

        return value;
    } else {

        return null;
    }
}

這個功能現在的行為更接近於其他基於C的語言。變數value是使用let而不是var來宣告的。這意味著該宣告不會懸掛在頂端,而且變數value在一旦執行流程超出了if語句塊時就會被銷燬。如果condition總是計算出false,那麼value醬永遠不會被宣告或初始化。

也許,開發人員最想要變數塊級作用域的其中一處地方是for迴圈。這樣的程式碼我們常常可以看到:

for (var i=0; i < items.length; i++) {
    process(items[i]);
}

// i is still accessible here

在其他那些預設含有塊級作用域的語言中,像這樣的程式碼會按預期工作。然而在JavaScript中,因為var的宣告懸掛。變數i在迴圈完成後仍然可以被訪問。使用let則可以讓你得到預期的行為:

for (let i=0; i < items.length; i++) {
    process(items[i]);
}

// i is not accessible here

在這個例子中,變數i只存在於for迴圈之內。一旦迴圈完成後,該變數就會被摧毀,其他地方無法再次訪問它。

不同於varlet沒有懸掛特性。使用了let宣告的變數在let語句之前不能被訪問。任何試圖這麼做的行為都將會引發一個格式錯誤:

if (condition) {
    console.log(value);     // error!
    let value = "blue";
}

在這段程式碼中,變數value使用了let來定義和初始化,但該語句永遠不會執行,因為上一行會丟擲一個錯誤。

如果識別符號已在塊中定義,那麼在'let'宣告中使用該識別符號將會導致丟擲一個錯誤。例如:

var count = 30;

// Throws an error
let count = 40;

在這個例子中,count被宣告瞭兩次,一次用var,一次用let。因為let不會重新定義已經存在於同一範圍內的識別符號,所以宣告會丟擲一個錯誤。然而,如果一個let在作用域A中宣告瞭一個新的變數,同時這個變數的變數名在作用域B中已經存在,並且作用域B包含了作用域A,則不會丟擲錯誤,如:

var count = 30;

// Does not throw an error
if (condition) {

    let count = 40;

    // more code
}

在這裡,let宣告將不會丟擲一個錯誤,因為它在if語句的作用域中建立了一個新的count變數。這個新的變數會遮蔽全域性的count,導致我們無法從if語句塊中訪問到它。

提出let的目的在長遠看來是取代var,因為前者行為與其他語言中的變數宣告能夠保持一致。如果你正在編寫一段將只在ECMAScript 6或更高的環境中執行的JavaScript,你可能會想試試使用let並且只在那些為需要向後相容的其它指令碼中使用var

注:由於所有let宣告不會被懸掛在封閉塊的頂部,你可能需要自己把let宣告放在第一步。

常量宣告

另一種定義變數的新方式是使用const宣告語法。使用const來定義的變數被認為是常量,所以一旦設定,它的值不能被改變。出於這個原因,每個const常量必須被初始化。例如:

// Valid constant
const MAX_ITEMS = 30;

// Syntax error: missing initialization
const NAME;

常量也是塊級的宣告,類似於let。也就是說,一旦執行流跑出了它們被宣告的程式碼塊,常量就會被銷燬。並且常量宣告也會被提升到塊的頂部。例如:

if (condition) {
    const MAX_ITEMS = 5;

    // more code
}

// MAX_ITEMS isn't accessible here

在這段程式碼中,常量的MAX_ITEMS是在if語句的程式碼塊中宣告的。一旦該語句執行完畢後,MAX_ITEMS就會被銷燬,所以不能從塊的外部來訪問它。

並且,類似於let,如果一個const宣告的常量命名與在同一個作用域中的其它已經定義的變數/常量相同的話,就會丟擲異常。無論該變數是使用var(全域性或函式範圍內)還是使用let(在塊作用域)中宣告。例如:

var message = "Hello!";
let age = 25;

// Each of these would cause an error given the previous declarations
const message = "Goodbye!";
const age = 30;

注:一些瀏覽器實現了 ECMAScript 6預覽版本的const語句。它們實現的範圍從單純的var的代名詞(即允許被覆蓋的值),到確實符合定義,但只能在全域性或函式範圍內有效都有。因此,在生成系統中,你應該謹慎使用const,它可能無法給你提供你所期望的功能。

數字和數學

TODO:介紹

八進位制和二進位制字面量

ECMAScript 5 試圖通過在paseInt()和strict mode這兩處移除之前引入的八進位制整數字面量符號來簡化一些常見的數值錯誤。在ECMAScript 3和更早的版本中,八進位制數使用一個0後跟任意數量的數字來表示。例如:

// ECMAScript 3
var number = 071;       // 57 in decimal

var value1 = parseInt("71");    // 71
var value2 = parseInt("071");   // 57

許多開發者對於這一版本的八進位制字面量數字表示感到疑惑,也因為對於前導零在不同地方所產生的不同影響的誤解而犯下了許多錯誤。最令人震驚的是parseInt(),在其中前導零意味著該值將被視為八進位制而不是十進位制。這也導致了Douglas Crockford 的第一個JSLint的規則之一:始終使用parseInt()函式的第二個引數來指定字串應該怎樣被解釋。

ECMAScript 5減少了對八進位制數字的使用。首先,parseInt()方法已經被更改,因此它在沒有第二個引數時會忽略第一個引數的前導零。這意味著數字不再會被意外地視為八進位制。第二個變化是去除了在嚴格模式下的八進位制字面量符號。在嚴格模式下嘗試使用一個八進位制字面量會導致一個語法錯誤。

// ECMAScript 5
var number = 071;       // 57 in decimal

var value1 = parseInt("71");        // 71
var value2 = parseInt("071");       // 71
var value3 = parseInt("071", 8);    // 57

function getValue() {
    "use strict";
    return 071;     // syntax error
}

通過引入這兩個變化,ECMAScript 5嘗試消除了很多與八進位制字面量相關的混亂和錯誤。

ECMAScript 6又更進了一步,它重新採用八進位制字面量符號,以及一個二進位制字面量符號。這兩個符號通過在值的前面加上0x0X來代表十六進位制字面量符號。新的八進位制字面量格式以0o0O而新的二進位制字面量格式開始於0b0B。每個字面量型別後面必須跟一個或多個數字,0-7為八進位制,0-1二進位制。如下例:

// ECMAScript 6
var value1 = 0o71;      // 57 in decimal
var value2 = 0b101;     // 5 in decimal

新增這兩個字面量型別將允許JavaScript開發人員快速,輕鬆地引入包括二進位制,八進位制,十進位制和十六進位制格式在內的數字值,這對於某些型別的數學運算來說是非常重要的。

parseInt()方法不會處理看起來像八進位制或二進位制字面量的字串:

console.log(parseInt("0o71"));      // 0
console.log(parseInt("0b101"));     // 0

然而,Number() 函式則會正確地轉換八進位制或二進位制字面量的字串:

console.log(Number("0o71"));      // 57
console.log(Number("0b101"));     // 5

當使用八進位制或二進位制字面量字串時,一定要了解您的使用情況,並使用最適當的方法將其轉換為數字值。

更多

本章可能的內容:

  • Number中引入的新方法

小結

待補充

相關文章