ES2015 classes(三)

八、Class 继承

一开始 JavaScript 并没有提供传统面向对象语言中的类式继承,而是通过 原型委托 的方式来实现对象与对象之间的继承,也没有在语言层面提供对抽象类和接口的支持。

许多面对对象语言都支持两种继承方式:接口继承、实现继承。ECMAScript 只支持实现继承(因为接口继承继承的是方法签名,而不是实际方法,js 的方法不支持签名)。而实现继承在 js 中主要依靠原型链。

1. 传统继承:依靠原型链

许多大神总结了有很多种继承的方式,其中 通过依靠原型链实现继承的方式 和今天要讲的 通过 Class extends 关键字实现继承的方式,相比而言后者要清晰和方便很多。

  • 对于什么是原型链,许多文章都有细讲,这里不做赘述

直接引用一个例子帮助理解:[原文]https://juejin.im/post/58f94c9bb123db411953691b

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

Father.prototype.getValue = function() { return this.property; }

function.Son() { this.sonProperty = false; }

Son.prototype = new Father(); //overwrite Son.prototype and Son.prototype.constructor
Son.prototype.getSonValue = function() { return this.sonProperty; }

var instance = new Son(); //instance.constructor -> Father
console.log(instance.getValue()); //true

在上例中,instance 实例对象通过原型链找到了 Father 原型对象中的 getValue() 方法。

让我们用 instance 操作符来测试一下 instance 的引用类型是什么:

alert(instance instanceof Object); //true
alert(instance instanceof Father); //true
alert(instance instanceof Son); //true

再通过 isPrototypeOf() 方法来找出 instance 实例对象所处原型链中的原型对象:

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(Father.prototype.isPrototypeOf(instance)); //true
alert(Son.prototype.isPrototypeOf(instance)); //true
  • constructor stealing :经典继承

在上面解释了什么是原型链的例子中,我们会发现 单独使用原型链 很难在开发中解决问题,其包含不足:

  1. 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享
  2. 在创建子类时,并不能向超类的构造函数传递参数

因此,开发者开始使用一种技术:借用构造函数,constructor stealing,在子类构造函数内部调用超类构造函数。也称经典继承

扫描二维码关注公众号,回复: 6155133 查看本文章
  1. 保证了原型链中引用类型值的原型的独立,不为所有实例共享
  2. 子类创建时可以向父类传递参数
function Father() { this.colors = ["red","blue","green"]; }

function Son() { Father.call(this) }; //Pass parameters to the parent with the call

var instance1 = new Son();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"

var instance2 = new Son();
console.log(instance2.colors);//"red,blue,green"

这种方法其实也存在一些问题:

  1. 方法都在构造函数中定义,无法实现函数复用
  2. 超类中的方法对于子类是不可见的
  • 组合继承:伪经典继承(JavaScript 中最常用的继承模式

结合原型链和经典继承的思想,使用原型链实现对原型属性和方法的正常继承,通过 constructor stealing 来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性.。

function Father(name) { this.name=name; this.colors = ["red","blue","green"]; }
Father.prototype.sayName = function() { alert(this.name); }

function Son(name, age) { 
    Father.call(this, name); //constructor stealing
    this.age = age;
}
Son.prototype = new Father(); //原型链
Son.prototype.sayAge = function() { alert(this.age; }


var instance1 = new Son('test1', 5);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //test1
instance1.sayAge(); //5

var instance2 = new Son("zhai", 10);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //zhai
instance2.sayAge(); //10

组合继承避免了原型链和 constructor stealing 的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。

但组合继承其实会造成不必要的消耗:调用了两次父类构造函数:new Father();Father.call(this, name);

  • 原型继承(思想十分重要)

道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》的文章中提出:借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型

主要思想:在 Object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为该构造函数的原型,最后返回这个临时类型的一个新实例。

function Object(Father) {
    function Son(){}
    Son.prototype = Father;
    return new Son();
}

从本质上,Object() 对于传入的对象会进行浅复制,即 Father 作为原型,其包含引用类型值属性,所以 Father.xx 对于返回的所有 Son 实例来说是共享的。如下例:

var person = { friends : ["Van","Louis","Nick"] };
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");

alert(anotherPerson.friends); //"Van,Louis,Nick,Rob"
alert(yetAnotherPerson.friends); //"Van,Louis,Nick,Rob,Style"
alert(person.friends); //"Van,Louis,Nick,Rob,Style"

在 ES5 中,通过 object.create() 方法规范化了上面的原型式继承。

var person = { friends : ["Van", "Louis", "Nick"] };

var anotherPerson = Object.create(person);

var yetAnotherPerson = Object.create(person);

注意:原型式继承中,包含引用类型值的属性始终都会共享相应的值,,就像单独使用原型模式一样。

  • 寄生式继承,思路是基于原型式继承,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象(关键)并返回。要注意使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率。
function createAnother(original) {
    var clone = object(original); //通过调用object函数创建一个新对象
    clone.sayHi = function() { alert("hi") }; //以某种方式来增强这个对象
    return clone;
}
  • 寄生组合式继承,集寄生式继承和组合继承的优点,是实现基于类型继承的最有效方法。前面我们知道组合继承最大的问题就是无论什么情况下都会调用两次父类构造函数,所以寄生组合式继承就是为了降低调用父类构造函数的开销而出现的,其思路是不必为了指定子类型的原型而调用超类的构造函数。
function extend(subClass, superClass) {
    var prototype = object(superClass.prototype); //创建对象
    prototype.constructor = subClass; //增强对象
    subClass.prototype = prototype; //指定对象
}

extend 的高效率体现在:它没有调用 superClass 构造函数,因此避免了在 subClass.prototype 上面创建不必要、多余的属性。并且能保持原型链不发生改变,因此还能正常使用 instanceof 和 isPrototypeOf() 方法。

  • 扩展:认识 new 运算符

new 运算符究竟干了什么:三件事

var foo = new F();

//做了三件事如下

var obj = {}; //创建一个空对象
obj.__proto__ = F.prototype; //将该空对象的__proto__指向F函数对象prototype
F.call(obj); //将F函数对象的this替换成obj,然后再调用F函数

注意:__proto__是指定原型的关键,通过设置 __proto__ 属性继承了父类。

根据MDN关于new的解释:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new

new constructor[(arguments)]

constructor 是一个指向对象实例的类型的类或函数。

new 用来创建一个用户自定义的对象类型的实例或一个具有原生构造函数(指语言内置的构造函数,通常用来生成数据结构)的内置对象(如 Boolean()、Number()、String()、Array()、Date()、Function()、RegExp()、Error()、Object() 等)的实例。

创建一个用户自定义的对象类型:

1. 编写函数定义对象类型(构造器其实就是一个普通的函数,当使用new来调用这个函数,它就可以被称为构造函数或构造方法

2. 通过 new 来创建对象实例

以上内容主要总结了传统的几种继承方式...苦于时间和理解层度所限,内容较杂乱浅薄,还有很多值得深入对比和缺失的内容。将来再完善吧。

2. Class 继承:extends

  • Class 可以通过关键字 extends 实现子类继承父类的所有属性和方法:

这比 ES5 的通过修改原型链实现继承,要清晰和方便很多

class Son extends Father {
    constructor(x, y, color) {
        super(x, y); //调用父类的constructor(x, y),super代表父类的构造函数,用来新建父类的this对象
        this.color = color;
    }

    toString() {
        return this.color + '' + super.toString(); //调用父类的toString(),super代表父类的构造函数,用来新建父类的this对象
    }
}

var foo = new Son(25, 2, 'red');
foo instance Son //true
foo instance Father //true

注意,作为子类,其必须在构造器 constructor 中调用 super ,并且调用之后才可以在子类中使用 this 关键字:这是因为子类自己的 this 对象,必须先通过父类的构造函数 super 完成塑造,得到与父类一致的属性和方法,super 会返回子类的实例(这里要特别注意,supper 虽然代表了父类的构造函数,并且新建了父类的 this对象,但是,它返回的结果是子类的实例,因为 super() === Father.prototype.constructor.call(this) ,所以此时通过 super 对某个属性赋值,即变成子类实例的属性),然后再对子类实例进行加工,加上子类自己的属性和方法。

所以 如果不调用 super,子类是得不到 this 对象的

ES5 继承实质 ES6 继承实质
先创建子类的实例对象 this,然后将父类的方法添加到 this 上,即 Parent.apply(this);,(constructor stealing方式) 先将父类实例对象的属性和方法,加到 this 上面,所以必须先调用 super,再用子类的构造函数修改 this
  • Class 继承中,子类会继承父类的静态方法
  • 一样可以使用 Object.getPrototyeOf() 从子类获取父类:Object.getPrototypeOf(Son) === Father // true。可以用来判断一个类是否继承了另一个类
  • super 关键字:

我们在上面提到 super 在子类继承中很重要,现在我们来认识一些它:

作为函数 作为对象
代表父类的构造函数。子类的构造函数中要求必须执行一次 super  函数。super() 等价于 Father.prototype.constructor.call(this) 在普通方法中指向父类的原型对象 super.method() === Father.prototype.method();在静态方法中指向父类(我们知道定义在class中的方法都是添加到prototype原型对象当中的。假如是静态方法static,那么是父类的,而不是prototype原型对象的

super 在普通方法中作为对象的示例:

class Father {
    p() { return 2; }
}
class Son extends Father {
    constructor() {
        super();
        console.log(super.p()); // 2
    }
}

上例要注意的是,由于 super 指向父类的原型对象,所以定义在父类实例(this)上的方法或属性,是无法通过 super 调用的。示例如下

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

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

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 调用父类的方法时,方法内部的 this 指向的是当前的子类,而不是子类的实例

  • 拓展:由于对象总是继承其他对象的,所以可以在任意一个对象中,使用 super 关键字:
var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]
  • Class 的 prototype 和 __proto__

ES5 中每个对象都有一个 __proto__ 属性,指向对于的构造函数的 prototype 属性。

ES6 Class 作为构造函数的语法糖,同时有 prototype 和 __proto__ 两个属性。因此存在两条继承链

(1) 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。

(2) 子类的 prototype 属性的 __proto__ 属性,表示方法的继承(定义在class里的方法都会添加到prototype原型对象上),总是指向父类的 prototype 属性。

class Father() {}

class Son extends Father {}

Son.__proto__ === Father //true
Son.prototype.__proto__ === Father.prototype //true

这两条继承链,可以这样理解:

  1. 作为一个对象,子类的原型(__proto__属性)是父类;
  2. 作为一个构造函数,子类的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。
var B = Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

//我们知道,在 ES5 中,通过 object.create() 方法规范化了原型式继承。
  • Class 中子类实例的 __proto__ 属性

子类实例的 __proto__ 属性的__proto__ 属性,指向的就是父类实例的 __proto__ 属性。也就是说,子类的原型的原型,是父类的原型。

var p1 = new Father(2, 3);
var p2 = new Son(2, 3, 'red');

p2.__proto__.__proto__ === p1.__proto__ // true

因此,通过子类实例的 xx.__proto__.__proto__ ,可以修改父类实例的行为。

var p1 = new Father(2, 3);
var p2 = new Son(2, 3, 'red');

//向Father类添加方法,会影响到Son类的实例
p2.__proto__.__proto__.printName = function () {
  console.log('Ha');
};

p1.printName() // "Ha"

猜你喜欢

转载自blog.csdn.net/hoanFir/article/details/87351590
今日推荐