Appearance
Class 继承
简介
Class 可以通过 extends 关键字实现继承,让子类继承父类的属性和方法
js
class Point {}
class ColorPoint extends Point {}super 函数:可以调用父类方法
js
class Point {
/* ... */
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y) // 调用父类的constructor(x, y)
this.color = color
}
toString() {
return this.color + ' ' + super.toString() // 调用父类的toString()
}
}ES6 规定,子类必须在 constructor 方法中调用 super,否则就会报错。
原因:子类 this 对象的构造,先通过对父类构造函数,得到与父类相同的实例属性和方法,然后加工,添加自己的实例属性和方法。不调用 super 子类就得不到自己的 this 对象
ES5 继承机制:先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”
ES6 继承机制:先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”
意味父类构造必须先运行一次
js
class Foo {
constructor() {
console.log(1)
}
}
class Bar extends Foo {
constructor() {
super()
console.log(2)
}
}
const bar = new Bar()
// 1
// 2注意:子类构造函数中,调用 super()之后,才可以使用 this 关键字,否则会报错
子类没有定义 constructor 方法,会默认添加,自动调用 super 函数
js
class ColorPoint extends Point {}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args)
}
}除了私有属性和方法,父类的所有属性和方法,都会被子类继承,其中包括静态方法
原因:私有属性和方法只能在定义它的 class 里面使用
js
class Foo {
#p = 1
#m() {
console.log('hello')
}
}
class Bar extends Foo {
constructor() {
super()
console.log(this.#p) // 报错
this.#m() // 报错
}
}注:子类在父类作用域下能取到 私有属性
js
class A {
#foo = 0
static test(obj) {
console.log(#foo in obj)
}
}
class SubA extends A {}
A.test(new SubA()) // true父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性
js
class Foo {
#p = 1
getP() {
return this.#p
}
}
class Bar extends Foo {
constructor() {
super()
console.log(this.getP()) // 1
}
}Object.getPrototypeOf
Object.getPrototypeOf 方法:取子类上获取父类
js
class Point {
/*...*/
}
class ColorPoint extends Point {
/*...*/
}
Object.getPrototypeOf(ColorPoint) === Point
// truesuper 关键字
super 关键字,两个用法
- 作函数使用
super 作为函数调用时,代表父类的构造函数。
js
class A {
constructor() {
console.log(new.target.name)
}
}
class B extends A {
constructor() {
super()
// super 等效于
// A.prototype.constructor.call(this)
}
}
new A() // A
new B() // B作为函数使用,只能在构造函数中使用,其余地方使用会报错
- 作对象使用
在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
js
class A {
p() {
return 2
}
}
class B extends A {
constructor() {
super()
// A.prototype.p()
console.log(super.p()) // 2
}
}
let b = new B()super 指向父类的原型对象,所以无法调用父类实例上的方法或属性
js
class A {
constructor() {
this.p = 2
}
}
A.prototype.x = 2
class B extends A {
test() {
console.log(super.p) // undefined
console.log(super.x) // 2
}
}
let b = new B()
b.test()ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例
js
class A {
constructor() {
this.x = 1
}
print() {
console.log(this.x)
}
}
class B extends A {
constructor() {
super()
this.x = 2
}
m() {
super.print()
}
}
let b = new B()
b.m() // 2通过 super 对某个属性赋值,这时 super 中 this 也是子类实例
js
class A {
constructor() {
this.x = 1
}
}
class B extends A {
constructor() {
super()
this.x = 2
super.x = 3
console.log(super.x) // undefined
console.log(this.x) // 3
}
}
let b = new B()分析:super.x = 3 ,等同于 this.x = 3。取 super.x 时,是取 A.prototype.x,所以 undefined
super 作为对象,在静态方法中,这时 super 将指向父类,而不是父类原型对象。
js
class Parent {
static myMethod(msg) {
console.log('static', msg)
}
myMethod(msg) {
console.log('instance', msg)
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg)
}
myMethod(msg) {
super.myMethod(msg)
}
}
Child.myMethod(1) // static 1
var child = new Child()
child.myMethod(2) // instance 2子类的静态方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类,而不是子类的实例
js
class A {
constructor() {
this.x = 1
}
static print() {
console.log(this.x)
}
}
class B extends A {
constructor() {
super()
this.x = 2
}
static m() {
super.print()
}
}
B.x = 3
B.m() // 3总结:
- super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象。即在静态方法中指向静态方法,普通方法指向普通方法。
- super 中 this,普通方法指向子类实例,静态方法指向子类
注:使用 super 关键字,必须显式指定是作为函数、还是作为对象使用,否则会报错
js
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}js
class A {}
class B extends A {
constructor() {
super()
console.log(super.valueOf() instanceof B) // true
}
}
let b = new B()super.valueOf()表明 super 是一个对象,因此就不会报错。由于 super 使得 this 指向 B 的实例,所以 super.valueOf()返回的是一个 B 的实例。
对象总是继承其他对象的,在任意一个对象中,都可以使用 super 关键字
js
var obj = {
toString() {
return 'MyObject: ' + super.toString()
}
}
obj.toString() // MyObject: [object Object]类的 prototype 属性和__proto__属性
ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的 prototype 属性
Class 作为构造函数的语法糖,同时有 prototype 属性和__proto__属性,因此同时存在两条继承链
(1)子类的__proto__属性:总是指向父类
(2)子类 prototype 属性的__proto__属性,表示方法的继承,总是指向父类的 prototype 属性
js
class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true类的继承实现模式如下
js
class A {}
class B {}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype)
// B.prototype.__proto__ = A.prototype;
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A)
// B.__proto__ = A;
const b = new B()extends 关键字后面可以跟多种类型的值
第一种,子类继承 Object 类
js
class A extends Object {}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true第二种,不存在任何继承
js
class A {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // trueA 作为一个基类,就是一个普通函数,所以直接继承 Function.prototype
实例的 __proto__ 属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。子类的原型的原型,是父类的原型
js
var p1 = new Point(2, 3)
var p2 = new ColorPoint(2, 3, 'red')
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true原生构造函数的继承
原生构造函数:指语言内置的构造函数
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
原生构造函数是无法继承的,无法定义一个子类
js
function MyArray() {
Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})js
var colors = new MyArray()
colors[0] = 'red'
colors.length // 0
colors.length = 0
colors[0] // "red"问题:子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。
原因:原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性
ES5 是先新建子类的实例对象 this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。
比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
例子:让一个普通对象继承Error对象
js
var e = {}
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []想通过 Error.call(e)这种写法,让普通对象 e 具有 Error 对象的实例属性。但 Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e 本身没有任何变化。再次说明,通过 call 方法无法继承原生对象。
ES6 允许继承原生构造函数定义子类,即 ES6 可以自定义原生数据结构
原理:ES6 是先新建父类的实例对象 this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承
js
class MyArray extends Array {
constructor(...args) {
super(...args)
}
}
var arr = new MyArray()
arr[0] = 12
arr.length // 1
arr.length = 0
arr[0] // undefined例子:自定义 Error 子类
js
class ExtendableError extends Error {
constructor(message) {
super()
this.message = message
this.stack = new Error().stack
this.name = this.constructor.name
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m)
}
}
var myerror = new MyError('ll')
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
// at MyError.ExtendableError
// ...注:ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object()这种形式调用,ES6 规定 Object 构造函数会忽略参数
js
class NewObj extends Object {
constructor() {
super(...arguments)
}
}
var o = new NewObj({ attr: true })
o.attr === true // falseMixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
js
const a = {
a: 'a'
}
const b = {
b: 'b'
}
const c = { ...a, ...b } // {a: 'a', b: 'b'}多个类的接口“混入”(mix in)另一个类
js
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()) // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin) // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype) // 拷贝原型属性
}
return Mix
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
let desc = Object.getOwnPropertyDescriptor(source, key)
Object.defineProperty(target, key, desc)
}
}
}js
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}