[ES6]ES6语法中的class、extends与super的原理

class

首先, 在JavaScript中, class类是一种函数

class User {
    constructor(name) { this.name = name; }
    sayHi() {alert(this.name);}
}

alert(typeof User); // function

class User {…} 构造器内部干了啥?

  1. 创建一个以User为名称的函数, 这是类声明的结果(函数代码来自constructor中)
  2. 储存所有方法, 例如User.prototype中的sayHi
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

class并不是JavaScript中的语法糖, 虽然我们可以在没有 class 的情况下声明同样的内容:

// 以纯函数的重写 User 类

// 1. 创建构造器函数
function User(name{
     this.name = name;
}
// * 任何函数原型默认具有构造器属性,
// 所以,我们不需要创建它

// 2. 向原型中添加方法
User.prototype.sayHi = function() {
    alert(this.name);
};

// 使用方法:
let user = new User("John");
user.sayHi();
两者存在重大差异
  1. 首先,通过 class 创建的函数是由特殊内部属性标记的 [[FunctionKind]]:"classConstructor"。不像普通函数,调用类构造器时必须要用 new 关键词:

    class User {
       constructor() {}
    }

    alert(typeof User); // function
    User(); // Error: 没有 ‘new’ 关键词,类构造器 User 无法调用

    此外,大多数 JavaScript 引擎中的类构造函数的字符串表示形式都以 “class” 开头

    class User {
     constructor() {}
    }

    alert(User); // class User { ... }
  2. 方法不可枚举。 对于 "prototype" 中的所有方法,类定义将 enumerable 标记为false

    这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。

    枚举实例属性时, 不会出现class方法; 而普通创建的构造函数, 枚举实例属性时会出现prototype上的方法。

  3. 类默认使用 use strict。 在类构造函数中的所有方法自动使用严格模式。

Getters/setters 及其他 shorthands

就像对象字面量,类可能包括 getters/setters,generators,计算属性(computed properties)等。

使用 get/set 实现 user.name 的示例:

class User {

  constructor(name) {
    // 调用 setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name too short.

除了使用getter/setter语法,大多数时候我们首选 get…/set… 函数

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0throw new Error("Negative water");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().waterAmount = 100// setter 赋值函数
class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0throw new Error("Negative water");
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

虽然这看起来有点长,但函数更灵活。他们可以接受多个参数(即使我们现在不需要它们)// 更加灵活,原来getter中不能加参数,setter中只可以加一个参数,newVal,但是使用了函数后可以自定义加任意的参数

类声明在 User.prototype 中创建 getterssetters,示例:

Object.defineProperties(User.prototype, {
  name: {
    get() {
      return this._name
    },
    set(name) {
      // ...
    }
  }
});
class属性
class User {
  name = "Anonymous";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi();

属性不在 User.prototype 内。相反它是通过 new 分别为每个对象创建的。所以,该属性永远不会在同一个类的不同对象之间共享。

总结

基本的类语法:

class MyClass {
    prop = value;  // filed 公有字段声明(通过new分别为每个对象创建)
    #prop = value; // field 私有字段声明(从类外部引用私有字段是错误的。它们只能在类里面中读取或写入。)

    static prop = value; // 静态属性(存储类级别的数据,MyClass本身的属性, 而不是定义在实例对象this上的属性, 只能通过 MyClass.prop 访问);静态属性是继承的。

    constructor(...) { // 构造器
        // ...
    }

    method(...) {} // 方法

    static method(...) {} // 静态方法被用来实现属于整个类的功能,不涉及到某个具体的类实例的功能;静态方法是继承的;

    get something(...) {} // getter 方法
    set something(...) {} // setter 方法

    [Symbol.iterator]() {} // 计算 name/symbol 名方法 // 变量做属性
}

由于extends创建了两个[[prototype]]的引用

  1. Rabbit方法原型继承自Animal方法
  2. Rabbit.prototype 原型继承自Animal.prototype

Rabbit.__proto__ === Animal,因此对于class B extends A,类B的prototype指向了A,所以如果一个字段在B中没有找到,会继续在A中查找。故而静态属性和方法都是被继承的

技术上来说,静态声明等同于直接给类本身赋值:

class MyClass {
  static property = ...;

  static method() {
    ...
  }
}
// 等同于
MyClass.property = ...
MyClass.method = ...

实例属性的新写法:

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class IncreasingCounter {
  constructor() {
    this._count = 0// (*)
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

class IncreasingCounter {
  _count = 0// (**)
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
  }
}

上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。

extends

根据规范,如果一个类继承了另一个类并且没有constructor,那么将生成以下"空" constructor:

class Rabbit extends Animal {
    // 为没有构造函数的继承类生成以下的构造函数
    constructor(...ars) {
        super(...args);
    }
}
class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }
}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbit = new Rabbit("White Rabbit");
console.log(rabbit); // console: Rabbit {speed: 0, name: "White Rabbit"}
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

extends干了啥?

通过指定"extends Animal"让 Rabbit继承自 Animal

Rabbit内部,extends关键字添加了[[Prototype]]引用: 从 Rabbit.prototypeAnimal.prototype

`extends`允许后接任何表达式(高级编程模式中用到)

类语法不仅可以指定一个类,还可以指定extends之后的任何表达式

ex.一个生成父类的函数调用

function f(phrase{
    return class {
        sayHi() { alert(phrase) }
    }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

这里是 class User继承自f("Hello")的结果

我们可以根据多种状况使用函数生成类,并继承它们,这对于高级编程模式来说可能很有用。

super

通常来说,我们不希望完全替换父类的方法,而是希望基于它做一些调整或者功能性的扩展。我们在我们的方法中做一些事情,但是在它之前/之后或在执行过程中调用父类方法。

super关键字提供了上述功能

  1. 执行 super.method(…)调用父类方法; (借用并改造父类方法, 生成自己的方法)
  2. 执行super(…)调用父类构造函数(只能在子类的构造函数中运行) (继承父类属性)
重写原型方法
class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  stop() { // (*)
    super.stop(); // 调用父类的 stop 函数
    this.hide();  // 然后隐藏
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

箭头函数没有super

如果箭头函数中,super被访问,那么则会从外部函数中获取(类似this)

class Rabbit extends Animal {
    stop() {
        setTimtout(() => super.stop(), 1000); // 1 秒后调用父类 stop 方法
    }
}

因此,箭头函数中的superstop()中的是相同的,所以它能按预期工作。但如果我们在这里指定一个"普通"函数,那么将会抛出错误: (找不到super)

class Rabbit extends Animal {
  stop() {
    setTimeout(function () super.stop() }, 1000); // Unexpected super
  }
}

代码解析会出错,报Uncaught SyntaxError: 'super' keyword unexpected here

重写构造函数

根据 规范,如果一个类继承了另一个类并且没有 constructor,那么将生成以下“空” constructor

class Rabbit extends Animal {
    // 为没有构造函数的继承类生成以下的构造函数
    constructor(...args) {
        super(...args);
    }
}

可以看到,它调用了父类的constructor, 并传递了所有的参数。

如果给继承类添加一个自定义的额构造函数

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 不生效!
let rabbit = new Rabbit("White Rabbit"10); 

报错: Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

解释下就是: 继承类的构造函数必须调用 super(...), 并且一定要在this之前调用

这是因为, 在JavaScript中,“继承类的构造函数" 与所有其他的构造函数之间存在区别。在继承类中,相应的构造函数会被标记为特殊的的内部属性[[ConstructorKind]]:"derived"

不同点在于:

  • 当一个普通构造函数执行时,它会创建一个空对象作为this并继续执行。
  • 但是当继承的构造函数执行时,它并不会做这件事。它期望父类的构造函数来完成这项工作。

因此,如果我们在继承类中构建了自己的构造函数,我们必须调用super,因为如果不这样的话this指向的对象不会被创建。并且会收到一个报错。

正确的写法;需要在使用this之前调用super()

class Rabbit extends Animal {
    constructor(name, earLength) {
        super(name);
        this.earLength = earLength;
    }
}
super内部探究: [[HomeObject]]

当一个对象方法运行时,它会将当前对象作为this,如果调用super.method(),它需要从当前的原型中调用method

super技术上的实现,首先会想到,引擎知道当前对象的this,因此它可以获取父method作为this.__proto__.method。但这个解决方法是行不通的。

让我们来说明一下这个问题。没有类,为简单起见,使用普通对象。

let animal = {
    name'Animal',
    eat() {
        alert(`${this.name} eats.`);
    }
};

let rabbit = {
    __proto__: animal,
    name'Rabbit',
    eat() {
        // 这是 super.eat() 可能运行的原因
        this.__proto__.eat.call(this); // (*)
    }
};

rabbit.eat(); // Rabbit eats

在(*)这一行,我们从原型animal,我们从原型animal上获取eat方法,并在当前对象的上下文中调用它。注意, .call(this)在这里非常重要,因为简单的调用this.__proto__.eat()将在原型的上下文中执行eat,而非当前对象。

上述代码中,我们获得了正确的父类方法。但如果在原型链上再添加一个额外的对象。这就不成立了

let animal = {
    name'Animal',
    eat() {
        alert(`${this.name} eats`);
    }
};

let rabbit = {
    __proto__: animal,
    eat() {
        this.__proto__.eat.call(this); // (*)
    }
};

let longEar = {
    __proto__: rabbit,
    eat() {
        this.__proto__.eat.call(this); // (**)
    }
};

longEar.eat(); // Error: Maxium call stack size exceeded
// InternalError: too much recursion

代码无法运行;这是由于在()和(*)这两行中,this的值都是当前对象(longEar)。

在()和(*)这两行中,this.__proto__的值是完全相同的: 都是rabbit。在这个无限循环中,它们都调用了rabbit.eat,而并没有在原型链上向上寻找方法。

  1. longEar.eat()中,(**)这一行调用rabbit.eat并且此时this=longEar

    // 在 longEar.eat() 中 this 指向 longEar
    this.__proto__.eat.call(this// (**)
    // 变成了
    longEar.__proto__.eat.call(this)
    // 即等同于
    rabbit.eat.call(this);
  2. 之后在rabbit.eat的(*)行中,我们希望将函数调用再原型链上向更高层传递,但是因为this=longEar,因此this.__proto__.eat又是rabbit.eat

    // 在 rabbit.eat() 中 this 依旧等于 longEar
    this.__proto__.eat.call(this// (*)
    // 变成了
    longEar.__proto__.eat.call(this)
    // 再次等同于
    rabbit.eat.call(this);
  3. …所以 rabbit.eat 不停地循环调用自己,因此它无法进一步地往原型链的更高层调用。

因此,super无法单独使用this来解决

[[HomeObject]]

为了提供super的解决方法,javascript为函数额外添加了一个特殊的内部属性: [[HomeObjext]]

当一个函数被定义为类或者对象方法时, 它的[[HomeObject]]属性就成为那个对象。

然后super使用它来解析父类原型和它自己的方法。

let animal = {
    name'Animal',
    eat() { // animal.eat.[[HomeObject]] == animal // (3)
        alert(`${this.name} eats.`);
    }
};

let rabbit = {
    __proto__: animal,
    name'Rabbit',
    eat() {
        super.eat(); // rabbit.eat.[[HomeObject]] == rabbit
        // rabbit.eat.[[HomeObject]].__proto__.eat.call(this); // (2)
    }
};

let longEar = {
    __proto__: rabbit,
    name'Lonet Ear',
    eat() { // longEar.eat.[[HomeObject]] == longEar
        super.eat();
        // longEar.eat.[[HomeObject]].__proto__.eat.call(this); // (1)
    }
};

// 正常运行
longEar.eat(); // alert: Lonet Ear eats.

上述代码按照预期运行,基于[[HomeObject]]运行机制。 像longEar.eat这样的方法,知道[[HomeObejct]],并且从它的原型中获取父类方法, 并没有使用 this。( 调用顺序(1) -> (2) -> (3) )

方法并不是"自由"的

通常函数都是"自由"的,并没有绑定到javascript中的对象。因此,它们可以在对象之间赋值,并且用另外一个this调用它。
[[HomeObject]]的存在违反了这个原则,因为方法记住了它们的对象[[HomeObject]]不能被修改,所以这个绑定是永久的。

在javascript语言中[[HomeObject]]仅被用于super所以,如果一个方法不使用super,那么仍然可以被视为自由且可在对象之间复制。但在super中可能出错。

let animal = {
  sayHi() {
    console.log(`I'm an animal`);
  }
};

let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    console.log("I'm a plant");
  }
};

let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();  // I'm an animal (?!?)

原因很简单:

  • 在(*)行,tree.sayHi方法从rabbit复制而来。(可能是为了避免重复代码)
  • 所以它的[[HomeObject]]rabbit,因为它是在rabbit中创建的。无法修改[[HomeObject]]
  • tree.sayHi()内具有super.sayHi()。它从rabbit中上溯,然后从animal中获取方法。
方法, 不是函数属性

[[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象来说,方法必须确切指定为 method(),而不是 "method: function()"

这个差别对我们来说可能不重要,但是对 JavaScript 来说却是非常重要的。

下面的例子中,使用非方法(non-method)语句进行比较。[[HomeObject]] 属性未设置,并且继承不起作用:

let animal = {
    eatfunction() // eat() {...}
        // ...
    }
};

let rabbit = {
    __proto__: animal,
    eatfunction() {
        super.eat();
    }
}

rabbit.eat(); // 错误调用 super(因为这里并没有 [[HomeObject]])

总结

1.扩展类: class Child extends Parent:

  • 这就意味着Child.prototype.proto将是Parent.prototype,所以方法被继承

2.重写构造函数:

  • 在使用this之前,我们必须在Child构造函数中将父构造函数调用为super()。( super(…)用来初始化继承类构造函数里的 this值,相当于手动执行了 this = Reflect.construct(super.constructor, args, new.target))

3.重写方法:

  • 我们可以在Child方法中使用super.method()来调用Parent方法;(通过方法的内部属性[[HomeObject]]实现往原型链的更高层调用)

4.内部工作:

  • 方法在内部[[HomeObject]]属性中记住它们的类/对象。这就是super如何解析父类方法的。
  • 因此,将一个带有super的方法从一个对象复制到另一个对象是不安全的。

补充:

  • 箭头函数没有自己的thissuper,所以它们能融入到就近的上下文,像透明似的。

class Rabbitclass Rabbit extends Object的区别

extends语法会设置两个原型: (结果就是,继承对于常规的和静态的方法都生效)

1.在构造函数的prototype之间设置原型(为了获取实例方法)

2.在构造函数之间会设置原型(为了获取静态方法)

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true

// 所以现在 Rabbit 对象可以通过 Rabbit 访问 Object 的静态方法,如下所示:
class Rabbit extends Object {}

// 通常我们调用 Object.getOwnPropertyNames
alert ( Rabbit.getOwnPropertyNames({a1b2}) ); // a,b (*)

但是如果我们没有声明 extends Object,那么 Rabbit.__proto__ 将不会被设置为 Object。

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)
alert( Rabbit.__proto__ === Function.prototype ); // 所有函数都是默认如此

// 报错,Rabbit 上没有对应的函数
alert ( Rabbit.getOwnPropertyNames({a1b2})); // Error

顺便说一下,Function.prototype 也有一些函数的通用方法,比如 callbind 等等。在上述的两种情况下他们都是可用的,因为对于内置的 Object 构造函数来说,Object.__proto__ === Function.prototype。(所有函数都是默认如此)

因此class Rabbitclass Rabbit extends Object有两点区别

class Rabbitclass Rabbit extends Object
- needs to call super() in constructor
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object
posted @ 2019-11-17 23:07  rencoo  阅读(2405)  评论(1编辑  收藏  举报