JS类的继承

ES5

原型继承

// 声明父类
function SuperClass () {
    this.superValue = true;
}
// 为父类添加共有方法
SuperClass.prototype.getSuperValue = function () {
    return this.superValue;
}
// 声明子类
function SubClass () {
    this.subValue = false;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function () {
    return this.subValue;
}

var instance = new SubClass();
console.log(instance.getSuperValue()); // true
console.log(instance.getSubValue()); // false

console.log(instance instanceof SuperClass); // true
console.log(instance instanceof SubClass); // true
console.log(SubClass instanceof SuperClass); // false instanceof只是判断前面的对象是否是后面类(对象)的实例,并不表示两者的继承。

原型继承有两个缺点。其一,由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。其二,由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。

function SuperClass () {
    this.books = ['js', 'html', 'css']
}
function SubClass () {}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books); // ['js', 'html', 'css']
instance1.books.push('blog');
console.log(instance2.books); // ['js', 'html', 'css', 'blog']

构造函数继承

// 声明父类
function SuperClass (id) {
    // 引用类型共有属性
    this.books = ['js', 'html', 'css'];
    this.id = id;
}
// 父类声明原型方法
SuperClass.prototype.showBooks = function () {
    console.log(this.books);
}
// 声明子类
function SubClass (id) {
    // 继承父类
    SuperClass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new SubClass(10);
// 创建第二个子类的实例
var instance2 = new SubClass(11);

instance1.books.push('blog');
console.log(instance1.books); // ['js', 'html', 'css', 'blog']
console.log(instance1.id); // 10
console.log(instance2.books); // ['js', 'html', 'css']
console.log(instance2.id); // 11

instance1.showBooks); // TypeError

SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境。因此在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。为了综合这两种模式的优点,后来有了组合式继承。

组合继承

// 声明父类
function SuperClass (name) {
    // 值类型共有属性
    this.name = name;
    // 引用类型共有属性
    this.books = ['html', 'css', 'js'];
}
// 父类原型共有方法
SuperClass.prototype.getName = function () {
    console.log(this.name);
}
// 声明子类
function SubClass (name, time) {
    // 构造函数式继承父类name属性
    SuperClass.call(this, name);
    // 子类中新增共有属性
    this.time = time;
}
// 类式继承 子类型原型继承父类
SubClass.prototype = new SuperClass();
// 子类原型方法
SubClass.prototype.getTime = function () {
    console.log(this.time);
}

子类的实例中更改父类继承下来的引用类型属性如books,不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中,如name。

var instance1 = new SubClass('js book', 2017);
instance1.books.push('blog');
console.log(instance1.books); // ['html', 'css', 'js', 'blog']
instance1.getName(); // js book
instance1.getTime(); // 2017

var instance2 = new SubClass('css book', 2016);
console.log(instance2.books); // ['html', 'css', 'js']
instance2.getName(); // css book
instance2.getTime(); // 2016

原型式继承

寄生式继承

寄生组合式继承

多继承

在js中继承是依赖于原型prototype链实现的,只有一条原型链,所以理论上是不能继承多个父类的。然后js是灵活的,通过一些方式可以实现多继承,比如extend方法。

// 单继承 属性复制
var extend = function (target, source) {
    // 遍历源对象中的属性
    for (var property in source) {
        // 将源对象中的属性复制到目标对象中
        target[property] = source[property];
    }
    // 返回目标对象
    return target;
}
var book = {
    name: 'JavaScript',
    alike: ['css', 'html', 'js']
}
var anotherBook = {
    color: 'blue'
}
extend(anotherBook, book);
console.log(anotherBook, name); // JavaScript
console.log(anotherBook.alike); // ['css', 'html', 'js']

anotherBook.alike.push('ajax');
anotherBook.name = 'blog';
console.log(anotherBook.name); // blog
console.log(anotherBook.alike); // ['css', 'html', 'js', 'ajax']
console.log(book.name); // JavaScript
console.log(book.alike); // ['css', 'html', 'js', 'ajax']

实现多继承的方式在上面方法上多传递几个对象

// 多继承 属性复制
var mix = function () {
    var i = 1, // 从第二个参数起为被继承的对象
        len = arguments.length, // 获取参数长度
        target = arguments[0], // 从第一个对象为目标对象
        arg; // 缓存参数对象
    // 遍历被继承的对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i];
        // 遍历被继承对象中的属性
        for (var property in arg) {
            // 将被继承对象中的属性复制到目标对象中
            target[property] = arg[property];
        }
    }
    // 返回目标对象
    return target;
}

但是在使用的时候需要传入目标对象(第一个参数–需要继承的对象)。可以将它绑定到原生对象Object上,这样所有的对象就可以拥有这个方法。

Object.prototype.mix = function () {
    var i = 0, // 从第一个参数起为被继承的对象
        len = arguments.length, // 获取参数长度
        arg; // 缓存参数对象
    // 遍历被继承的对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i];
        // 遍历被继承对象中的属性
        for (var property in arg) {
            // 将被继承对象中的属性复制到目标对象中
            this[property] = arg[property];
        }
    }
}

otherBook.mix(book1, book2);
console.log(otherBook); // Object {color: 'blue', name: 'JavaScript', mix: function}

多态

多态,就是同一个方法多种调用方法。

function add () {
    // 获取参数
    var arg = arguments,
        // 获取参数长度
        len = arg.length;
    switch (len) {
        // 如果没有参数
        case 0:
            return 10;
        // 如果只有一个参数
        case 1:
            return 10 + arg[0];
        // 如果有两个参数
        case 2:
            return arg[0] + arg[1];
    }
}

console.log(add()); // 10
console.log(add(5)); // 15
console.log(add(6, 7)); // 13

或者转化成类形式,将不同运算方法封装在类内,这样代码更易懂

function Add () {
    // 无参数算法
    function zero () {
        return 10;
    }
    // 一个参数算法
    function one (num) {
        return 10 + num;
    }
    // 两个参数算法
    function two (num1, num2) {
        return num1 + num2;
    }
    // 相加共有方法
    this.add = function () {
        var arg = arguments,
        // 获取参数长度
        len = arg.length;
        switch (len) {
            // 如果没有参数
            case 0:
                return zero();
            // 如果只有一个参数
            case 1:
                return one(arg[0]);
            //如果有两个参数
            case 2:
                return two(arg[0], arg[1]);
        }
    }
}

var A = new Add();
console.log(A.add()); // 10
console.log(A.add(5)); // 15
console.log(A.add(6, 7)); // 13

ES6

简介

Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰方便的多。下面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。

class Point {
}

class ColorPoint extends Point {
}

下面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。下面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。下面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)。下面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。下面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。p是父类A实例的属性,super.p就引用不到它。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

如果属性定义在父类的原型对象上,super就可以取到。

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

ES6 规定,通过super调用父类的方法时,super会绑定子类的this。下面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(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