深入理解 ES6:https://oshotokill.gitbooks.io/understandinges6-simplified-chinese/content/?q=

函数

参数默认值如何影响 arguments 对象:


在 ES5 的
非严格模式下, arguments 对象会反映出具名参数的变化:**

  1. function mixArgs(first, second) {
  2. console.log(first === arguments[0]);
  3. console.log(second === arguments[1]);
  4. first = "c";
  5. second = "d";
  6. console.log(first === arguments[0]);
  7. console.log(second === arguments[1]);
  8. }
  9. mixArgs("a", "b");
  10. // true
  11. // true
  12. // true
  13. // true

在非严格模式下, arguments 对象总是会被更新以反映出具名参数的变化。因此当 first 与 second 变量被赋予新值时, arguments[0] 与 arguments[1] 也就相应地更新了,使得这里所有的 === 比较的结果都为 true 。

然而在 ES5 的严格模式下,关于 arguments 对象的这种混乱情况被消除了,它不再反映出具名参数的变化。在严格模式下重新使用上例中的函数:

  1. function mixArgs(first, second) {
  2. "use strict";
  3. console.log(first === arguments[0]);
  4. console.log(second === arguments[1]);
  5. first = "c";
  6. second = "d";
  7. console.log(first === arguments[0]);
  8. console.log(second === arguments[1]);
  9. }
  10. mixArgs("a", "b");
  11. // true
  12. // true
  13. // false
  14. // false

在使用 ES6 参数默认值的函数中, arguments 对象的表现总是会与 ES5 的严格模式一致,无论此时函数是否明确运行在严格模式下。参数默认值的存在触发了 arguments 对象与具名参数的分离。这是个细微但重要的细节,因为 arguments 对象的使用方式发生了变化。研究如下代码:

  1. // 非严格模式
  2. function mixArgs(first, second = "b") {
  3. console.log(arguments.length);
  4. console.log(first === arguments[0]);
  5. console.log(second === arguments[1]);
  6. first = "c";
  7. second = "d";
  8. console.log(first === arguments[0]);
  9. console.log(second === arguments[1]);
  10. }
  11. mixArgs("a");
  12. // 1
  13. // true
  14. // false
  15. // false
  16. // false

本例中 arguments.length 的值为 1 ,因为只给 mixArgs() 传递了一个参数。这也意味着 arguments[1] 的值是 undefined ,符合将单个参数传递给函数时的预期;这同时意味着 first 与 arguments[0] 是相等的。改变 first 和 second 的值不会对 arguments 对象造成影响,无论是否在严格模式下,所以你可以始终依据 arguments 对象来反映初始调用状态。

参数默认值表达式

  1. let value = 5;
  2. function getValue() {
  3. return value++;
  4. }
  5. function add(first, second = getValue()) {
  6. return first + second;
  7. }
  8. console.log(add(1, 1)); // 2
  9. console.log(add(1)); // 6
  10. console.log(add(1)); // 7

本例中 value 的初始值是 5 ,并且会随着对 getValue() 的每次调用而递增。首次调用 add(1) 返回的值为 6 ,再次调用则返回 7 ,因为 value 的值已经被增加了。由于 second 参数的默认值总是在 add() 函数被调用,而且未提供第二个参数时的情况下才被计算,因此就能随时更改该参数的值。

函数名

  1. var doSomething = function doSomethingElse() {
  2. // ...
  3. };
  4. var person = {
  5. get firstName() {
  6. return "Nicholas";
  7. },
  8. sayName: function() {
  9. console.log(this.name);
  10. }
  11. };
  12. console.log(doSomething.name); // "doSomethingElse"
  13. console.log(person.sayName.name); // "sayName"
  14. var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
  15. console.log(descriptor.get.name); // "get firstName"

本例中的 doSomething.name 的值是 “doSomethingElse” ,因为该函数表达式自己拥有一个名称,并且此名称的优先级要高于赋值目标的变量名。 person.sayName() 的 name 属性值是 “sayName” ,正如对象字面量指定的那样。类似的, person.firstName 实际是个 getter 函数,因此它的名称是 “get firstName” ,以标明它的特征;同样, setter 函数也会带有 “set” 的前缀( getter 与 setter 函数都必须用 Object.getOwnPropertyDescriptor() 来检索)。

函数名称还有另外两个特殊情况。使用 bind() 创建的函数会在名称属性值之前带有 “bound” 前缀;而使用 Function 构造器创建的函数,其名称属性则会有 “anonymous” 前缀,正如此例:

  1. var doSomething = function() {
  2. // ...
  3. };
  4. console.log(doSomething.bind().name); // "bound doSomething"
  5. console.log((new Function()).name); // "anonymous"

需要注意的是,函数的 name 属性值未必会关联到同名变量。 name 属性是为了在调试 时获得有用的相关信息,所以不能用 name 属性值去获取对函数的引用。

明确函数的双重用途

JS 为函数提供了两个不同的内部方法: [[Call]] 与 [[Construct]] ( [[Construct]] 就是指构造函数本身。) 。当函数未使用 new 进行调用时, [[call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new 进行调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新的对象并且使用该新目标作为 this 去执行函数体拥有 [[Construct]] 方法的函数被称为构造器。

记住并不是所有函数都拥有 [[Construct]] 方法,因此不是所有函数都可以用 new 来调用。

如何检测函数是否通过 new 调用

在 ES5 中判断函数是不是使用了 new 来调用(即作为构造器),最流行的方式是使用
instanceof ,例如:

  1. function Person(name) {
  2. if (this instanceof Person) {
  3. this.name = name; // 使用 new
  4. } else {
  5. throw new Error("You must use new with Person.");
  6. }
  7. }
  8. var person = new Person("Nicholas");
  9. var notAPerson = Person("Nicholas"); // 抛出错误

此处对 this 值进行了检查,来判断其是否为构造器的一个实例:若是,正常继续执行;否则抛出错误。这能奏效是因为 [[Construct]] 方法创建了 Person 的一个新实例并将其赋值给 this 。可惜的是,该方法并不绝对可靠,因为在不使用 new 的情况下 this 仍然可能是 Person 的实例,正如下例:

  1. function Person(name) {
  2. if (this instanceof Person) {
  3. this.name = name; // 使用 new
  4. } else {
  5. throw new Error("You must use new with Person.");
  6. }
  7. }
  8. var person = new Person("Nicholas");
  9. var notAPerson = Person.call(person, "Michael"); // 奏效了!

调用 Person.call() 并将 person 变量作为第一个参数传入,这意味着将 Person 内部的 this 设置为了 person 。对于该函数来说,没有任何方法能将这种方式与使用 new 调用区分开来。

new.target 元属性:
为了解决这个问题, ES6 引入了 new.target 元属性。元属性指的是“非对象”(例如 new )上的一个属性,并提供关联到它的目标的附加信息。当函数的 [[Construct]] 方法被调用时, new.target 会被填入 new 运算符的作用目标,该目标通常是新创建的对象实例的构造器,并且会成为函数体内部的 this 值。而若 [[Call]] 被执行, new.target 的值则会是 undefined 。

  1. function Person(name) {
  2. if (typeof new.target !== "undefined") {
  3. this.name = name; // 使用 new
  4. } else {
  5. throw new Error("You must use new with Person.");
  6. }
  7. }
  8. var person = new Person("Nicholas");
  9. var notAPerson = Person.call(person, "Michael"); // 出错!

块级函数

  1. // ES6 behavior
  2. if (true) {
  3. console.log(typeof doSomething); // "function"
  4. function doSomething() {
  5. // ...
  6. }
  7. doSomething();
  8. }
  9. console.log(typeof doSomething); // "function"

在 es5 严格模式下使用块级函数会报错,但在 es 6中会被视为块级声明,并允许它在定义所在的代码块内部被访问。

es6 非严格模式下:

  1. // ES6 behavior
  2. // 注意这是在非严格模式下
  3. if (true) {
  4. console.log(typeof doSomething); // "function"
  5. function doSomething() {
  6. // ...
  7. }
  8. doSomething();
  9. }
  10. console.log(typeof doSomething); // "function"

ES6 在非严格模式下同样允许使用块级函数,但行为有细微不同。块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的顶部。

es6 严格模式下:

  1. "use strict";
  2. if (true) {
  3. console.log(typeof doSomething); // "function"
  4. function doSomething() {
  5. // ...
  6. }
  7. doSomething();
  8. }
  9. console.log(typeof doSomething); // "undefined"

块级函数会被提升到定义所在的代码块的顶部,因此 typeof doSomething 会返回 “function” ,即便该检查位于此函数定义位置之前。一旦 if 代码块执行完毕,doSomething() 也就不复存在。

箭头函数

ES6 最有意思的一个新部分就是箭头函数( arrow function )。箭头函数正如名称所示那样使用一个“箭头”( => )来定义,但它的行为在很多重要方面与传统的 JS 函数不同:

  • 没有 this 、 super 、 arguments ,也没有 new.target 绑定: this 、 super 、arguments 、以及函数内部的 new.target 的值由所在的、最靠近的非箭头函数来决定。
  • 不能被使用 new 调用: 箭头函数没有 [[Construct]] 方法,因此不能被用为构造函数,使用 new 调用箭头函数会抛出错误。
  • 没有原型: 既然不能对箭头函数使用 new ,那么它也不需要原型,也就是没有 prototype 属性。
  • 不能更改 this : this 的值在函数内部不能被修改,在函数的整个生命周期内其值会保持不变。
  • 没有 arguments 对象: 既然箭头函数没有 arguments 绑定,你必须依赖于具名参数或剩余参数来访问函数的参数。
  • 不允许重复的具名参数: 箭头函数不允许拥有重复的具名参数,无论是否在严格模式下;而相对来说,传统函数只有在严格模式下才禁止这种重复。

尾调用

尾调用优化允许某些函数的调用被优化,以保持更小的调用栈、使用更少的内存,并防止堆栈溢出。当能进行安全优化时,它会由引擎自动应用。不过你可以考虑重写递归函数,以便能够利用这种优化。

在 ES6 中对函数最有趣的改动或许就是一项引擎优化,它改变了尾部调用的系统。尾调用( tail call )指的是调用函数的语句是另一个函数的最后语句,就像这样:

  1. function doSomething() {
  2. return doSomethingElse(); // 尾调用
  3. }

在 ES5 引擎中实现的尾调用,其处理就像其他函数调用一样:一个新的栈帧( stack frame )被创建并推到调用栈之上,用于表示该次函数调用。这意味着之前每个栈帧都被保留在内存中,当调用栈太大时会出问题。

es6 有何不同?

ES6 在
严格模式下力图为特定尾调用减少调用栈的大小(非严格模式的尾调用则保持不变)。当满足以下条件时,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧**:

  1. 尾调用不能引用当前栈帧中的变量(意味着该函数不能是闭包);
    2. 进行尾调用的函数在尾调用返回结果后不能做额外操作
    3. 尾调用的结果作为当前函数的返回值

作为一个例子,下面代码满足了全部三个条件,因此能被轻易地优化:

  1. "use strict";
  2. function doSomething() {
  3. // 被优化
  4. return doSomethingElse();
  5. }

下面的几种情况都是不允许的:

  1. "use strict";
  2. function doSomething() {
  3. // 未被优化:缺少 return
  4. doSomethingElse();
  5. }
  6. function doSomething() {
  7. // 未被优化:调用并不在尾部
  8. var result = doSomethingElse();
  9. return result;
  10. }
  11. function doSomething() {
  12. var num = 1,
  13. func = () => num;
  14. // 未被优化:此函数是闭包
  15. return func();
  16. }


对象

自有属性的枚举顺序

ES6 则严格定义了对象自有属性在被枚举时返回的顺序。这对 Object.getOwnPropertyNames() 与
Reflect.ownKeys (详见第十二章)如何返回属性造成了影响,还同样影响了 Object.assign() 处理属性的顺序。

for-in 循环的枚举顺序仍未被明确规定,因为并非所有的 JS 引擎都采用相同的方式。 而 Object.keys() 和 JSON.stringify() 也使用了与 for-in 一样的枚举顺序。

自有属性枚举时基本顺序如下:
1. 所有的数字类型键,按升序排列。
2. 所有的字符串类型键,按被添加到对象的顺序排列。
3. 所有的符号类型(详见第六章)键,也按添加顺序排列。

  1. var obj = {
  2. a: 1,
  3. 0: 1,
  4. c: 1,
  5. 2: 1,
  6. b: 1,
  7. 1: 1
  8. };
  9. obj.d = 1;
  10. console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"

Object.getOwnPropertyNames() 方法按 0 、 1 、 2 、 a 、 c 、 b 、 d 的顺序返回了 obj 对象的属性。注意,数值类型的键会被合并并排序,即使这未遵循在对象字面量中的顺序。字符串类型的键会跟在数值类型的键之后,按照被添加到 obj 对象的顺序,在对象字面量中定义的键会首先出现,接下来是此后动态添加到对象的键。

更强大的原型

对象原型的实际值被存储在一个内部属性 [[Prototype]] 上, Object.getPrototypeOf() 方法会返回此属性存储的值,而 Object.setPrototypeOf() 方法则能够修改该值。不过,使用 [[Prototype]] 属性的方式还不止这些。

使用 super 引用的简单原型访问

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. let dog = {
  7. getGreeting() {
  8. return "Woof";
  9. }
  10. };
  11. let friend = {
  12. // 使用 super 这里必须是 es6 的简写写法,不能是 es5 的这种:getGreeting: function() {}
  13. getGreeting() {
  14. return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!";
  15. // 等价于下面的 super
  16. // return super.getGreeting() + ", hi!";
  17. }
  18. };
  19. // 将原型设置为 person
  20. Object.setPrototypeOf(friend, person);
  21. console.log(friend.getGreeting()); // "Hello, hi!"
  22. console.log(Object.getPrototypeOf(friend) === person); // true
  23. // 将原型设置为 dog
  24. Object.setPrototypeOf(friend, dog);
  25. console.log(friend.getGreeting()); // "Woof, hi!"
  26. console.log(Object.getPrototypeOf(friend) === dog); // true

本例中 friend 上的 getGreeting() 调用了对象上的同名方法。 Object.getPrototypeOf() 方法确保了能调用正确的原型,并在其返回结果上附加了一个字符串;而附加的 call(this) 代码则能确保正确设置原型方法内部的 this 值。

调用原型上的方法时要记住使用 Object.getPrototypeOf() 与 .call(this) ,这有点复杂难懂,因此 ES6 才引入了 super 。简单来说, super 是指向当前对象的原型的一个指针,实际上就是 Object.getPrototypeOf(this) 的值。

调用 super.getGreeting() 等同于在上例的环境中使用 Object.getPrototypeOf(this).getGreeting.call(this) 。类似的,你能使用 super 引用来调用对象原型上的任何方法,只要这个引用是位于 es6 简写的方法之内。

正式的“方法”定义

在 ES6 之前,“方法”的概念从未被正式定义,它此前仅指对象的函数属性(而非数据属性)。 ES6 则正式做出了定义:方法是一个拥有 [[HomeObject]] 内部属性的函数,此内部属性指向该方法所属的对象。研究以下例子:

  1. let person = {
  2. // 方法
  3. getGreeting() {
  4. return "Hello";
  5. }
  6. };
  7. // 并非方法
  8. function shareGreeting() {
  9. return "Hi!";
  10. }

此例定义了拥有单个 getGreeting() 方法的 person 对象。由于 getGreeting() 被直接赋给了一个对象,它的 [[HomeObject]] 属性值就是 person 。 而另一方面, shareGreeting() 函数没有被指定 [[HomeObject]] 属性,因为它在被创建时并没有赋给一个对象。大多数情况下,这种差异并不重要,然而使用 super 引用时就完全不同了。

任何对 super 的引用都会使用 [[HomeObject]] 属性来判断要做什么。第一步是在 [[HomeObject]] 上调用 Object.getPrototypeOf() 来获取对原型的引用;接下来,在该原型上查找同名函数;最后,创建 this 绑定并调用该方法。这里有个例子:

  1. let person = {
  2. getGreeting() {
  3. return "Hello";
  4. }
  5. };
  6. // 原型为 person
  7. let friend = {
  8. getGreeting() {
  9. return super.getGreeting() + ", hi!";
  10. }
  11. };
  12. Object.setPrototypeOf(friend, person);
  13. console.log(friend.getGreeting()); // "Hello, hi!"

调用 friend.getGreeting() 返回了一个字符串,也就是 person.getGreeting() 的返回值与
“, hi!” 的合并结果。此时 friend.getGreeting() 的 [[HomeObject]] 值是 friend ,并且 friend 的原型是 person ,因此 super.getGreeting() 就等价于 person.getGreeting.call(this) 。

可以看到我们可以使用 super 关键字来调用对象原型上的方法,所调用的方法会被设置好其内部的 this 绑定,以自动使用该 this 值来进行工作。

类声明:

  1. class PersonClass {
  2. // 等价于 PersonType 构造器
  3. constructor(name) {
  4. this.name = name;
  5. }
  6. // 等价于 PersonType.prototype.sayName
  7. sayName() {
  8. console.log(this.name);
  9. }
  10. }
  11. let person = new PersonClass("Nicholas");
  12. person.sayName(); // 输出 "Nicholas"

es6 类与 es5 创建自定义类型的一些区别:
**
1. 类声明不会被提升,这与函数定义不同。类声明的行为与 let 相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。
2. 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
3. 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用 Object.defineProperty() 才能将方法改变为不可枚举。
4. 类的所有方法内部都没有 [[Construct]] ,因此使用 new 来调用它们会抛出错误。
5. 调用类构造器时不使用 new ,会抛出错误。
6. 试图在类的方法内部重写类名,会抛出错误。

es 6 类转换成下面的 es 5 的写法:

  1. // 直接等价于 PersonClass
  2. let PersonType2 = (function() {
  3. "use strict";
  4. const PersonType2 = function(name) {
  5. // 确认函数被调用时使用了 new
  6. if (typeof new.target === "undefined") {
  7. throw new Error("Constructor must be called with new.");
  8. }
  9. this.name = name;
  10. };
  11. Object.defineProperty(PersonType2.prototype, "sayName", {
  12. value: function() {
  13. // 确认函数被调用时没有使用 new
  14. if (typeof new.target !== "undefined") {
  15. throw new Error("Method cannot be called with new.");
  16. }
  17. console.log(this.name);
  18. },
  19. enumerable: false,
  20. writable: true,
  21. configurable: true
  22. });
  23. return PersonType2;
  24. })();

上面的实现注意三点:

  • 首先要注意这里有两个 PersonType2 声明:一个在外部作用域的 let 声明,一个在 IIFE 内部的 const 声明。这就是为何在类(类的内部)的方法不能对类名进行重写、而类外部的代码则被允许。
  1. class Foo {
  2. constructor() {
  3. Foo = "bar"; // 执行时抛出错误
  4. }
  5. }
  6. // 但在类声明之后没问题
  7. Foo = "baz";
  • 另外,构造器函数检查了 new.target ,以保证被调用时使用了 new ,否则就抛出错误。

  • 接下来,sayName() 方法被定义为不可枚举,并且此方法也检查了 new.target ,它则要保证在被调用时没有使用 new 。最后一步是将构造器函数返回出去。

类表达式:

  1. let PersonClass = class {
  2. // 等价于 PersonType 构造器
  3. constructor(name) {
  4. this.name = name;
  5. }
  6. // 等价于 PersonType.prototype.sayName
  7. sayName() {
  8. console.log(this.name);
  9. }
  10. };

类表达式不需要在 class 关键字后使用标识符。除了语法差异,类表达式的功能等价于类声明。

同函数是一级公民一样,类也是一级公民:

  1. function createObject(classDef) {
  2. return new classDef();
  3. }
  4. let obj = createObject(
  5. class {
  6. sayHi() {
  7. console.log("Hi!");
  8. }
  9. }
  10. );
  11. obj.sayHi(); // "Hi!"

此例中的 createObject() 函数被调用时接收了一个匿名类表达式作为参数,使用 new 创建了该类的一个实例,并将其返回出来。随后变量 obj 储存了所返回的实例。

类表达式的另一个有趣用途是立即调用类构造器,以创建单例( Singleton )。为此,你必须使用 new 来配合类表达式,并在表达式后面添加括号。例如:

  1. let person = new (class {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayName() {
  6. console.log(this.name);
  7. }
  8. })("Nicholas");
  9. person.sayName(); // "Nicholas"

此处创建了一个匿名类表达式,并立即执行了它。此模式允许你使用类语法来创建单例,从而不留下任何可被探查的类引用(回忆一下 PersonClass 的例子,匿名类表达式只在类的内部创建了绑定,而外部无绑定)。类表达式后面的圆括号表示要调用前面的函数,并且还允许传入参数。

从表达式中派生类

在 ES6 中派生类的最强大能力,或许就是能够从表达式中派生类。只要一个表达式能够返回一个具有 [[Construct]] 属性以及原型的函数,你就可以对其使用 extends 。

extends 后面能接受任意类型的表达式(但必须返回具有[[Construct]]函数,否则报错),这带来了巨大可能性,例如动态地决定所要继承的类。例如:

  1. let SerializableMixin = {
  2. serialize() {
  3. return JSON.stringify(this);
  4. }
  5. };
  6. let AreaMixin = {
  7. getArea() {
  8. return this.length * this.width;
  9. }
  10. };
  11. function mixin(...mixins) {
  12. var base = function() {};
  13. Object.assign(base.prototype, ...mixins);
  14. return base;
  15. }
  16. class Square extends mixin(AreaMixin, SerializableMixin) {
  17. constructor(length) {
  18. super();
  19. this.length = length;
  20. this.width = length;
  21. }
  22. }
  23. var x = new Square(3);
  24. console.log(x.getArea()); // 9
  25. console.log(x.serialize()); // "{"length":3,"width":3}"

此例使用了混入( mixin )而不是传统继承。 mixin() 函数接受代表混入对象的任意数量的参数,它创建了一个名为 base 的函数,并将每个混入对象的属性都赋值到新函数的原型上。此函数随后被返回,于是 Square 就能够对其使用 extends 关键字了。注意由于仍然使用了 extends ,你就必须在构造器内调用 super() 。

Square 的实例既有来自 AreaMixin 的 getArea() 方法,又有来自 SerializableMixin 的 serialize() 方法,这是通过原型继承实现的。 mixin() 函数使用了混入对象的所有自有属性,动态地填充了新函数的原型(记住:若多个混入对象拥有相同的属性,则只有最后添加的属性会被保留)。

任意表达式都能在 extends 关键字后使用,但并非所有表达式的结果都是一个有效的类。特别的,下列表达式类型会导致错误: null ; 生成器函数(详见第八章)。 试图使用结果为上述值的表达式来创建一个新的类实例,都会抛出错误,因为不存在[[Construct]] 可供调用。