本文主要是介绍ECMAScript 6:三、Class的继承,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
ECMAScript 6:三、Class的继承
1 前言
ES6学习笔记3:Class的继承。
学习地址:
https://es6.ruanyifeng.com/#docs/class-extends
2 笔记
(1)简介
Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
class People {}class Xiaoxu extends People {}
上面示例中,People是父类,Xiaoxu是子类,它通过extends关键字,继承了People类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个People类。
下面,我们在Xiaoxu内部加上代码。
const {log: l} = console;class People {constructor(names) {this.names = names;}toString(){return `[${this.names}]`;}
}class Xiaoxu extends People {constructor(names, age) {super(names); //调用父类的constructor()this.age = age;}toString(){return this.age + '|' + super.toString();}
}const x = new Xiaoxu("小徐", 20)
l(x) //Xiaoxu { names: '小徐', age: 20 }
l(x.toString()) //20|[小徐]
上面Xiaoxu类示例中,constructor()方法和toString()方法内部,都出现了super关键字。super在这里表示父类的构造函数,用来新建一个父类的实例对象。
ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。
class People {}class Xiaoxu extends People {constructor(){}
}// 上述写法,如果没有实例化Xiaoxu对象,不会抛出异常
// 一旦实例化上述Xiaoxu对象,则抛错:ReferenceError
let x = new Xiaoxu();
// ReferenceError: Must call super constructor
// in derived class before accessing 'this'
// or returning from derived constructor
上面代码中,Xiaoxu继承了父类People,但是它的构造函数没有调用super(),导致新建实例时报错。
为什么子类的构造函数,一定要调用super()?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
注意,这意味着新建子类实例时,父类的构造函数必定会先运行一次。
class People {constructor() {console.log(1);}
}class Xiaoxu extends People {constructor() {super();console.log(2);}
}const x = new Xiaoxu();
// 1
// 2
上面示例中,子类Xiaoxu新建实例时,会输出1和2。原因就是子类构造函数调用super()时,会执行一次父类构造函数。
另一个需要注意的地方是,在子类的构造函数中,只有调用super()之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
class People {constructor(names) {this.names = names;}
}class Xiaoxu extends People {constructor(names, age) {// ReferenceError: Must call super constructor // in derived class before accessing 'this' or // returning from derived constructorthis.age = age; // ReferenceErrorsuper(names); //调用父类的constructor()this.age = age;}
}new Xiaoxu();
上面代码中,子类的constructor()方法没有调用super()之前,就使用this关键字,结果报错,而放在super()之后就是正确的。
如果子类没有定义constructor()方法,这个方法会默认添加,并且里面会调用super()。也就是说,不管有没有显式定义,任何一个子类都有constructor()方法。
class Xiaoxu extends People {
}// 等同于
class Xiaoxu extends People {constructor(...args) {super(...args);}
}
有了子类的定义,就可以生成子类的实例了。
class People {constructor(names) {this.names = names;}
}class Xiaoxu extends People {constructor(names, age) {super(names); //调用父类的constructor()this.age = age;}
}const x = new Xiaoxu("小徐", 20)
l(x instanceof Xiaoxu) // true
l(x instanceof People) // true
上面示例中,实例对象x同时是Xiaoxu和People两个类的实例,这与 ES5 的行为完全一致。
– 总结:子类必须在constructor()方法中调用super(),该步骤会生成一个继承父类的this对象,且只有调用super()之后,才可以使用this关键字。
(2)私有属性和私有方法的继承
父类所有的属性和方法,都会被子类继承,除了私有的属性和方法。
子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。
class People {#p = 1;#m() {console.log('hello');}
}class Man extends People {constructor() {super();// SyntaxError: Private field '#p' // must be declared in an enclosing classconsole.log(this.#p); // 报错// SyntaxError: Private field '#m' // must be declared in an enclosing classthis.#m(); // 报错}
}
上述表明子类调用父类的私有属性或私有方法,均会报错。
如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性。
class People {#p = 1;getP() {return this.#p;}
}class Man extends People {constructor() {super();console.log(this.getP()); // 1}
}new Man();
上面示例中,getP()是父类用来读取私有属性的方法,通过该方法,子类就可以读到父类的私有属性。
另外,虽然子类继承父类的私有属性不能在子类中直接使用,但是在父类中使用时,子类用in判断是否存在父类的私有属性时,依然是成立的:
class A {#foo = 0;static test(obj) {console.log(#foo in obj);}
}class SubA extends A {};A.test(new SubA()) // true
– 总结:子类无法继承父类的私有属性和私有方法,无法在子类中直接使用。虽然子类继承父类的私有属性不能在子类中直接使用,但是在父类中使用时,子类用in判断是否存在父类的私有属性时是成立的。
(3)静态属性和静态方法的继承
父类的静态属性和静态方法,也会被子类继承。
class A {static hello() {console.log('hello world');}
}class B extends A {
}B.hello() // hello world
上面代码中,hello()是A类的静态方法,B继承A,也继承了A的静态方法。
注意,静态属性是通过浅拷贝实现继承的。
class A {static number = 99;
}class B extends A {constructor() {super();B.number--;}
}const b = new B();
l(B.number) // 98
l(A.number) // 99
上面示例中,number是 A 类的静态属性,B 类继承了 A 类,因此也继承了这个属性。但是,在 B 类内部操作B.number这个静态属性,影响不到A.number,原因就是 B 类继承静态属性时,会采用浅拷贝,拷贝父类静态属性的值,因此A.number和B.number是两个彼此独立的属性(基础属性的浅拷贝,依然是互不影响的)。
但是,由于这种拷贝是浅拷贝,如果父类的静态属性的值是一个对象,那么子类的静态属性也会指向这个对象,因为浅拷贝只会拷贝对象的内存地址。
class A {static number = {n: 99};
}class B extends A {constructor() {super();B.number.n--;}
}const b = new B();
l(B.number) // { n: 98 }
l(A.number) // { n: 98 }
上面示例中,A.number的值是一个对象,浅拷贝导致B.number和A.number指向同一个对象。所以,子类B修改这个对象的属性值,会影响到父类A。
– 总结:父类的静态属性和静态方法,会被子类继承。静态属性的继承是浅拷贝,若静态属性是对象,则继承时拷贝的是对象内存地址,子类实例对象修改该静态属性,父类此静态属性一同改变。
(4)Object.getPrototypeOf()
Object.getPrototypeOf()方法可以用来从子类上获取父类。
class Parent {
}class Child extends Parent {
}l(Object.getPrototypeOf(Child) === Parent)
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
(5)super关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super()函数。
class A {}class B extends A {constructor() {super();}
}new B();
上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则报错。
调用super()的作用是形成子类的this对象,把父类的实例属性和方法放到这个this对象上面。子类在调用super()之前,是没有this对象的,任何对this的操作都要放在super()的后面。
注意,这里的super虽然代表了父类的构造函数,但是因为返回的是子类的this(即子类的实例对象),所以super内部的this代表子类的实例,而不是父类的实例,这里的super()相当于A.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)。
class A {constructor() {console.log(new.target.name);}
}
class B extends A {constructor() {super();}
}
new A() // A
new B() // B
上面示例中,new.target指向当前正在执行的函数。可以看到,在super()执行时(new B()),它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。
不过,由于super()在子类构造方法中执行时,子类的属性和方法还没有绑定到this,所以如果存在同名属性,此时拿到的是父类的属性。
class A {name = 'A';constructor() {console.log('My name is ' + this.name);}
}class B extends A {name = 'B';// 注意这里不写constructor的话,// 那么默认添加的constructor是会调用super()的// 但是若是自己写了constructor,那么必须写上super()
}const b = new B(); // My name is A
上述代码等同于:
class A {name = 'A';constructor() {console.log('My name is ' + this.name);}
}class B extends A {name = 'B';// 注意这里不写constructor的话,// 那么默认添加的constructor是会调用super()的// 但是若是自己写了constructor,那么必须写上super()constructor() {super();}
}const b = new B(); // My name is A
上面示例中,最后一行输出的是A,而不是B,原因就在于super()执行时,B的name属性还没有绑定到this,所以this.name拿到的是A类的name属性。
作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
class A {}class B extends A {m() {// SyntaxError: 'super' keyword unexpected heresuper(); // 报错}
}
上面代码中,super()用在B类的m方法之中,就会造成语法错误。
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {p() {return 2;}
}class B extends A {constructor() {super();console.log(super.p()); // 2}
}let b = new B();
上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。因为constructor构造方法,就在原型链上,视作普通方法。
这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
class A {constructor() {this.p = 2;}
}class B extends A {get m() {return super.p;}
}let b = new B();
l(b.m) // undefined
上面代码中,p是父类A实例的属性,super.p就引用不到它。当然,因为子类B自身也没有定义m的值,若子类中定义了m的值,则返回是子类的m值:
class A {constructor() {this.p = 2;}
}class B extends A {m = 99;get m() {return super.p;}
}let b = new B();
l(b.m) // 99
如果属性定义在父类的原型对象上,super就可以取到,即父类原型上定义了p的值为88,且子类中没有自定义m,则返回88,否则返回子类中定义的m的值:
class A {constructor() {this.p = 2;}
}A.prototype.p = 88;class B extends A {// m = 99;get m() {return super.p;}
}let b = new B();
l(b.m) // 88
ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例(注意是super调用父类方法,this就指向当前子类实例)。
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.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
class A {constructor() {this.x = 1;}
}class B extends A {constructor() {super();this.x = 2;super.x = 3;console.log(super.x); // undefinedconsole.log(this.x); // 3}
}let b = new B();
上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
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 1var child = new Child();
child.myMethod(2); // instance 2
上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
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
上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。
注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}class B extends A {constructor() {super();// SyntaxError: 'super' keyword unexpected hereconsole.log(super); // 报错}
}
上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。
class A {}class B extends A {constructor() {super();console.log(super.valueOf() instanceof B); // trueconsole.log(super.valueOf()) //B {}super.valueOf().print() //B come inthis.valueOf().print() //B come in}print(){console.log("B come in")}
}let b = new B();
上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。
最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
var obj = {toString() {return "MyObject: " + super.toString();}
};l(obj.toString()); // MyObject: [object Object]
– 总结:super作为函数调用时(super()),代表父类的构造函数,且子类的构造函数必须执行一次super()函数,super内部的this代表子类的实例,而非父类的实例。若子类不写constructor,默认会执行一次super(),但若子类显式声明了constructor,那么必须写上super()。super()只能用在子类的构造函数之中。
– 总结:super作为对象时(super.xxx),在普通方法中,指向父类的原型对象;在静态方法中,指向父类。super在普通方法之中,指向父类.prototype,super调用父类属性时,自身不存在该属性时,从父类.prototype获取该属性,否则取自身属性;super调用父类属性并为其赋值时,这里的super指代this,意即为自身实例对象属性赋值;super调用父类方法时,方法从父类.prototype获取,但方法内部的this指向当前子类实例。super在静态方法之中,指向父类,并调用父类的静态方法,且方法内部的this指向当前的子类,而不是子类的实例。同时可以在任意一个对象中,使用super关键字。
(6)类的prototype属性和__proto__属性
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}class B extends A {
}l(B.__proto__ === A) // true
l(B.prototype.__proto__ === A.prototype) // true
上面代码中,子类B的__proto__属性指向父类A,子类B的prototype属性的__proto__属性指向父类A的prototype属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
class A {
}class B {
}// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);const b = new B();
Object.setPrototypeOf方法的实现如下:
Object.setPrototypeOf = function (obj, proto) {obj.__proto__ = proto;return obj;
}
因此,就得到了上面的结果。
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。
B.prototype = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
extends关键字后面可以跟多种类型的值,JS类是单继承(Python也是类单继承,Java是类多继承):
class B extends A {
}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。如下所示:
function Base(names) {this.names = names;
}class A extends Base {constructor(names){super(names);}
}let a = new A();
l(a.names) //undefinedlet b = new A("小徐");
l(b.names) //小徐
下面,讨论两种情况。第一种,子类继承Object类。
class A extends Object {
}l(A.__proto__ === Object) // true
l(A.prototype.__proto__ === Object.prototype) // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
第二种情况,不存在任何继承。
class A {
}l(A.__proto__ === Function.prototype) // true
l(A.prototype.__proto__ === Object.prototype) // true
l(A.prototype.__proto__ === Function.prototype) // false
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__ 指向构造函数(Object)的prototype属性。
实例的__proto__ 属性
子类实例的__proto__ 属性的__proto__ 属性,指向父类实例的__proto__ 属性。也就是说,子类的原型的原型,是父类的原型。
class A {}class B extends A {}var a = new A();
var b = new B();l(b.__proto__ === a.__proto__) // false
l(b.__proto__.__proto__ === a.__proto__) // true
上面代码中,B继承了A,导致前者原型的原型是后者的原型。
因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。
class A {}class B extends A {}var a = new A();
var b = new B();l(b.__proto__ === a.__proto__) // false
l(b.__proto__.__proto__ === a.__proto__) // trueb.__proto__.__proto__.printName = function () {console.log('Hello xiaoxu.');
};a.printName() // "Hello xiaoxu."
上面代码在B的实例b上向A类添加方法,结果影响到了A的实例a。
– 总结:JavaScript中类是单继承,只要是一个有prototype属性的函数,就能被继承。
(7)原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
function MyArray() {Array.apply(this, arguments);
}MyArray.prototype = Object.create(Array.prototype, {constructor: {value: MyArray,writable: true,configurable: true,enumerable: true}
});
上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。
var colors = new MyArray();
colors[0] = "red";
l(colors.length) // 0colors.length = 0;
l(colors[0]) // "red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。
ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。
下面的例子中,我们想让一个普通对象继承Error对象。
var e = {};l(Object.getOwnPropertyNames(Error.call(e)))
// [ 'stack' ]l(Object.getOwnPropertyNames(e))
// []
上面代码中,我们想通过Error.call(e)这种写法,让普通对象e具有Error对象的实例属性。但是,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法,无法继承原生构造函数。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {constructor(...args) {super(...args);}
}var arr = new MyArray();
arr[0] = 12;
l(arr.length) // 1arr.length = 0;
l(arr[0]) // undefined
上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。
上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。
class VersionedArray extends Array {constructor(){super();this.history = [[]];}commit(){// this.slice()是原有数组的全部值this.history.push(this.slice());}revert(){this.splice(0, this.length, ...this.history[this.history.length - 1])}
}var x = new VersionedArray();
l(x) //VersionedArray(0) [ history: [ [] ] ]
x.push(99)
l(x) //VersionedArray(1) [ 99, history: [ [] ] ]
x.push(100)
l(x) //VersionedArray(2) [ 99, 100, history: [ [] ] ]
l(x.history) //[ [] ]x.commit();
l(x)
// VersionedArray(2) [
// 99,
// 100,
// history: [ [], VersionedArray(2) [ 99, 100, history: [Array] ] ]
// ]
l(x.history)
// [ [], VersionedArray(2) [ 99, 100, history: [ [] ] ] ]console.log("打印:")for(let i of x.history[1]) {console.log(i)// 99// 100
}x.push(101)
l(x)
// VersionedArray(3) [
// 99,
// 100,
// 101,
// history: [ [], VersionedArray(2) [ 99, 100, history: [Array] ] ]
// ]x.revert();
l(x)
// VersionedArray(2) [
// 99,
// 100,
// history: [ [], VersionedArray(2) [ 99, 100, history: [Array] ] ]
// ]
slice() 方法用数组的某个片段切出新数组,该方法创建新数组,它不会从源数组中删除任何元素(不会改变原有的数组)。
slice() 可接受两个参数,比如 (1, 3)。
该方法会从开始参数选取元素,直到结束参数(不包括)为止(左闭右开区间)。当参数仅1个时,默认是从0开始,即将区间范围为:[0, 参数)的片段切出去除,保留剩下数据。
var fruits = ["pear", 'apple', 'mango', 'orange']
let f = fruits.slice(1)
l(f) //[ 'apple', 'mango', 'orange' ]
l(fruits) //[ 'pear', 'apple', 'mango', 'orange' ]let f2 = fruits.slice(3)
l(f2) //[ 'orange' ]
let f3 = fruits.slice()
l(f3) //[ 'pear', 'apple', 'mango', 'orange' ]
splice() 方法可用于向数组添加新项(会改变原有的数组):
第一个参数(3)定义了应添加新元素的位置(拼接)。第二个参数(0)定义应删除多少元素。其余参数('banana', 'litchi')定义要添加的新元素。splice() 方法返回一个包含已删除项的数组:
var fruits = ["pear", 'apple', 'mango', 'orange']
let f = fruits.splice(3, 0, 'banana', 'litchi')
l(f) //[]
l(fruits) //[ 'pear', 'apple', 'mango', 'banana', 'litchi', 'orange' ]//原本[ 'pear', 'apple', 'mango', 'banana', 'litchi', 'orange' ]
let f2 = fruits.splice(0, 2, 'pineapple')
l(f2) //[ 'pear', 'apple' ]
l(fruits) //[ 'pineapple', 'mango', 'banana', 'litchi', 'orange' ]
//原本[ 'pineapple', 'mango', 'banana', 'litchi', 'orange' ]
fruits.splice(0, fruits.length, 'kiwifruit')
l(fruits) //[ 'kiwifruit' ]
上面代码中,VersionedArray会通过commit方法,将自己的当前状态生成一个版本快照,存入history属性。revert方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray依然是一个普通数组,所有原生的数组方法都可以在它上面调用。
下面是一个自定义Error子类的例子,可以用来定制报错时的行为。
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 err = new MyError("调用接口异常")
l(err.message)
l(err instanceof Error)
l(err.name)
l(err.stack)// 调用接口异常
// true
// MyError
// Error
// at new ExtendableError (D:\my_react\hello_react\es6\3-ClassExtends.js:822:23)
// at new MyError (D:\my_react\hello_react\es6\3-ClassExtends.js:830:9)
// at Object.<anonymous> (D:\my_react\hello_react\es6\3-ClassExtends.js:834:11)
// at Module._compile (node:internal/modules/cjs/loader:1267:14)
// at Module._extensions..js (node:internal/modules/cjs/loader:1321:10)
// at Module.load (node:internal/modules/cjs/loader:1125:32)
// at Module._load (node:internal/modules/cjs/loader:965:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
// at node:internal/main/run_main_module:23:47
注意,继承Object的子类,有一个行为差异。
class NewObject extends Object {constructor (){super(...arguments);}
}var o = new NewObject({attr: true})
l(o.attr === true) // false
上面代码中,NewObject继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。
(8)Mixin模式的实现
在JavaScript中,原生不支持多继承(JS类原生是单继承),但可以通过混入(mixin)模式来模拟多继承的效果。
Mixin指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {a: 'a'
};const b = {b: 'b'
};const c = {...a, ...b};
l(c) // { a: 'a', b: 'b' }
上面代码中,c对象是a对象和b对象的合成,具有两者的接口。
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
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){console.log("开始copy属性:", Reflect.ownKeys(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);}}
}class Base {age = 99;names = "小徐";static addr = "CQ";static number = 9527;values(){console.log(this.names)}
}class Serializable {serialUid = 123456;
}class DistributeEdit extends mix(Base, Serializable){static addr = "农场";
}let edit = new DistributeEdit();
// 开始copy属性: [ 'length', 'name', 'prototype',
// 'addr', 'number' ]
// 开始copy属性: [ 'constructor', 'values' ]
// 开始copy属性: [ 'length', 'name', 'prototype' ]
// 开始copy属性: [ 'constructor' ]
// 开始copy属性: [ 'age', 'names' ]
// 开始copy属性: [ 'serialUid' ]
edit.values(); //小徐
l(edit.age) //99
l(DistributeEdit.addr) //农场
l(DistributeEdit.number) //9527
l(edit.serialUid) //123456
上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。同时可以发现,JS原生不支持的多继承,通过mixin的方式实现了类的多继承。
– 总结:Javascript原生类仅支持单继承,不支持类的多继承。Mixin的方式可以实现类的多继承。
这篇关于ECMAScript 6:三、Class的继承的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!