[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 {…} 构造器内部干了啥?
- 创建一个以
User
为名称的函数, 这是类声明的结果(函数代码来自constructor
中) - 储存所有方法, 例如
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();
两者存在重大差异
-
首先,通过
class
创建的函数是由特殊内部属性标记的[[FunctionKind]]:"classConstructor"
。不像普通函数,调用类构造器时必须要用new
关键词:class User {
constructor() {}
}
alert(typeof User); // function
User(); // Error: 没有 ‘new’ 关键词,类构造器 User 无法调用此外,大多数 JavaScript 引擎中的类构造函数的字符串表示形式都以 “class” 开头
class User {
constructor() {}
}
alert(User); // class User { ... } -
方法不可枚举。 对于
"prototype"
中的所有方法,类定义将enumerable
标记为false
。这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。
枚举实例属性时, 不会出现class方法; 而普通创建的构造函数, 枚举实例属性时会出现prototype上的方法。
-
类默认使用
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 < 0) throw 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 < 0) throw new Error("Negative water");
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
虽然这看起来有点长,但函数更灵活。他们可以接受多个参数(即使我们现在不需要它们)// 更加灵活,原来getter中不能加参数,setter中只可以加一个参数,newVal,但是使用了函数后可以自定义加任意的参数
类声明在 User.prototype
中创建 getters
和setters
,示例:
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]]
的引用
Rabbit
方法原型继承自Animal
方法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.prototype
到Animal.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
关键字提供了上述功能
- 执行
super.method(…)
调用父类方法; (借用并改造父类方法, 生成自己的方法) - 执行
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 方法
}
}
因此,箭头函数中的super
与stop()
中的是相同的,所以它能按预期工作。但如果我们在这里指定一个"普通"函数,那么将会抛出错误: (找不到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
,而并没有在原型链上向上寻找方法。
-
在
longEar.eat()
中,(**)这一行调用rabbit.eat
并且此时this=longEar
。// 在 longEar.eat() 中 this 指向 longEar
this.__proto__.eat.call(this) // (**)
// 变成了
longEar.__proto__.eat.call(this)
// 即等同于
rabbit.eat.call(this); -
之后在
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); -
…所以
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 = {
eat: function() { // eat() {...}
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
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
的方法从一个对象复制到另一个对象是不安全的。
补充:
- 箭头函数没有自己的
this
或super
,所以它们能融入到就近的上下文,像透明似的。
class Rabbit
与class 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({a: 1, b: 2}) ); // 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({a: 1, b: 2})); // Error
顺便说一下,Function.prototype
也有一些函数的通用方法,比如 call
、bind
等等。在上述的两种情况下他们都是可用的,因为对于内置的 Object
构造函数来说,Object.__proto__ === Function.prototype
。(所有函数都是默认如此)
因此class Rabbit
与class Rabbit extends Object
有两点区别
class Rabbit | class Rabbit extends Object |
---|---|
- | needs to call super() in constructor |
Rabbit.__proto__ === Function.prototype |
Rabbit.__proto__ === Object |