如何确定原型和实例的关系???

参考:【原型】instanceof | isPrototypeOf

原型链的问题:

  1. 问题一: 当原型链中包含 引用类型值的原型时 ,该引用类型值会被所有实例共享;
  2. 问题二:在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数。

由于以上原型链的问题,实际中很少单独使用原型链来实现继承,多数情况下用以下继承来弥补原型链中的不足:

1. 原型链继承

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

  1. function SuperType() {
  2. this.attr = 'SuperType'
  3. // 新加一个引用类型,让其暴露出原型链继承问题一
  4. this.info = { name: 'chen' };
  5. }
  6. SuperType.prototype.getSuperTypeValue = function () {
  7. return this.attr
  8. }
  9. function SubType() {
  10. this.subAttr = 'SubType'
  11. }
  12. SubType.prototype.getSubTypeValue = function () {
  13. return this.subAttr
  14. }
  15. // 重点:创建 SuperType 实例,并将该实例赋值给 SubType.prototype
  16. SubType.prototype = new SuperType()
  17. let sub = new SubType()
  18. console.log(sub.getSuperTypeValue()) // SuperType
  19. sub.info; // {name: "chen"}
  20. sub.info.name = 'lu'
  21. let sub_02 = new SubType();
  22. sub_02.info; // {name: "lu"} 引用类型被各个实例共享后,一改全改,牵一发而动全身

缺点:多个实例对引用类型的操作会被篡改。

2. 借用构造函数

为解决原型链中上述的两个问题,则使用 借用构造函数(也称:经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数。

  1. function Father() {
  2. this.colors = ['red', 'green', 'blue'];
  3. }
  4. function Son() {
  5. Father.call(this); // 继承了 Father ,并向父类型传递参数
  6. }
  7. let instance1 = new Son();
  8. instance1.colors.push('black');
  9. instance1.colors; // 'red', 'green', 'blue', 'black'
  10. let instance2 = new Son();
  11. // 可见引用类型值是独立的
  12. instance2.colors; // 'red', 'green', 'blue'

很明显,借用构造函数一举解决了原型链的两大问题:

  1. 保证了原型链中引用类型值的独立,不再被所有实例共享;
  2. 子类型创建时也能够向父类型传递参数。

借用构造函数实现继承缺点:

  1. 方法都在构造函数中定义,复用函数是不可能了;
  2. 超类型构造函数中定义的方法,对子类型是不可见的。

另外一版:
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)

  1. // 父类
  2. function SuperType() {
  3. this.color = ['red', 'yellow', 'pink']
  4. }
  5. // 子类
  6. function SubType() {
  7. SuperType.call(this)
  8. }
  9. let sub1 = new SubType()
  10. sub1.color.push('blue')
  11. console.log('sub1.color :>> ', sub1.color)
  12. let sub2 = new SubType()
  13. console.log('sub2.color :>> ', sub2.color)

核心代码是SuperType.call(this),创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将 SuperType 中的属性复制一份。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

    3. 组合继承

    组合继承(伪经典继承),指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。

    基本思路:使用原型链实现对原型属性和方法的继承,借用构造函数来实现对实例属性的继承。

这样,即通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。代码如下:

  1. function Father(name) {
  2. this.name = name;
  3. this.colors = ['red', 'green', 'blue'];
  4. }
  5. Father.prototype.sayName = function() {
  6. console.log(this.name);
  7. }
  8. function Son(name, age) {
  9. Father.call(this, name); // 继承实例属性,第一次调用 Father()
  10. this.age = age;
  11. }
  12. Son.prototype = new Father(); // 继承父类方法,第二次调用 Father()
  13. Son.prototype.sayAge = function() {
  14. console.log(this.age);
  15. }
  16. let instance1 = new Son('chen', 29);
  17. instance1.colors.push('black');
  18. instance1.colors; // 'red', 'green', 'blue', 'black'
  19. instance1.sayName(); // chen
  20. instance1.sayAge(); // 29
  21. let instance2 = new Son('lu', 99);
  22. instance1.colors; // 'red', 'green', 'blue'
  23. instance1.sayName(); // lu
  24. instance1.sayAge(); // 99

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为 JS 中最常用的继承模式。
同时, instanceofisPrototypeOf() 也能用于识别基于组合继承的对象。
缺点:调用两次父类构造函数,造成不必要的消耗。

另外一版:
组合上述两种方法就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承。

  1. function SuperType(name) {
  2. this.name = name
  3. this.color = ['red', 'yellow', 'pink']
  4. }
  5. SuperType.prototype.sayName = function() {
  6. console.log(this.name)
  7. }
  8. function SubType(name, age) {
  9. // 继承属性
  10. // 第二次调用SuperType()
  11. SuperType.call(this, name)
  12. this.age = age
  13. }
  14. // 继承方法
  15. // 构建原型链
  16. // 第一次调用SuperType()
  17. SubType.prototype = new SuperType()
  18. // 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
  19. SubType.prototype.constructor = SubType
  20. SubType.prototype.sayAge = function() {
  21. console.log(this.age)
  22. }
  23. let sub3 = new SubType('che', 10)
  24. sub3.color.push('blue')
  25. console.log(sub3.color)
  26. console.log(sub3.sayName())
  27. console.log(sub3.sayAge())
  28. let sub4 = new SubType('ga', 30)
  29. sub4.color.push('tea')
  30. console.log(sub4.color)
  31. console.log(sub4.sayName())
  32. console.log(sub4.sayAge())

组合继承.png
缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象 SubType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

4. 原型式继承

该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:

在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

  1. function object(o) {
  2. function F() {}
  3. F.prototype = o;
  4. return new F();
  5. }

从本质上来看,object() 返回了一个引用传入对象的新对象,这样可能带来一些共享数据的问题,代码如下:

  1. var person = {
  2. friends : ["Van","Louis","Nick"]
  3. };
  4. var anotherPerson = object(person);
  5. anotherPerson.friends.push("Rob");
  6. var yetAnotherPerson = object(person);
  7. yetAnotherPerson.friends.push("Style");
  8. alert(person.friends);//"Van,Louis,Nick,Rob,Style"

在这个例子中,可以作为另一个对象基础的是person对象,于是我们把它传入到 object() 函数中,然后该函数就会返回一个新对象. 这个新对象将 person 作为原型,因此它的原型中就包含引用类型值属性. 这意味着 person.friends 不仅属于 person 所有,而且也会被 anotherPerson 以及 yetAnotherPerson 共享.

ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.

object.create() 接收两个参数:

  • 一个用作新对象原型的对象
  • (可选的)一个为新对象定义额外属性的对象
  1. var person = {
  2. friends : ["Van","Louis","Nick"]
  3. };
  4. var anotherPerson = Object.create(person);
  5. anotherPerson.friends.push("Rob");
  6. var yetAnotherPerson = Object.create(person);
  7. yetAnotherPerson.friends.push("Style");
  8. alert(person.friends);//"Van,Louis,Nick,Rob,Style"

object.create() 只有一个参数时功能与上述object方法相同。

它的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

  1. var person = {
  2. name : "Van"
  3. };
  4. var anotherPerson = Object.create(person, {
  5. name : {
  6. value : "Louis"
  7. }
  8. });
  9. alert(anotherPerson.name);//"Louis"

目前支持 Object.create() 的浏览器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.
提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样。

另外一版:

  1. function O(obj) {
  2. // 重点
  3. function F() {}
  4. F.prototype = obj
  5. return new F()
  6. }
  7. let i = {
  8. name: 'lu',
  9. list: ['a', 'b', 'c']
  10. }
  11. let sub4_1 = O(i)
  12. // console.log(sub4_1);
  13. sub4_1.name = 'ui'
  14. sub4_1.list.push('d')

O()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
原型式继承.png
缺点:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

另外,ES5中存在Object.create()的方法,能够代替上面的object方法。

5. 寄生继承

核心:在原型式继承的基础上,增强对象,返回构造函数。

  1. /*
  2. 5. 寄生继承
  3. 宿主是谁?也就是寄生到哪里了。
  4. */
  5. function createO(original) {
  6. const clone = O(original) // 通过调用 O() 函数创建一个新对象
  7. // 以某种方式来增强 clone 对象
  8. clone.sayHi = function() {
  9. alert('Hi')
  10. }
  11. return clone
  12. }
  13. let sub5_1 = createO(i)
  14. console.log(sub5_1);

寄生继承.png
函数的主要作用是为构造函数新增属性和方法,以增强函数
缺点(同原型式继承):

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数

    6. 寄生组合式继承

    结合借用构造函数传递参数和寄生模式实现继承。

  1. /*
  2. 6. 寄生组合继承(常用)
  3. */
  4. function inheritO(Sub, Super) {
  5. const attr = Object.create(Super.prototype) // 创建对象,创建父类原型的一个副本
  6. attr.constructor = Sub // ? 增强对象,弥补因重写原型而失去默认的 constructor 属性
  7. Sub.prototype = attr // 指定对象,将新创建的对象赋值给子类原型
  8. }
  9. // 父类初始化实例属性和原型属性
  10. function Super(name) {
  11. this.name = name
  12. this.list = [1, 2, 3]
  13. }
  14. Super.prototype.sayName = function() {
  15. console.log(this.name)
  16. }
  17. // 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
  18. function Sub(name, age) {
  19. Super.call(this, name)
  20. this.age = age
  21. }
  22. // 将父类原型指向子类
  23. inheritO(Sub, Super)
  24. // 新增子类原型属性
  25. Sub.prototype.sayAge = function() {
  26. console.log(this.age)
  27. }
  28. let sub6_1 = new Sub('ma', 34)
  29. console.log(sub6_1)

寄生组合式继承.png
这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在Sub.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法

7. 混入方式继承多个对象

  1. function MyClass() {
  2. SuperClass.call(this)
  3. OtherSuperClass.call(this)
  4. }
  5. // 继承一个类
  6. MyClass.prototype = Object.create(SuperClass.prototype)
  7. // 混合其它
  8. Object.assign(MyClass.prototype, OtherSuperClass.prototype)
  9. // 重新指定 constructor
  10. MyClass.prototype.constructor= MyClass
  11. MyClass.prototype.myMethod = function() {
  12. // do something
  13. console.log('myMethod')
  14. }

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

8. ES6 Class 继承 extends

  • 一个类只能有一个 constructor ,若显式写入多个 constructor 会报错 SyntaxError 。
  • 若没有显式指定 constructor ,则会添加默认的 constructor 方法。
  • 若子类中显式指定 constructor 函数,则在此函数第一行调用 super() 方法。

    1. /*
    2. extends 继承核心代码如下,其实和寄生组合式继承一样
    3. */
    4. function inherits(Child, Parent) {
    5. Child.prototype = Object.create(Parent && Parent.prototype, {
    6. constructor: {
    7. value: Child,
    8. enumerable: false,
    9. writable: true,
    10. configurable: true
    11. }
    12. })
    13. if (Parent) {
    14. // TODO
    15. Object.setPrototypeOf ? Object.setPrototypeOf(Child, Parent) : Child.__proto__ = Parent
    16. }
    17. }

    总结:

  • 函数声明和类声明的区别
    函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError

  • ES5 继承和 ES6 继承的区别
    ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this
    Parent.call(this));

ES6 的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。