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 constructor
        this.age = age; // ReferenceError
        super(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 class
        console.log(this.#p); // 报错

        // SyntaxError: Private field '#m' 
        // must be declared in an enclosing class
        this.#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 here
    super(); // 报错
  }
}

上面代码中,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); // undefined
        console.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 1

var 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 here
    console.log(super); // 报错
  }
}

上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
    console.log(super.valueOf())    //B {}
    super.valueOf().print()         //B come in
    this.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)  //undefined

let 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__) // true

b.__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)  // 0

colors.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) // 1

arr.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的方式可以实现类的多继承。

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值