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

官方中文版原文链接

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

对象字面量扩展

ES6给不起眼儿的{ .. }对象字面量增加了几个重要的便利扩展。

简约属性

你一定很熟悉用这种形式的对象字面量声明:

var x = 2, y = 3,
    o = {
        x: x,
        y: y
    };

如果到处说x: x总是让你感到繁冗,那么有个好消息。如果你需要定义一个名称和词法标识符一致的属性,你可以将它从x: x缩写为x。考虑如下代码:

var x = 2, y = 3,
    o = {
        x,
        y
    };

简约方法

本着与我们刚刚检视的简约属性相同的精神,添附在对象字面量属性上的函数也有一种便利简约形式。

以前的方式:

var o = {
    x: function(){
        // ..
    },
    y: function(){
        // ..
    }
}

而在ES6中:

var o = {
    x() {
        // ..
    },
    y() {
        // ..
    }
}

警告: 虽然x() { .. }看起来只是x: function(){ .. }的缩写,但是简约方法有一种特殊行为,是它们对应的老方式所不具有的;确切地说,是允许super(参见本章稍后的“对象super”)的使用。

Generator(见第四章)也有一种简约方法形式:

var o = {
    *foo() { .. }
};

简约匿名

虽然这种便利缩写十分诱人,但是这其中有一个微妙的坑要小心。为了展示这一点,让我们检视一下如下的前ES6代码,你可能会试着使用简约方法来重构它:

function runSomething(o) {
    var x = Math.random(),
        y = Math.random();

    return o.something( x, y );
}

runSomething( {
    something: function something(x,y) {
        if (x > y) {
            // 使用相互对调的`x`和`y`来递归地调用
            return something( y, x );
        }

        return y - x;
    }
} );

这段蠢代码只是生成两个随机数,然后用大的减去小的。但这里重要的不是它做的是什么,而是它是如何被定义的。让我把焦点放在对象字面量和函数定义上,就像我们在这里看到的:

runSomething( {
    something: function something(x,y) {
        // ..
    }
} );

为什么我们同时说something:function something?这不是冗余吗?实际上,不是,它们俩被用于不同的目的。属性something让我们能够调用o.something(..),有点儿像它的公有名称。但是第二个something是一个词法名称,使这个函数可以为了递归而从内部引用它自己。

你能看出来为什么return something(y,x)这一行需要名称something来引用这个函数吗?因为这里没有对象的词法名称,要是有的话我们就可以说return o.something(y,x)或者其他类似的东西。

当一个对象字面量的确拥有一个标识符名称时,这其实是一个很常见的做法,比如:

var controller = {
    makeRequest: function(..){
        // ..
        controller.makeRequest(..);
    }
};

这是个好主意吗?也许是,也许不是。你在假设名称controller将总是指向目标对象。但它也很可能不是 —— 函数makeRequest(..)不能控制外部的代码,因此不能强制你的假设一定成立。这可能会回过头来咬到你。

另一些人喜欢使用this定义这样的东西:

var controller = {
    makeRequest: function(..){
        // ..
        this.makeRequest(..);
    }
};

这看起来不错,而且如果你总是用controller.makeRequest(..)来调用方法的话它就应该能工作。但现在你有一个this绑定的坑,如果你做这样的事情的话:

btn.addEventListener( "click", controller.makeRequest, false );

当然,你可以通过传递controller.makeRequest.bind(controller)作为绑定到事件上的处理器引用来解决这个问题。但是这很讨厌 —— 它不是很吸引人。

或者要是你的内部this.makeRequest(..)调用需要从一个嵌套的函数内发起呢?你会有另一个this绑定灾难,人们经常使用var self = this这种用黑科技解决,就像:

var controller = {
    makeRequest: function(..){
        var self = this;

        btn.addEventListener( "click", function(){
            // ..
            self.makeRequest(..);
        }, false );
    }
};

更讨厌。

注意: 更多关于this绑定规则和陷阱的信息,参见本系列的 this与对象原型 的第一到二章。

好了,这些与简约方法有什么关系?回想一下我们的something(..)方法定义:

runSomething( {
    something: function something(x,y) {
        // ..
    }
} );

在这里的第二个something提供了一个超级便利的词法标识符,它总是指向函数自己,给了我们一个可用于递归,事件绑定/解除等等的完美引用 —— 不用乱搞this或者使用不可靠的对象引用。

太好了!

那么,现在我们试着将函数引用重构为这种ES6解约方法的形式:

runSomething( {
    something(x,y) {
        if (x > y) {
            return something( y, x );
        }

        return y - x;
    }
} );

第一眼看上去不错,除了这个代码将会坏掉。return something(..)调用经不会找到something标识符,所以你会得到一个ReferenceError。噢,但为什么?

上面的ES6代码段将会被翻译为:

runSomething( {
    something: function(x,y){
        if (x > y) {
            return something( y, x );
        }

        return y - x;
    }
} );

仔细看。你看出问题了吗?简约方法定义暗指something: function(x,y)。看到我们依靠的第二个something是如何被省略的了吗?换句话说,简约方法暗指匿名函数表达式。

对,讨厌。

注意: 你可能认为在这里=>箭头函数是一个好的解决方案。但是它们也同样不够,因为它们也是匿名函数表达式。我们将在本章稍后的“箭头函数”中讲解它们。

一个部分地补偿了这一点的消息是,我们的简约函数something(x,y)将不会是完全匿名的。参见第七章的“函数名”来了解ES6函数名称的推断规则。这不会在递归中帮到我们,但是它至少在调试时有用处。

那么我们怎样总结简约方法?它们简短又甜蜜,而且很方便。但是你应当仅在你永远不需要将它们用于递归或事件绑定/解除时使用它们。否则,就坚持使用你的老式something: function something(..)方法定义。

你的很多方法都将可能从简约方法定义中受益,这是个非常好的消息!只要小心几处未命名的灾难就好。

ES5 Getter/Setter

技术上讲,ES5定义了getter/setter字面形式,但是看起来它们没有被太多地使用,这主要是由于缺乏转译器来处理这种新的语法(其实,它是ES5中加入的唯一的主要新语法)。所以虽然它不是一个ES6的新特性,我们也将简单地复习一下这种形式,因为它可能会随着ES6的向前发展而变得有用得多。

考虑如下代码:

var o = {
    __id: 10,
    get id() { return this.__id++; },
    set id(v) { this.__id = v; }
}

o.id;           // 10
o.id;           // 11
o.id = 20;
o.id;           // 20

// 而:
o.__id;         // 21
o.__id;         // 还是 —— 21!

这些getter和setter字面形式也可以出现在类中;参见第三章。

警告: 可能不太明显,但是setter字面量必须恰好有一个被声明的参数;省略它或罗列其他的参数都是不合法的语法。这个单独的必须参数 可以 使用解构和默认值(例如,set id({ id: v = 0 }) { .. }),但是收集/剩余...是不允许的(set id(...v) { .. })。

计算型属性名

你可能曾经遇到过像下面的代码段那样的情况,你的一个或多个属性名来自于某种表达式,因此你不能将它们放在对象字面量中:

var prefix = "user_";

var o = {
    baz: function(..){ .. }
};

o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..

ES6为对象字面定义增加了一种语法,它允许你指定一个应当被计算的表达式,其结果就是被赋值属性名。考虑如下代码:

var prefix = "user_";

var o = {
    baz: function(..){ .. },
    [ prefix + "foo" ]: function(..){ .. },
    [ prefix + "bar" ]: function(..){ .. }
    ..
};

任何合法的表达式都可以出现在位于对象字面定义的属性名位置的[ .. ]内部。

很有可能,计算型属性名最经常与Symbol(我们将在本章稍后的“Symbol”中讲解)一起使用,比如:

var o = {
    [Symbol.toStringTag]: "really cool thing",
    ..
};

Symbol.toStringTag是一个特殊的内建值,我们使用[ .. ]语法求值得到,所以我们可以将值"really cool thing"赋值给这个特殊的属性名。

计算型属性名还可以作为简约方法或简约generator的名称出现:

var o = {
    ["f" + "oo"]() { .. }   // 计算型简约方法
    *["b" + "ar"]() { .. }  // 计算型简约generator
};

设置[[Prototype]]

我们不会在这里讲解原型的细节,所以关于它的更多信息,参见本系列的 this与对象原型

有时候在你声明对象字面量的同时给它的[[Prototype]]赋值很有用。下面的代码在一段时期内曾经是许多JS引擎的一种非标准扩展,但是在ES6中得到了标准化:

var o1 = {
    // ..
};

var o2 = {
    __proto__: o1,
    // ..
};

o2是用一个对象字面量声明的,但它也被[[Prototype]]链接到了o1。这里的__proto__属性名还可以是一个字符串"__proto__",但是要注意它 不能 是一个计算型属性名的结果(参见前一节)。

客气点儿说,__proto__是有争议的。在ES6中,它看起来是一个最终被很勉强地标准化了的,几十年前的自主扩展功能。实际上,它属于ES6的“Annex B”,这一部分罗列了JS感觉它仅仅为了兼容性的原因,而不得不标准化的东西。

警告: 虽然我勉强赞同在一个对象字面定义中将__proto__作为一个键,但我绝对不赞同在对象属性形式中使用它,就像o.__proto__。这种形式既是一个getter也是一个setter(同样也是为了兼容性的原因),但绝对存在更好的选择。更多信息参见本系列的 this与对象原型

对于给一个既存的对象设置[[Prototype]],你可以使用ES6的工具Object.setPrototypeOf(..)。考虑如下代码:

var o1 = {
    // ..
};

var o2 = {
    // ..
};

Object.setPrototypeOf( o2, o1 );

注意: 我们将在第六章中再次讨论Object。“Object.setPrototypeOf(..)静态函数”提供了关于Object.setPrototypeOf(..)的额外细节。另外参见“Object.assign(..)静态函数”来了解另一种将o2原型关联到o1的形式。

对象super

super通常被认为是仅与类有关。然而,由于JS对象仅有原型而没有类的性质,super是同样有效的,而且在普通对象的简约方法中行为几乎一样。

考虑如下代码:

var o1 = {
    foo() {
        console.log( "o1:foo" );
    }
};

var o2 = {
    foo() {
        super.foo();
        console.log( "o2:foo" );
    }
};

Object.setPrototypeOf( o2, o1 );

o2.foo();       // o1:foo
                // o2:foo

警告: super仅在简约方法中允许使用,而不允许在普通的函数表达式属性中。而且它还仅允许使用super.XXX形式(属性/方法访问),而不是super()形式。

在方法o2.foo()中的super引用被静态地锁定在了o2,而且明确地说是o2[[Prototype]]。这里的super基本上是Object.getPrototypeOf(o2) —— 显然被解析为o1 —— 这就是他如何找到并调用o1.foo()的。

关于super的完整细节,参见第三章的“类”。

模板字面量

在这一节的最开始,我将不得不呼唤这个ES6特性的极其……误导人的名称,这要看在你的经验中 模板(template) 一词的含义是什么。

许多开发者认为模板是一段可复用的,可重绘的文本,就像大多数模板引擎(Mustache,Handlebars,等等)提供的能力那样。ES6中使用的 模板 一词暗示着相似的东西,就像一种声明可以被重绘的内联模板字面量的方法。然而,这根本不是考虑这个特性的正确方式。

所以,在我们继续之前,我把它重命名为它本应被称呼的名字:插值型字符串字面量(或者略称为 插值型字面量)。

你已经十分清楚地知道了如何使用"'分隔符来声明字符串字面量,而且你还知道它们不是(像有些语言中拥有的)内容将被解析为插值表达式的 智能字符串

但是,ES6引入了一种新型的字符串字面量,使用反引号`作为分隔符。这些字符串字面量允许嵌入基本的字符串插值表达式,之后这些表达式自动地被解析和求值。

这是老式的前ES6方式:

var name = "Kyle";

var greeting = "Hello " + name + "!";

console.log( greeting );            // "Hello Kyle!"
console.log( typeof greeting );     // "string"

现在,考虑这种新的ES6方式:

var name = "Kyle";

var greeting = `Hello ${name}!`;

console.log( greeting );            // "Hello Kyle!"
console.log( typeof greeting );     // "string"

如你所见,我们在一系列被翻译为字符串字面量的字符周围使用了`..`,但是${..}形式中的任何表达式都将立即内联地被解析和求值。称呼这样的解析和求值的高大上名词就是 插值(interpolation)(比模板要准确多了)。

被插值的字符串字面量表达式的结果只是一个老式的普通字符串,赋值给变量greeting

警告: typeof greeting == "string"展示了为什么不将这些实体考虑为特殊的模板值很重要,因为你不能将这种字面量的未求值形式赋值给某些东西并复用它。`..`字符串字面量在某种意义上更像是IIFE,因为它自动内联地被求值。`..`字符串字面量的结果只不过是一个简单的字符串。

插值型字符串字面量的一个真正的好处是他们允许被分割为多行:

var text =
`Now is the time for all good men
to come to the aid of their
country!`;

console.log( text );
// Now is the time for all good men
// to come to the aid of their
// country!

在插值型字符串字面量中的换行将会被保留在字符串值中。

除非在字面量值中作为明确的转义序列出现,回车字符\r(编码点U+000D)的值或者回车+换行序列\r\n(编码点U+000DU+000A)的值都会被泛化为一个换行字符\n(编码点U+000A)。但不要担心;这种泛化很少见而且很可能仅会在你将文本拷贝粘贴到JS文件中时才会发生。

插值表达式

在一个插值型字符串字面量中,任何合法的表达式都被允许出现在${..}内部,包括函数调用,内联函数表达式调用,甚至是另一个插值型字符串字面量!

考虑如下代码:

function upper(s) {
    return s.toUpperCase();
}

var who = "reader";

var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;

console.log( text );
// A very WARM welcome
// to all of you READERS!

当我们组合变量who与字符串s时, 相对于who + "s",这里的内部插值型字符串字面量`${who}s`更方便一些。有些情况下嵌套的插值型字符串字面量是有用的,但是如果你发现自己做这样的事情太频繁,或者发现你自己嵌套了好几层时,你就要小心一些。

如果确实有这样情况,你的字符串你值生产过程很可能可以从某些抽象中获益。

警告: 作为一个忠告,使用这样的新发现的力量时要非常小心你代码的可读性。就像默认值表达式和解构赋值表达式一样,仅仅因为你 做某些事情,并不意味着你 应该 做这些事情。在使用新的ES6技巧时千万不要做过了头,使你的代码比你或者你的其他队友聪明。

表达式作用域

关于作用域的一个快速提醒是它用于解析表达式中的变量时。我早先提到过一个插值型字符串字面量与IIFE有些相像,事实上这也可以考虑为作用域行为的一种解释。

考虑如下代码:

function foo(str) {
    var name = "foo";
    console.log( str );
}

function bar() {
    var name = "bar";
    foo( `Hello from ${name}!` );
}

var name = "global";

bar();                  // "Hello from bar!"

在函数bar()内部,字符串字面量`..`被表达的那一刻,可供它查找的作用域发现变量的name的值为"bar"。既不是全局的name也不是foo(..)name。换句话说,一个插值型字符串字面量在它出现的地方是词法作用域的,而不是任何方式的动态作用域。

标签型模板字面量

再次为了合理性而重命名这个特性:标签型字符串字面量

老实说,这是一个ES6提供的更酷的特性。它可能看起来有点儿奇怪,而且也许一开始看起来一般不那么实用。但一旦你花些时间在它上面,标签型字符串字面量的用处可能会令你惊讶。

例如:

function foo(strings, ...values) {
    console.log( strings );
    console.log( values );
}

var desc = "awesome";

foo`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]

让我们花点儿时间考虑一下前面的代码段中发生了什么。首先,跳出来的最刺眼的东西就是foo`Everything...`;。它看起来不像是任何我们曾经见过的东西。不是吗?

它实质上是一种不需要( .. )的特殊函数调用。标签 —— 在字符串字面量`..`之前的foo部分 —— 是一个应当被调用的函数的值。实际上,它可以是返回函数的任何表达式,甚至是一个返回另一个函数的函数调用,就像:

function bar() {
    return function foo(strings, ...values) {
        console.log( strings );
        console.log( values );
    }
}

var desc = "awesome";

bar()`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]

但是当作为一个字符串字面量的标签时,函数foo(..)被传入了什么?

第一个参数值 —— 我们称它为strings —— 是一个所有普通字符串的数组(所有被插值的表达式之间的东西)。我们在strings数组中得到两个值:"Everything is ""!"

之后为了我们示例的方便,我们使用...收集/剩余操作符(见本章早先的“扩散/剩余”部分)将所有后续的参数值收集到一个称为values的数组中,虽说你本来当然可以把它们留作参数strings后面单独的命名参数。

被收集进我们的values数组中的参数值,就是在字符串字面量中发现的,已经被求过值的插值表达式的结果。所以在我们的例子中values里唯一的元素显然就是awesome

你可以将这两个数组考虑为:在values中的值原本是你拼接在stings的值之间的分隔符,而且如果你将所有的东西连接在一起,你就会得到完整的插值字符串值。

一个标签型字符串字面量像是一个在插值表达式被求值之后,但是在最终的字符串被编译之前的处理步骤,允许你在从字面量中产生字符串的过程中进行更多的控制。

一般来说,一个字符串字面连标签函数(在前面的代码段中是foo(..))应当计算一个恰当的字符串值并返回它,所以你可以使用标签型字符串字面量作为一个未打标签的字符串字面量来使用:

function tag(strings, ...values) {
    return strings.reduce( function(s,v,idx){
        return s + (idx > 0 ? values[idx-1] : "") + v;
    }, "" );
}

var desc = "awesome";

var text = tag`Everything is ${desc}!`;

console.log( text );            // Everything is awesome!

在这个代码段中,tag(..)是一个直通操作,因为它不实施任何特殊的修改,而只是使用reduce(..)来循环遍历,并像一个未打标签的字符串字面量一样,将stringsvalues拼接/穿插在一起。

那么实际的用法是什么?有许多高级的用法超出了我们要在这里讨论的范围。但这里有一个格式化美元数字的简单想法(有些像基本的本地化):

function dollabillsyall(strings, ...values) {
    return strings.reduce( function(s,v,idx){
        if (idx > 0) {
            if (typeof values[idx-1] == "number") {
                // 看,也使用插值性字符串字面量!
                s += `$${values[idx-1].toFixed( 2 )}`;
            }
            else {
                s += values[idx-1];
            }
        }

        return s + v;
    }, "" );
}

var amt1 = 11.99,
    amt2 = amt1 * 1.08,
    name = "Kyle";

var text = dollabillsyall
`Thanks for your purchase, ${name}! Your
product cost was ${amt1}, which with tax
comes out to ${amt2}.`

console.log( text );
// Thanks for your purchase, Kyle! Your
// product cost was $11.99, which with tax
// comes out to $12.95.

如果在values数组中遇到一个number值,我们就在它前面放一个"$"并用toFixed(2)将它格式化为小数点后两位有效。否则,我们就不碰这个值而让它直通过去。

原始字符串

在前一个代码段中,我们的标签函数接受的第一个参数值称为strings,是一个数组。但是有一点儿额外的数据被包含了进来:所有字符串的原始未处理版本。你可以使用.raw属性访问这些原始字符串值,就像这样:

function showraw(strings, ...values) {
    console.log( strings );
    console.log( strings.raw );
}

showraw`Hello\nWorld`;
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]

原始版本的值保留了原始的转义序列\n\n是两个分离的字符),但处理过的版本认为它是一个单独的换行符。但是,早先提到的行终结符泛化操作,是对两个值都实施的。

ES6带来了一个内建函数,它可以用做字符串字面量的标签:String.raw(..)。它简单地直通strings值的原始版本:

console.log( `Hello\nWorld` );
// Hello
// World

console.log( String.raw`Hello\nWorld` );
// Hello\nWorld

String.raw`Hello\nWorld`.length;
// 12

字符串字面量标签的其他用法包括国际化,本地化,和许多其他的特殊处理。

箭头函数

我们在本章早先接触了函数中this绑定的复杂性,而且在本系列的 this与对象原型 中也以相当的篇幅讲解过。理解普通函数中基于this的编程带来的挫折是很重要的,因为这是ES6的新=>箭头函数的主要动机。

作为与普通函数的比较,我们首先来展示一下箭头函数看起来什么样:

function foo(x,y) {
    return x + y;
}

// 对比

var foo = (x,y) => x + y;

箭头函数的定义由一个参数列表(零个或多个参数,如果参数不是只有一个,需要有一个( .. )包围这些参数)组成,紧跟着是一个=>符号,然后是一个函数体。

所以,在前面的代码段中,箭头函数只是(x,y) => x + y这一部分,而这个函数的引用刚好被赋值给了变量foo

函数体仅在含有多于一个表达式,或者由一个非表达式语句组成时才需要用{ .. }括起来。如果仅含有一个表达式,而且你省略了外围的{ .. },那么在这个表达式前面就会有一个隐含的return,就像前面的代码段中展示的那样。

这里是一些其他种类的箭头函数:

var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
    var z = x * 2 + y;
    y++;
    x *= 3;
    return (x + y + z) / 2;
};

箭头函数 总是 函数表达式;不存在箭头函数声明。而且很明显它们都是匿名函数表达式 —— 它们没有可以用于递归或者事件绑定/解除的命名引用 —— 但在第七章的“函数名”中将会讲解为了调试的目的而存在的ES6函数名接口规则。

注意: 普通函数参数的所有功能对于箭头函数都是可用的,包括默认值,解构,剩余参数,等等。

箭头函数拥有漂亮,简短的语法,这使得它们在表面上看起来对于编写简洁代码很有吸引力。确实,几乎所有关于ES6的文献(除了这个系列中的书目)看起来都立即将箭头函数仅仅认作“新函数”。

这说明在关于箭头函数的讨论中,几乎所有的例子都是简短的单语句工具,比如那些作为回调传递给各种工具的箭头函数。例如:

var a = [1,2,3,4,5];

a = a.map( v => v * 2 );

console.log( a );               // [2,4,6,8,10]

在这些情况下,你的内联函数表达式很适合这种在一个单独语句中快速计算并返回结果的模式,对于更繁冗的function关键字和语法来说箭头函数确实看起来是一个很吸人,而且轻量的替代品。

大多数人看着这样简洁的例子都倾向于发出“哦……!啊……!”的感叹,就像我想象中你刚刚做的那样!

然而我要警示你的是,在我看来,使用箭头函数的语法代替普通的,多语句函数,特别是那些可以被自然地表达为函数声明的函数,是某种误用。

回忆本章早前的字符串字面量标签函数dollabillsyall(..) —— 让我们将它改为使用=>语法:

var dollabillsyall = (strings, ...values) =>
    strings.reduce( (s,v,idx) => {
        if (idx > 0) {
            if (typeof values[idx-1] == "number") {
                // look, also using interpolated
                // string literals!
                s += `$${values[idx-1].toFixed( 2 )}`;
            }
            else {
                s += values[idx-1];
            }
        }

        return s + v;
    }, "" );

在这个例子中,我做的唯一修改是删除了functionreturn,和一些{ .. },然后插入了=>和一个var。这是对代码可读性的重大改进吗?呵呵。

实际上我会争论,缺少return和外部的{ .. }在某种程度上模糊了这样的事实:reduce(..)调用是函数dollabillsyall(..)中唯一的语句,而且它的结果是这个调用的预期结果。另外,那些受过训练而习惯于在代码中搜索function关键字来寻找作用域边界的眼睛,现在需要搜索=>标志,在密集的代码中这绝对会更加困难。

虽然不是一个硬性规则,但是我要说从=>箭头函数转换得来的可读性,与被转换的函数长度成反比。函数越长,=>能帮的忙越少;函数越短,=>的闪光之处就越多。

我觉得这样做更明智也更合理:在你需要短的内联函数表达式的地方采用=>,但保持你的一般长度的主函数原封不动。

不只是简短的语法,而是this

曾经集中在=>上的大多数注意力都是它通过在你的代码中除去functionreturn,和{ .. }来节省那些宝贵的击键。

但是至此我们一直忽略了一个重要的细节。我在这一节最开始的时候说过,=>函数与this绑定行为密切相关。事实上,=>箭头函数 主要的设计目的 就是以一种特定的方式改变this的行为,解决在this敏感的编码中的一个痛点。

节省击键是掩人耳目的东西,至多是一个误导人的配角。

让我们重温本章早前的另一个例子:

var controller = {
    makeRequest: function(..){
        var self = this;

        btn.addEventListener( "click", function(){
            // ..
            self.makeRequest(..);
        }, false );
    }
};

我们使用了黑科技var self = this,然后引用了self.makeRequest(..),因为在我们传递给addEventListener(..)的回调函数内部,this绑定将与makeRequest(..)本身中的this绑定不同。换句话说,因为this绑定是动态的,我们通过self变量退回到了可预测的词法作用域。

在这其中我们终于可以看到=>箭头函数主要的设计特性了。在箭头函数内部,this绑定不是动态的,而是词法的。在前一个代码段中,如果我们在回调里使用一个箭头函数,this将会不出所料地成为我们希望它成为的东西。

考虑如下代码:

var controller = {
    makeRequest: function(..){
        btn.addEventListener( "click", () => {
            // ..
            this.makeRequest(..);
        }, false );
    }
};

前面代码段的箭头函数中的词法this现在指向的值与外围的makeRequest(..)函数相同。换句话说,=>var self = this的语法上的替代品。

var self = this(或者,另一种选择是,.bind(this)调用)通常可以帮忙的情况下,=>箭头函数是一个基于相同原则的很好的替代操作。听起来很棒,是吧?

没那么简单。

如果=>取代var self = this.bind(this)可以工作,那么猜猜=>用于一个 不需要 var self = this就能工作的this敏感的函数会发生么?你可能会猜到它将会把事情搞砸。没错。

考虑如下代码:

var controller = {
    makeRequest: (..) => {
        // ..
        this.helper(..);
    },
    helper: (..) => {
        // ..
    }
};

controller.makeRequest(..);

虽然我们以controller.makeRequest(..)的方式进行了调用,但是this.helper引用失败了,因为这里的this没有像平常那样指向controller。那么它指向哪里?它通过词法继承了外围的作用域中的this。在前面的代码段中,它是全局作用域,this指向了全局作用域。呃。

除了词法的this以外,箭头函数还拥有词法的arguments —— 它们没有自己的arguments数组,而是从它们的上层继承下来 —— 同样还有词法的supernew.target(参见第三章的“类”)。

所以,关于=>在什么情况下合适或不合适,我们现在可以推论出一组更加微妙的规则:

  • 如果你有一个简短的,单语句内联函数表达式,它唯一的语句是某个计算后的值的return语句,并且 这个函数没有在它内部制造一个this引用,并且 没有自引用(递归,事件绑定/解除),并且 你合理地预期这个函数绝不会变得需要this引用或自引用,那么你就可能安全地将它重构为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的var self = this黑科技或者.bind(this)调用来确保正确的this绑定,那么这个内部函数表达式就可能安全地变为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的类似于var args = Array.prototype.slice.call(arguments)这样的东西来制造一个arguments的词法拷贝,那么这个内部函数就可能安全地变为一个=>箭头函数。
  • 对于其他的所有东西 —— 普通函数声明,较长的多语句函数表达式,需要词法名称标识符进行自引用(递归等)的函数,和任何其他不符合前述性质的函数 —— 你就可能应当避免=>函数语法。

底线:=>thisarguments,和super的词法绑定有关。它们是ES6为了修正一些常见的问题而被有意设计的特性,而不是为了修正bug,怪异的代码,或者错误。

不要相信任何说=>主要是,或者几乎是,为了减少几下击键的炒作。无论你是省下还是浪费了这几下击键,你都应当确切地知道你打入的每个字母是为了做什么。

提示: 如果你有一个函数,由于上述各种清楚的原因而不适合成为一个=>箭头函数,但同时它又被声明为一个对象字面量的一部分,那么回想一下本章早先的“简约方法”,它有简短函数语法的另一种选择。

对于如何/为何选用一个箭头函数,如果你喜欢一个可视化的决策图的话:

[图片上传失败...(image-27a646-1515411105375)]

for..of循环

伴随着我们熟知的JavaScriptforfor..in循环,ES6增加了一个for..of循环,它循环遍历一组由一个 迭代器(iterator) 产生的值。

你使用for..of循环遍历的值必须是一个 可迭代对象(iterable),或者它必须是一个可以被强制转换/封箱(参见本系列的 类型与文法)为一个可迭代对象的值。一个可迭代对象只不过是一个可以生成迭代器的对象,然后由循环使用这个迭代器。

让我们比较for..offor..in来展示它们的区别:

var a = ["a","b","c","d","e"];

for (var idx in a) {
    console.log( idx );
}
// 0 1 2 3 4

for (var val of a) {
    console.log( val );
}
// "a" "b" "c" "d" "e"

如你所见,for..in循环遍历数组a中的键/索引,而for.of循环遍历a中的值。

这是前面代码段中for..of的前ES6版本:

var a = ["a","b","c","d","e"],
    k = Object.keys( a );

for (var val, i = 0; i < k.length; i++) {
    val = a[ k[i] ];
    console.log( val );
}
// "a" "b" "c" "d" "e"

而这是一个ES6版本的非for..of等价物,它同时展示了手动迭代一个迭代器(见第三章的“迭代器”):

var a = ["a","b","c","d","e"];

for (var val, ret, it = a[Symbol.iterator]();
    (ret = it.next()) && !ret.done;
) {
    val = ret.value;
    console.log( val );
}
// "a" "b" "c" "d" "e"

在幕后,for..of循环向可迭代对象要来一个迭代器(使用内建的Symbol.iterator;参见第七章的“通用Symbols”),然后反复调用这个迭代器并将它产生的值赋值给循环迭代的变量。

在JavaScript标准的内建值中,默认为可迭代对象的(或提供可迭代能力的)有:

  • 数组
  • 字符串
  • Generators(见第三章)
  • 集合/类型化数组(见第五章)

警告: 普通对象默认是不适用于for..of循环的。因为他们没有默认的迭代器,这是有意为之的,不是一个错误。但是,我们不会进一步探究这其中微妙的原因。在第三章的“迭代器”中,我们将看到如何为我们自己的对象定义迭代器,这允许for..of遍历任何对象来得到我们定义的一组值。

这是如何遍历一个基本类型的字符串中的字符:

for (var c of "hello") {
    console.log( c );
}
// "h" "e" "l" "l" "o"

基本类型字符串"hello"被强制转换/封箱为等价的String对象包装器,它是默认就是一个可迭代对象。

for (XYZ of ABC)..中,XYZ子句既可以是一个赋值表达式也可以是一个声明,这与forfor..in中相同的子句一模一样。所以你可以做这样的事情:

var o = {};

for (o.a of [1,2,3]) {
    console.log( o.a );
}
// 1 2 3

for ({x: o.a} of [ {x: 1}, {x: 2}, {x: 3} ]) {
  console.log( o.a );
}
// 1 2 3

与其他的循环一样,使用breakcontinuereturn(如果是在一个函数中),以及抛出异常,for..of循环可以被提前终止。在任何这些情况下,迭代器的return(..)函数(如果存在的话)都会被自动调用,以便让迭代器进行必要的清理工作。

注意: 可迭代对象与迭代器的完整内容参见第三章的“迭代器”。

正则表达式扩展

让我们承认吧:长久以来在JS中正则表达式都没怎么改变过。所以一件很棒的事情是,在ES6中它们终于学会了一些新招数。我们将在这里简要地讲解一下新增的功能,但是正则表达式整体的话题是如此厚重,以至于如果你需要复习一下的话你需要找一些关于它的专门章节/书籍(有许多!)。

Unicode标志

我们将在本章稍后的“Unicode”一节中讲解关于Unicode的更多细节。在此,我们将仅仅简要地看一下ES6+正则表达式的新u标志,它使这个正则表达式的Unicode匹配成为可能。

JavaScript字符串通常被解释为16位字符的序列,它们对应于 基本多文种平面(Basic Multilingual Plane (BMP)) (http://en.wikipedia.org/wiki/Plane_%28Unicode%29)中的字符。但是有许多UTF-16字符在这个范围以外,而且字符串可能含有这些多字节字符。

在ES6之前,正则表达式只能基于BMP字符进行匹配,这意味着在匹配时那些扩展字符被看作是两个分离的字符。这通常不理想。

所以,在ES6中,u标志告诉正则表达式使用Unicode(UTF-16)字符的解释方式来处理字符串,这样一来一个扩展的字符将作为一个单独的实体被匹配。

警告: 尽管名字的暗示是这样,但是“UTF-16”并不严格地意味着16位。现代的Unicode使用21位,而且像UTF-8和UTF-16这样的标准大体上是指有多少位用于表示一个字符。

一个例子(直接从ES6语言规范中拿来的): 𝄞 (G大调音乐符号)是Unicode代码点U+1D11E(0x1D11E)。

如果这个字符出现在一个正则表达式范例中(比如/𝄞/),标准的BMP解释方式将认为它是需要被匹配的两个字符(0xD834和0xDD1E)。但是ES6新的Unicode敏感模式意味着/𝄞/u(或者Unicode的转义形式/\u{1D11E}/u)将会把"𝄞"作为一个单独的字符在一个字符串中进行匹配。

你可能想知道为什么这很重要。在非Unicode的BMP模式下,这个正则表达式范例被看作两个分离的字符,但它仍然可以在一个含有"𝄞"字符的字符串中找到匹配,如果你试一下就会看到:

/𝄞/.test( "𝄞-clef" );         // true

重要的是匹配的长度。例如:

/^.-clef/ .test( "𝄞-clef" );       // false
/^.-clef/u.test( "𝄞-clef" );       // true

这个范例中的^.-clef说要在普通的"-clef"文本前面只匹配一个单独的字符。在标准的BMP模式下,这个匹配会失败(因为是两个字符),但是在Unicode模式标志位u打开的情况下,这个匹配会成功(一个字符)。

另外一个重要的注意点是,u使像+*这样的量词实施于作为一个单独字符的整个Unicode代码点,而不仅仅是字符的 低端替代符(也就是符号最右边的一半)。对于出现在字符类中的Unicode字符也是一样,比如/[💩-💫]/u

注意: 还有许多关于u在正则表达式中行为的细节,对此Mathias Bynens(https://twitter.com/mathias)撰写了大量的作品(https://mathiasbynens.be/notes/es6-unicode-regex)。

粘性标志

另一个加入ES6正则表达式的模式标志是y,它经常被称为“粘性模式(sticky mode)”。粘性 实质上意味着正则表达式在它开始时有一个虚拟的锚点,这个锚点使正则表达式仅以自己的lastIndex属性所指示的位置为起点进行匹配。

为了展示一下,让我们考虑两个正则表达式,第一个没有使用粘性模式而第二个有:

var re1 = /foo/,
    str = "++foo++";

re1.lastIndex;          // 0
re1.test( str );        // true
re1.lastIndex;          // 0 —— 没有更新

re1.lastIndex = 4;
re1.test( str );        // true —— `lastIndex`被忽略了
re1.lastIndex;          // 4 —— 没有更新

关于这个代码段可以观察到三件事:

  • test(..)根本不在意lastIndex的值,而总是从输入字符串的开始实施它的匹配。
  • 因为我们的模式没有输入的起始锚点^,所以对"foo"的搜索可以在整个字符串上自由向前移动。
  • lastIndex没有被test(..)更新。

现在,让我们试一下粘性模式的正则表达式:

var re2 = /foo/y,       // <-- 注意粘性标志`y`
    str = "++foo++";

re2.lastIndex;          // 0
re2.test( str );        // false —— 在`0`没有找到“foo”
re2.lastIndex;          // 0

re2.lastIndex = 2;
re2.test( str );        // true
re2.lastIndex;          // 5 —— 在前一次匹配后更新了

re2.test( str );        // false
re2.lastIndex;          // 0 —— 在前一次匹配失败后重置

于是关于粘性模式我们可以观察到一些新的事实:

  • test(..)str中使用lastIndex作为唯一精确的位置来进行匹配。在寻找匹配时不会发生向前的移动 —— 匹配要么出现在lastIndex的位置,要么就不存在。
  • 如果发生了一个匹配,test(..)就更新lastIndex使它指向紧随匹配之后的那个字符。如果匹配失败,test(..)就将lastIndex重置为0

没有使用^固定在输入起点的普通非粘性范例可以自由地在字符串中向前移动来搜索匹配。但是粘性模式制约这个范例仅在lastIndex的位置进行匹配。

正如我在这一节开始时提到过的,另一种考虑的方式是,y暗示着一个虚拟的锚点,它位于正好相对于(也就是制约着匹配的起始位置)lastIndex位置的范例的开头。

警告: 在关于这个话题的以前的文献中,这种行为曾经被声称为y像是在范例中暗示着一个^(输入的起始)锚点。这是不准确的。我们将在稍后的“锚定粘性”中讲解更多细节。

粘性定位

对反复匹配使用y可能看起来是一种奇怪的限制,因为匹配没有向前移动的能力,你不得不手动保证lastIndex恰好位于正确的位置上。

这是一种可能的场景:如果你知道你关心的匹配总是会出现在一个数字(例如,01020,等等)倍数的位置。那么你就可以只构建一个受限的范例来匹配你关心的东西,然后在每次匹配那些固定位置之前手动设置lastIndex

考虑如下代码:

var re = /f../y,
    str = "foo       far       fad";

str.match( re );        // ["foo"]

re.lastIndex = 10;
str.match( re );        // ["far"]

re.lastIndex = 20;
str.match( re );        // ["fad"]

然而,如果你正在解析一个没有像这样被格式化为固定位置的字符串,在每次匹配之前搞清楚为lastIndex设置什么东西的做法可能会难以维系。

这里有一个微妙之处要考虑。y要求lastIndex位于发生匹配的准确位置。但它不严格要求 来手动设置lastIndex

取而代之的是,你可以用这样的方式构建你的正则表达式:它们在每次主匹配中都捕获你所关心的东西的前后所有内容,直到你想要进行下一次匹配的东西为止。

因为lastIndex将被设置为一个匹配末尾之后的下一个字符,所以如果你已经匹配了到那个位置的所有东西,lastIndex将总是位于下次y范例开始的正确位置。

警告: 如果你不能像这样足够范例化地预知输入字符串的结构,这种技术可能不合适,而且你可能不应使用y

拥有结构化的字符串输入,可能是y能够在一个字符串上由始至终地进行反复匹配的最实际场景。考虑如下代码:

var re = /\d+\.\s(.*?)(?:\s|$)/y
    str = "1. foo 2. bar 3. baz";

str.match( re );        // [ "1. foo ", "foo" ]

re.lastIndex;           // 7 —— 正确位置!
str.match( re );        // [ "2. bar ", "bar" ]

re.lastIndex;           // 14 —— 正确位置!
str.match( re );        // ["3. baz", "baz"]

这能够工作是因为我事先知道输入字符串的结构:总是有一个像"1. "这样的数字的前缀出现在期望的匹配("foo",等等)之前,而且它后面要么是一个空格,要么就是字符串的末尾($锚点)。所以我构建的正则表达式在每次主匹配中捕获了所有这一切,然后我使用一个匹配分组( )使我真正关心的东西被方便地分离出来。

在第一次匹配("1. foo ")之后,lastIndex7,它已经是开始下一次匹配"2. bar "所需的位置了,如此类推。

如果你要使用粘性模式y进行反复匹配,那么你就可能想要像我们刚刚展示的那样寻找一个机会自动地定位lastIndex

粘性对比全局

一些读者可能意识到,你可以使用全局匹配标志位gexec(..)方法来模拟某些像lastIndex相对匹配的东西,就像这样:

var re = /o+./g,        // <-- 看,`g`!
    str = "foot book more";

re.exec( str );         // ["oot"]
re.lastIndex;           // 4

re.exec( str );         // ["ook"]
re.lastIndex;           // 9

re.exec( str );         // ["or"]
re.lastIndex;           // 13

re.exec( str );         // null —— 没有更多的匹配了!
re.lastIndex;           // 0 —— 现在重新开始!

虽然使用exec(..)g范例确实从lastIndex的当前值开始它们的匹配,而且也在每次匹配(或失败)之后更新lastIndex,但这与y的行为不是相同的东西。

注意前面代码段中被第二个exec(..)调用匹配并找到的"ook",被定位在位置6,即便在这个时候lastIndex4(前一次匹配的末尾)。为什么?因为正如我们前面讲过的,非粘性匹配可以在它们的匹配过程中自由地向前移动。一个粘性模式表达式在这里将会失败,因为它不允许向前移动。

除了也许不被期望的向前移动的匹配行为以外,使用g代替y的另一个缺点是,g改变了一些匹配方法的行为,比如str.match(re)

考虑如下代码:

var re = /o+./g,        // <-- 看,`g`!
    str = "foot book more";

str.match( re );        // ["oot","ook","or"]

看到所有的匹配是如何一次性地被返回的吗?有时这没问题,但有时这不是你想要的。

test(..)match(..)这样的工具一起使用,粘性标志位y将给你一次一个的推进式的匹配。只要保证每次匹配时lastIndex总是在正确的位置上就行!

锚定粘性

正如我们早先被警告过的,将粘性模式认为是暗含着一个以^开头的范例是不准确的。在正则表达式中锚点^拥有独特的含义,它 没有 被粘性模式改变。^总是 一个指向输入起点的锚点,而且 以任何方式相对于lastIndex

在这个问题上,除了糟糕/不准确的文档,一个在Firefox中进行的老旧的前ES6粘性模式实验不幸地加深了这种困惑,它确实 曾经 使^相对于lastIndex,所以这种行为曾经存在了许多年。

ES6选择不这么做。^在一个范例中绝对且唯一地意味着输入的起点。

这样的后果是,一个像/^foo/y这样的范例将总是仅在一个字符串的开头找到"foo"匹配,如果它被允许在那里匹配的话。如果lastIndex不是0,匹配就会失败。考虑如下代码:

var re = /^foo/y,
    str = "foo";

re.test( str );         // true
re.test( str );         // false
re.lastIndex;           // 0 —— 失败之后被重置

re.lastIndex = 1;
re.test( str );         // false —— 由于定位而失败
re.lastIndex;           // 0 —— 失败之后被重置

底线:y^lastIndex > 0是一种不兼容的组合,它将总是导致失败的匹配。

注意: 虽然y不会以任何方式改变^的含义,但是多行模式m,这样^就意味着输入的起点 或者 一个换行之后的文本的起点。所以,如果你在一个范例中组合使用ym,你会在一个字符串中发现多个开始于^的匹配。但是要记住:因为它的粘性y,将不得不在后续的每次匹配时确保lastIndex被置于正确的换行的位置(可能是通过匹配到行的末尾),否者后续的匹配将不会执行。

正则表达式flags

在ES6之前,如果你想要检查一个正则表达式来看看它被施用了什么标志位,你需要将它们 —— 讽刺的是,可能是使用另一个正则表达式 —— 从source属性的内容中解析出来,就像这样:

var re = /foo/ig;

re.toString();          // "/foo/ig"

var flags = re.toString().match( /\/([gim]*)$/ )[1];

flags;                  // "ig"

在ES6中,你现在可以直接得到这些值,使用新的flags属性:

var re = /foo/ig;

re.flags;               // "gi"

虽然是个细小的地方,但是ES6规范要求表达式的标志位以"gimuy"的顺序罗列,无论原本的范例中是以什么顺序指定的。这就是出现/ig"gi"的区别的原因。

是的,标志位被指定和罗列的顺序无所谓。

ES6的另一个调整是,如果你向构造器RegExp(..)传递一个既存的正则表达式,它现在是flags敏感的:

var re1 = /foo*/y;
re1.source;                         // "foo*"
re1.flags;                          // "y"

var re2 = new RegExp( re1 );
re2.source;                         // "foo*"
re2.flags;                          // "y"

var re3 = new RegExp( re1, "ig" );
re3.source;                         // "foo*"
re3.flags;                          // "gi"

在ES6之前,构造re3将抛出一个错误,但是在ES6中你可以在复制时覆盖标志位。

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

推荐阅读更多精彩内容