3

继承 inherit

class 是对原型继承的一种语法糖的包装。那相对于原型继承,它有什么优点呢?
我们来先看一个典型的基于原型链继承的例子。部分内容来自“Javascript高级程序设计”

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

function SubType() {
    this.subProperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subProperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());  // true
console.log(instance instanceof Object);  // true
console.log(instance instanceof SuperType);  // true
console.log(instance instanceof SubType);  // true

问题,当包含引用类型的值。

function SuperType() {
    this.colors = ["red", "blue", "green"];
}

function SubType() {
   
}

SubType.prototype = new SuperType();


var instance = new SubType();
instance.colors.push("black");
var instance1 = new SubType();
instance1.colors.push("white");
console.log(instance.colors);  // [ 'red', 'blue', 'green', 'black', 'white' ]
console.log(instance1.colors);  // [ 'red', 'blue', 'green', 'black', 'white' ]

解决方案:

  • 借用构造函数

    function SuperType() {
      this.colors = ["red", "blue", "green"];
    }
    
    function SubType() {
     SuperType.call(this);
    }
    
    SubType.prototype = new SuperType();
    
    
    var instance = new SubType();
    instance.colors.push("black");
    var instance1 = new SubType();
    instance1.colors.push("white");
    console.log(instance.colors);
    console.log(instance1.colors);
  • 组合继承

    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    
    SuperType.prototype.sayName = function() {    
      console.log(this.name);
    }
    
    function SubType(name, age) {
     SuperType.call(this, name);
     this.age = age;
    }
    
    SubType.prototype = new SuperType();
    SubType.prototype.sayAge = function() {
      console.log(this.age);
    }
  • 寄生组合式继承

    function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }
    
    function inheritPrototype(subType, superType) {
      let prototype = object(superType.prototype);
      prototype.constructor = subType;
      subType.prototype = prototype;
    }
    
    function SuperType(name) {
      this.name = name;
      this.colors = ["red", "blue", "green"];
    }
    
    SuperType.prototype.sayName = function() {    
      console.log(this.name);
    }
    
    function SubType(name, age) {
     SuperType.call(this, name);
     this.age = age;
    }
    
    inheritPrototype(SubType, SuperType);
    
    SubType.prototype.sayAge = function() {
      console.log(this.age);
    }
    var instance = new SubType("Tom", 70);
    instance.colors.push("black");
    var instance1 = new SubType("Jerry", 69);
    instance1.colors.push("white");
    console.log(instance.colors);
    console.log(instance.sayName());
    console.log(instance.sayAge());
    console.log(instance1.colors);
    console.log(instance1.sayName());
    console.log(instance1.sayAge());

    MDN 原型链继承
    (欠图一张)

    extends

    从es5来说,实现对象的继承,还是相当麻烦的。而extends 关键字的出现,使继承变得简单,原型会自动进行调整,super()/super关键字可以访问父类的构造方法和属性。

    class Animal { 
    constructor(name) {
      this.name = name;
    }
    
    speak() {
      console.log(this.name + ' makes a noise.');
    }
    }
    
    class Dog extends Animal {
    speak() {
      console.log(this.name + ' barks.');
    }
    }
    
    var d = new Dog('Mitzie');
    d.speak();// 'Mitzie barks.'

    分析:Dog类没有构造函数,这样合理吗?

    // 等价于上个类定义
    class Dog extends Animal {
    constructor(name) {
      super(name)
    }
    speak() {
      console.log(this.name + ' barks.');
    }
    }

    super()方法调用注意:

    • 只可在以extends 实现的派生类中的constructor方法中调用,在非派生类或方法中直接调用,会报错。
    • 在constructor中访问this之前,一定要先调用super(),因为它负责初始化this,如果在super()调用之前尝试访问this,会报错。
    • 如果不想调用super(),则唯一的方法是让类的constructor()返回一个对象。

类方法遮蔽

说明:派生类中的方法总会覆盖基类中的同名方法。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}
// 基类中的speak()方法被覆盖

静态类成员继承

说明:如果基类有静态成员,那么这些静态成员在派生类中也可用。

class Animal { 
    constructor(name) {
      this.name = name;
    }
    
    speak() {
      console.log(this.name + ' makes a noise.');
    }
    static create(name) {
        return new Animal(name);
    }
  }
  
  class Dog extends Animal {
    speak() {
      console.log(this.name + ' barks.');
    }
  }

  let a1 = Animal.create("Monkey");
  let a2 = Dog.create("BeijinDog");
  console.log(a1 instanceof Animal);  // true
  console.log(a2 instanceof Animal);  // true
  console.log(a2 instanceof Dog);  // false 这个是不是很意外?

派生自表达式的类

由ES6的class定义可以知道,是function的语法糖,但为实现原型继承,提供了方便的实现。JS的强大的一点就是函数可以返回函数,那如果返回类的定义呢?是否支持继承?返回对象是个函数,并且有[[Constrcutor]]属性和原型,就能满足extends实现。

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

function getBase() {
  return Animal;
}
class Dog extends getBase() {
  speak() {
    console.log(this.name + ' barks.');
  }
}

const dog = new Dog('Tom');
dog.speak();

如果这个例子基于class的实现,有点取巧的意思,那看另一个例子。


const SerializableMixin = {
  serialize() {
    return JSON.stringify(this);
  }
}

const AnimalMixin = {
  speak() {
    console.log(this.name + ' barks.');
  }
}

function mixin(...mixins) {
  const base = function() {};
  Object.assign(base.prototype, ...mixins);
  return base;
}

class Dog extends mixin(AnimalMixin, SerializableMixin) {
  constructor(name){
    super(name);
    this.name = name;
  }
}

const dog = new Dog('Tom');
dog.speak();  // Tom barks.

关于function,class,extends,mixin,是否有新的理解呢?

内建对象继承

在ES6之前,内建对象很难实现继承的,更多用has-a思想,实现对内建对象的处理。ES6中,大量内建对象的内部实现得以暴漏,也使得继承内建对象变成了可能。

class ColorsArray extends Array {
}
const colors = new ColorsArray();
colors[0] = 'red';
console.log(colors.length); // 1

colors.length = 0;
console.log(colors[0]); // undefined

分析:基类(Array)创建 this 的值,然后派生类的构造函数(ColorsArray)再修改这个值。所以一开始可以通过this访问基类的所有内建功能,然后再正确地接收所有与之相关的功能。这与Array.apply/call 这种方法实现继承的this处理方式正好相反。这也是extends特殊的地方。

Symbol.species

class ColorsArray extends Array {
}
const colors = new ColorsArray('red', 'green', 'blue');
const subColors = colors.slice(0,1);
console.log(colors instanceof ColorsArray);  // true
console.log(subColors instanceof ColorsArray);  // true

通常来讲,slice 方法继承自 Array ,返回的应该是Array的实例,但在这个示例中,却返回的是ColorsArray的实例,这是为什么呢?这是ES6中Symbol.species的功劳。Symbol.species MDN 详细说明

class MyArray extends Array {
  // Overwrite species to the parent Array constructor
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

注意:重写实现的时候,使用getter+static,可以返回想用的类型,也可以返回 this,是的,你没看错,在static getter中使用了this,它指向的是MyArray的构造函数。

constructor中new.target

new.target是es6中新添加的元属性,只有通过new操作创建对象的时候,new.target才会被指向类/方法本身,通过call/apply操作,new.target为undefined。可以通过判断new.target,来确实函数是否允许new操作。MDN new.target 说明
惯例,再加个代码示例,偷懒,直接从MDN上拷了。

function Foo() {
  if (!new.target) throw 'Foo() must be called with new';
  console.log('Foo instantiated with new');
}

new Foo(); // logs "Foo instantiated with new"
Foo(); // throws "Foo() must be called with new"

又是先说function,不是已经升级到ES6,使用class了吗?始终要有一个清楚的认识,class,是function实现原型继承的语法糖,但有自己的特性存在的(不然,也不用引入class了)。

class A {
  constructor() {
    console.log(new.target.name);
  }
}

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

var a = new A(); // logs "A"
var b = new B(); // logs "B"

class C { constructor() { console.log(new.target); } }
class D extends C { constructor() { super(); } }
 
var c = new C(); // logs class C{constructor(){console.log(new.target);}}
var d = new D(); // logs class D extends C{constructor(){super();}}

这个就是类的了。


anleo
30 声望1 粉丝

准备写es6系列、设计模式(node,javascript)、前端工程化、TDD&BDD(Reactjs&angularjs)