你不懂JS:ES6与未来 第二章:语法(下)

官方中文版原文链接

感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大奖:点击这里领取

数字字面量扩展

在ES5之前,数字字面量看起来就像下面的东西 —— 八进制形式没有被官方指定,唯一被允许的是各种浏览器已经实质上达成一致的一种扩展:

var dec = 42,
    oct = 052,
    hex = 0x2a;

注意: 虽然你用不同的进制来指定一个数字,但是数字的数学值才是被存储的东西,而且默认的输出解释方式总是10进制的。前面代码段中的三个变量都在它们当中存储了值42

为了进一步说明052是一种非标准形式扩展,考虑如下代码:

Number( "42" );             // 42
Number( "052" );            // 52
Number( "0x2a" );           // 42

ES5继续允许这种浏览器扩展的八进制形式(包括这样的不一致性),除了在strict模式下,八进制字面量(052)是不允许的。做出这种限制的主要原因是,许多开发者似乎习惯于下意识地为了将代码对齐而在十进制的数字前面前缀0,然后遭遇他们完全改变了数字的值的意外!

ES6延续了除十进制数字之外的数字字面量可以被表示的遗留的改变/种类。现在有了一种官方的八进制形式,一种改进了的十六进制形式,和一种全新的二进制形式。由于Web兼容性的原因,在非strict模式下老式的八进制形式052将继续是合法的,但其实应当永远不再被使用了。

这些是新的ES6数字字面形式:

var dec = 42,
    oct = 0o52,         // or `0O52` :(
    hex = 0x2a,         // or `0X2a` :/
    bin = 0b101010;     // or `0B101010` :/

唯一允许的小数形式是十进制的。八进制,十六进制,和二进制都是整数形式。

而且所有这些形式的字符串表达形式都是可以被强制转换/变换为它们的数字等价物的:

Number( "42" );         // 42
Number( "0o52" );       // 42
Number( "0x2a" );       // 42
Number( "0b101010" );   // 42

虽然严格来说不是ES6新增的,但一个鲜为人知的事实是你其实可以做反方向的转换(好吧,某种意义上的):

var a = 42;

a.toString();           // "42" —— 也可使用`a.toString( 10 )`
a.toString( 8 );        // "52"
a.toString( 16 );       // "2a"
a.toString( 2 );        // "101010"

事实上,以这种方你可以用从236的任何进制表达一个数字,虽然你会使用标准进制 —— 2,8,10,和16 ——之外的情况非常少见。

Unicode

我只能说这一节不是一个穷尽了“关于Unicode你想知道的一切”的资料。我想讲解的是,你需要知道在ES6中对Unicode改变了什么,但是我们不会比这深入太多。Mathias Bynens (http://twitter.com/mathias) 大量且出色地撰写/讲解了关于JS和Unicode (参见 https://mathiasbynens.be/notes/javascript-unicodehttp://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode)。

0x00000xFFFF范围内的Unicode字符包含了所有的标准印刷字符(以各种语言),它们都是你可能看到过和互动过的。这组字符被称为 基本多文种平面(Basic Multilingual Plane (BMP))。BMP甚至包含像这个酷雪人一样的有趣字符: ☃ (U+2603)。

在这个BMP集合之外还有许多扩展的Unicode字符,它们的范围一直到0x10FFFF。这些符号经常被称为 星形(astral) 符号,这正是BMP之外的字符的16组 平面 (也就是,分层/分组)的名称。星形符号的例子包括𝄞 (U+1D11E)和💩 (U+1F4A9)。

在ES6之前,JavaScript字符串可以使用Unicode转义来指定Unicode字符,例如:

var snowman = "\u2603";
console.log( snowman );         // "☃"

然而,\uXXXXUnicode转义仅支持四个十六进制字符,所以用这种方式表示你只能表示BMP集合中的字符。要在ES6以前使用Unicode转义表示一个星形字符,你需要使用一个 代理对(surrogate pair) —— 基本上是两个经特殊计算的Unicode转义字符放在一起,被JS解释为一个单独星形字符:

var gclef = "\uD834\uDD1E";
console.log( gclef );           // "𝄞"

在ES6中,我们现在有了一种Unicode转义的新形式(在字符串和正则表达式中),称为Unicode 代码点转义

var gclef = "\u{1D11E}";
console.log( gclef );           // "𝄞"

如你所见,它的区别是出现在转义序列中的{ },它允许转义序列中包含任意数量的十六进制字符。因为你只需要六个就可以表示在Unicode中可能的最高代码点(也就是,0x10FFFF),所以这是足够的。

Unicode敏感的字符串操作

在默认情况下,JavaScript字符串操作和方法对字符串值中的星形符号是不敏感的。所以,它们独立地处理每个BMP字符,即便是可以组成一个单独字符的两半代理。考虑如下代码:

var snowman = "☃";
snowman.length;                 // 1

var gclef = "𝄞";
gclef.length;                   // 2

那么,我们如何才能正确地计算这样的字符串的长度呢?在这种场景下,下面的技巧可以工作:

var gclef = "𝄞";

[...gclef].length;              // 1
Array.from( gclef ).length;     // 1

回想一下本章早先的“for..of循环”一节,ES6字符串拥有内建的迭代器。这个迭代器恰好是Unicode敏感的,这意味着它将自动地把一个星形符号作为一个单独的值输出。我们在一个数组字面量上使用扩散操作符...,利用它创建了一个字符串符号的数组。然后我们只需检查这个结果数组的长度。ES6的Array.from(..)基本上与[...XYZ]做的事情相同,不过我们将在第六章中讲解这个工具的细节。

警告: 应当注意的是,相对地讲,与理论上经过优化的原生工具/属性将做的事情比起来,仅仅为了得到一个字符串的长度就构建并耗尽一个迭代器在性能上的代价是高昂的。

不幸的是,完整的答案并不简单或直接。除了代理对(字符串迭代器可以搞定的),一些特殊的Unicode代码点有其他特殊的行为,解释起来非常困难。例如,有一组代码点可以修改前一个相邻的字符,称为 组合变音符号(Combining Diacritical Marks)

考虑这两个数组的输出:

console.log( s1 );              // "é"
console.log( s2 );              // "é"

它们看起来一样,但它们不是!这是我们如何创建s1s2的:

var s1 = "\xE9",
    s2 = "e\u0301";

你可能猜到了,我们前面的length技巧对s2不管用:

[...s1].length;                 // 1
[...s2].length;                 // 2

那么我们能做什么?在这种情况下,我们可以使用ES6的String#normalize(..)工具,在查询这个值的长度前对它实施一个 Unicode正规化操作

var s1 = "\xE9",
    s2 = "e\u0301";

s1.normalize().length;          // 1
s2.normalize().length;          // 1

s1 === s2;                      // false
s1 === s2.normalize();          // true

实质上,normalize(..)接受一个"e\u0301"这样的序列,并把它正规化为\xE9。正规化甚至可以组合多个相邻的组合符号,如果存在适合他们组合的Unicode字符的话:

var s1 = "o\u0302\u0300",
    s2 = s1.normalize(),
    s3 = "ồ";

s1.length;                      // 3
s2.length;                      // 1
s3.length;                      // 1

s2 === s3;                      // true

不幸的是,这里的正规化也不完美。如果你有多个组合符号在修改一个字符,你可能不会得到你所期望的长度计数,因为一个被独立定义的,可以表示所有这些符号组合的正规化字符可能不存在。例如:

var s1 = "e\u0301\u0330";

console.log( s1 );              // "ḛ́"

s1.normalize().length;          // 2

你越深入这个兔子洞,你就越能理解要得到一个“长度”的精确定义是很困难的。我们在视觉上看到的作为一个单独字符绘制的东西 —— 更精确地说,它称为一个 字形 —— 在程序处理的意义上不总是严格地关联到一个单独的“字符”上。

提示: 如果你就是想看看这个兔子洞有多深,看看“字形群集边界(Grapheme Cluster Boundaries)”算法(http://www.Unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)。

字符定位

与长度的复杂性相似,“在位置2上的字符是什么?”,这么问的意思究竟是什么?前ES6的原生答案来自charAt(..),它不会遵守一个星形字符的原子性,也不会考虑组合符号。

考虑如下代码:

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

console.log( s1 );              // "abćd"
console.log( s2 );              // "abćd"
console.log( s3 );              // "ab𝒞d"

s1.charAt( 2 );                 // "c"
s2.charAt( 2 );                 // "ć"
s3.charAt( 2 );                 // "" <-- 不可打印的代理字符
s3.charAt( 3 );                 // "" <-- 不可打印的代理字符

那么,ES6会给我们Unicode敏感版本的charAt(..)吗?不幸的是,不。在本书写作时,在后ES6的考虑之中有一个这样的工具的提案。

但是使用我们在前一节探索的东西(当然也带着它的限制!),我们可以黑一个ES6的答案:

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

[...s1.normalize()][2];         // "ć"
[...s2.normalize()][2];         // "ć"
[...s3.normalize()][2];         // "𝒞"

警告: 提醒一个早先的警告:在每次你想得到一个单独的字符时构建并耗尽一个迭代器……在性能上不是很理想。对此,希望我们很快能在后ES6时代得到一个内建的,优化过的工具。

那么charCodeAt(..)工具的Unicode敏感版本呢?ES6给了我们codePointAt(..)

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

s1.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s2.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s3.normalize().codePointAt( 2 ).toString( 16 );
// "1d49e"

那么从另一个方向呢?String.fromCharCode(..)的Unicode敏感版本是ES6的String.fromCodePoint(..)

String.fromCodePoint( 0x107 );      // "ć"

String.fromCodePoint( 0x1d49e );    // "𝒞"

那么等一下,我们能组合String.fromCodePoint(..)codePointAt(..)来得到一个刚才的Unicode敏感charAt(..)的更好版本吗?是的!

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

String.fromCodePoint( s1.normalize().codePointAt( 2 ) );
// "ć"

String.fromCodePoint( s2.normalize().codePointAt( 2 ) );
// "ć"

String.fromCodePoint( s3.normalize().codePointAt( 2 ) );
// "𝒞"

还有好几个字符串方法我们没有在这里讲解,包括toUpperCase()toLowerCase()substring(..)indexOf(..)slice(..),以及其他十几个。它们中没有任何一个为了完全支持Unicode而被改变或增强过,所以在处理含有星形符号的字符串是,你应当非常小心 —— 可能干脆回避它们!

还有几个字符串方法为了它们的行为而使用正则表达式,比如replace(..)match(..)。值得庆幸的是,ES6为正则表达式带来了Unicode支持,正如我们在本章早前的“Unicode标志”中讲解过的那样。

好了,就是这些!有了我们刚刚讲过的各种附加功能,JavaScript的Unicode字符串支持要比前ES6时代好太多了(虽然还不完美)。

Unicode标识符名称

Unicode还可以被用于标识符名称(变量,属性,等等)。在ES6之前,你可以通过Unicode转义这么做,比如:

var \u03A9 = 42;

// 等同于:var Ω = 42;

在ES6中,你还可以使用前面讲过的代码点转义语法:

var \u{2B400} = 42;

// 等同于:var 𫐀 = 42;

关于究竟哪些Unicode字符被允许使用,有一组复杂的规则。另外,有些字符只要不是标识符名称的第一个字符就允许使用。

注意: 关于所有这些细节,Mathias Bynens写了一篇了不起的文章 (https://mathiasbynens.be/notes/javascript-identifiers-es6)。

很少有理由,或者是为了学术上的目的,才会在标识符名称中使用这样不寻常的字符。你通常不会因为依靠这些深奥的功能编写代码而感到舒服。

Symbol

在ES6中,长久以来首次,有一个新的基本类型被加入到了JavaScript:symbol。但是,与其他的基本类型不同,symbol没有字面形式。

这是你如何创建一个symbol:

var sym = Symbol( "some optional description" );

typeof sym;     // "symbol"

一些要注意的事情是:

  • 你不能也不应该将newSymbol(..)一起使用。它不是一个构造器,你也不是在产生一个对象。
  • 被传入Symbol(..)的参数是可选的。如果传入的话,它应当是一个字符串,为symbol的目的给出一个友好的描述。
  • typeof的输出是一个新的值("symbol"),这是识别一个symbol的主要方法。

如果描述被提供的话,它仅仅用于symbol的字符串化表示:

sym.toString();     // "Symbol(some optional description)"

与基本字符串值如何不是String的实例的原理很相似,symbol也不是Symbol的实例。如果,由于某些原因,你想要为一个symbol值构建一个封箱的包装器对像,你可以做如下的事情:

sym instanceof Symbol;      // false

var symObj = Object( sym );
symObj instanceof Symbol;   // true

symObj.valueOf() === sym;   // true

注意: 在这个代码段中的symObjsym是可以互换使用的;两种形式可以在symbol被用到的地方使用。没有太多的理由要使用封箱的包装对象形式(symObj),而不用基本类型形式(sym)。和其他基本类型的建议相似,使用sym而非symObj可能是最好的。

一个symbol本身的内部值 —— 称为它的name —— 被隐藏在代码之外而不能取得。你可以认为这个symbol的值是一个自动生成的,(在你的应用程序中)独一无二的字符串值。

但如果这个值是隐藏且不可取得的,那么拥有一个symbol还有什么意义?

一个symbol的主要意义是创建一个不会和其他任何值冲突的类字符串值。所以,举例来说,可以考虑将一个symbol用做表示一个事件的名称的值:

const EVT_LOGIN = Symbol( "event.login" );

然后你可以在一个使用像"event.login"这样的一般字符串字面量的地方使用EVT_LOGIN

evthub.listen( EVT_LOGIN, function(data){
    // ..
} );

其中的好处是,EVT_LOGIN持有一个不能被其他任何值所(有意或无意地)重复的值,所以在哪个事件被分发或处理的问题上不可能存在任何含糊。

注意: 在前面的代码段的幕后,几乎可以肯定地认为evthub工具使用了EVT_LOGIN参数值的symbol值作为某个跟踪事件处理器的内部对象的属性/键。如果evthub需要将symbol值作为一个真实的字符串使用,那么它将需要使用String(..)或者toString(..)进行明确强制转换,因为symbol的隐含字符串强制转换是不允许的。

你可能会将一个symbol直接用做一个对象中的属性名/键,如此作为一个你想将之用于隐藏或元属性的特殊属性。重要的是,要知道虽然你试图这样对待它,但是它 实际上 并不是隐藏或不可接触的属性。

考虑这个实现了 单例 模式行为的模块 —— 也就是,它仅允许自己被创建一次:

const INSTANCE = Symbol( "instance" );

function HappyFace() {
    if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

    function smile() { .. }

    return HappyFace[INSTANCE] = {
        smile: smile
    };
}

var me = HappyFace(),
    you = HappyFace();

me === you;         // true

这里的symbol值INSTANCE是一个被静态地存储在HappyFace()函数对象上的特殊的,几乎是隐藏的,类元属性。

替代性地,它本可以是一个像__instance这样的普通属性,而且其行为将会是一模一样的。symbol的使用仅仅增强了程序元编程的风格,将这个INSTANCE属性与其他普通的属性间保持隔离。

Symbol注册表

在前面几个例子中使用symbol的一个微小的缺点是,变量EVT_LOGININSTANCE不得不存储在外部作用域中(甚至也许是全局作用域),或者用某种方法存储在一个可用的公共位置,这样代码所有需要使用这些symbol的部分都可以访问它们。

为了辅助组织访问这些symbol的代码,你可以使用 全局symbol注册表 来创建symbol。例如:

const EVT_LOGIN = Symbol.for( "event.login" );

console.log( EVT_LOGIN );       // Symbol(event.login)

和:

function HappyFace() {
    const INSTANCE = Symbol.for( "instance" );

    if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

    // ..

    return HappyFace[INSTANCE] = { .. };
}

Symbol.for(..)查询全局symbol注册表来查看一个symbol是否已经使用被提供的说明文本存储过了,如果有就返回它。如果没有,就创建一个并返回。换句话说,全局symbol注册表通过描述文本将symbol值看作它们本身的单例。

但这也意味着只要使用匹配的描述名,你的应用程序的任何部分都可以使用Symbol.for(..)从注册表中取得symbol。

讽刺的是,基本上symbol的本意是在你的应用程序中取代 魔法字符串 的使用(被赋予了特殊意义的随意的字符串值)。但是你正是在全局symbol注册表中使用 魔法 描述字符串值来唯一识别/定位它们的!

为了避免意外的冲突,你可能想使你的symbol描述十分独特。这么做的一个简单的方法是在它们之中包含前缀/环境/名称空间的信息。

例如,考虑一个像下面这样的工具:

function extractValues(str) {
    var key = Symbol.for( "extractValues.parse" ),
        re = extractValues[key] ||
            /[^=&]+?=([^&]+?)(?=&|$)/g,
        values = [], match;

    while (match = re.exec( str )) {
        values.push( match[1] );
    }

    return values;
}

我们使用魔法字符串值"extractValues.parse",因为在注册表中的其他任何symbol都不太可能与这个描述相冲突。

如果这个工具的一个用户想要覆盖这个解析用的正则表达式,他们也可以使用symbol注册表:

extractValues[Symbol.for( "extractValues.parse" )] =
    /..some pattern../g;

extractValues( "..some string.." );

除了symbol注册表在全局地存储这些值上提供的协助以外,我们在这里看到的一切其实都可以通过将魔法字符串"extractValues.parse"作为一个键,而不是一个symbol,来做到。这其中在元编程的层次上的改进要多于在函数层次上的改进。

你可能偶然会使用一个已经被存储在注册表中的symbol值来查询它底层存储了什么描述文本(键)。例如,因为你无法传递symbol值本身,你可能需要通知你的应用程序的另一个部分如何在注册表中定位一个symbol。

你可以使用Symbol.keyFor(..)取得一个被注册的symbol描述文本(键):

var s = Symbol.for( "something cool" );

var desc = Symbol.keyFor( s );
console.log( desc );            // "something cool"

// 再次从注册表取得symbol
var s2 = Symbol.for( desc );

s2 === s;                       // true

Symbols作为对象属性

如果一个symbol被用作一个对象的属性/键,它会被以一种特殊的方式存储,以至这个属性不会出现在这个对象属性的普通枚举中:

var o = {
    foo: 42,
    [ Symbol( "bar" ) ]: "hello world",
    baz: true
};

Object.getOwnPropertyNames( o );    // [ "foo","baz" ]

要取得对象的symbol属性:

Object.getOwnPropertySymbols( o );  // [ Symbol(bar) ]

这表明一个属性symbol实际上不是隐藏的或不可访问的,因为你总是可以在Object.getOwnPropertySymbols(..)的列表中看到它。

内建Symbols

ES6带来了好几种预定义的内建symbol,它们暴露了在JavaScript对象值上的各种元行为。然而,正如人们所预料的那样,这些symbol 没有 没被注册到全局symbol注册表中。

取而代之的是,它们作为属性被存储到了Symbol函数对象中。例如,在本章早先的“for..of”一节中,我们介绍了值Symbol.iterator

var a = [1,2,3];

a[Symbol.iterator];         // native function

语言规范使用@@前缀注释指代内建的symbol,最常见的几个是:@@iterator@@toStringTag@@toPrimitive。还定义了几个其他的symbol,虽然他们可能不那么频繁地被使用。

注意: 关于这些内建symbol如何被用于元编程的详细信息,参见第七章的“通用Symbol”。

复习

ES6给JavaScript增加了一堆新的语法形式,有好多东西要学!

这些东西中的大多数都是为了缓解常见编程惯用法中的痛点而设计的,比如为函数参数设置默认值和将“剩余”的参数收集到一个数组中。解构是一个强大的工具,用来更简约地表达从数组或嵌套对象的赋值。

虽然像箭头函数=>这样的特性看起来也都是关于更简短更好看的语法,但是它们实际上拥有非常特殊的行为,你应当在恰当的情况下有意地使用它们。

扩展的Unicode支持,新的正则表达式技巧,和新的symbol基本类型充实了ES6语法的发展演变。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容