JavaScript原型对象5-继承的两种方式

261 阅读4分钟
function Person() {}
Person.prototype.dance = function () {}

function Ninja() {}
Ninja.prototype = new Person()

为了实现Ninja继承Person,且能用Person的dance方法

两种继承方式:

  • Ninja.prototype = new Person()
  • util.inherits(Ninja, Person)

两种都是Ninja继承Person,区别是什么?

1. Ninja.prototype = new Person()

这种方式就是让 Ninja的原型指向Person的实例对象

1.png

如图显示了当定义一个Person函数时,同时也创建了Person原型,

该原型通过constructor属性引用函数本身。

正常来说,我们可以使用附加属性扩展Person原型,

在本例中,我们在Person的原型上添加了一个dance方法,

因此每个Person的实例对象也都有dance方法:

function Person() {}
Person.prototype.dance = function() {}

我们也定义了一个Ninja函数。

该函数的原型也具有一个constructor属性指向函数本身

function Ninja() {}

接下来为了实现继承,将Ninja的原型赋值为Person的实例。

现在,每当创建一个新的Ninja对象时, 新创建的Ninja对象的原型(proto)将设置为 Ninja的原型属性(prototype)所指向的对象,即Person实例:

Ninja.prototype = new Person()
var ninja = new Ninja()

尝试通过Ninja对象访问dance方法,JavaScript运行时将会首先查找Ninja对象本身

由于Ninja对象本身不具有dance方法,接下来搜索Ninja对象的原型即Person对象。

Person对象也不具有dance方法,所以再接着查找Person对象的原型,

最终找到了dance方法。

这就是在JavaScript中实现继承的原理!

问题:

如果我们仔细观察上面的图,

通过设置Person实例对象作为Ninja的构造器的原型时,

我们已经丢失了Ninja与Ninja初始原型之间的关系

这是一个问题,

因为constructor属性可用于检测一个对象是否由某一个函数创建的。

我们代码的使用者有这样一个非常合理的假设,运行一下测试会通过

console.log(ninja.constructor === Ninja)

但是在目前的程序状态中,这个测试无法通过,

因为其无法找到Ninja对象的constructor属性。

回到原型上,原型上也没有constructor属性,继续在原型链上追溯,

在Person对象的原型上具有指向Person本身的constructor属性。

事实上,如果我们询问Ninja对象的构造函数,我们得到的答案是Person,

但这个答案是错误的,这可能是某些严重缺陷的来源。

即这种new方式的继承又一个问题就是: 我们丢失了Ninja与Ninja初始原型之间的关系 随即影响了Ninja实例与Ninja初始原型之间的关系 解决方式是: Object.defineProperty

'use strict'

function Person() {}
Person.prototype.dance = function () {}

function Ninja() {}
Ninja.prototype = new Person()

Object.defineProperty(Ninja.prototype, 'constructor', {
    enumerable: false,
    value: Ninja,
    writable: true
})

var ninja = new Ninja()

2.png

2. util.inherits(Ninja, Person)

先看俺Object.create的源码

Object.create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

可以看出来。
Object.create是内部定义一个对象,
并且让F.prototype对象 赋值为引进的对象/函数 o,
并return出一个新的对象。

即Ob*ject.create出来的 是一个实例对象(空对象),

其__proto__指向 引进的对象/函数o

再看看inherits源码

function inherits(ctor, superCtor) {
  ctor.super_ = superCtor
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};

这里就是

Ninja 多了一个属性 _super 指向 Person

Ninja原型 的prototype 指向了一个 新创建的对象

这里的新创建是:

Ninja.prototype = Object.create(Person.prototype, {
    constructor: {
      value: Ninja,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });

就是说 Ninja的原型 指向了一个新创建的对象,

该对象是空对象{},

但该空对象的原型 指向 Person的原型对象

而且该空对象有一个constructor属性 指向 Ninja

总结

第二种继承方法 
近乎等于
第一种继承方法 + Object.defineProperty

这里是近乎等于是因为,他们之间还有一个区别:

  • 第一种继承方式,其对象是 Person的实例对象(所以它拥有Person的属性及方法)

  • 第二种继承方式,其对象是 一个空对象( {} )

但它们都拥有Person原型的属性方法(因为它们的原型指向Person的原型)

它们以不同的方式使得

Ninja的原型 指向该 对象
该对象的[[prototype]] 指向 Person原型对象
该对象的constructor     指向 Ninja函数

例子

第一个实例
'use strict'

function Person() {
    this.name = 'linzx'
}

Person.prototype.dance = function () {
    return 'linzx'
}

function Ninja() {}

Ninja.prototype = new Person()

Object.defineProperty(Ninja.prototype, 'constructor', {
    enumerable: false,
    value: Ninja,
    writable: true
})

var ninja = new Ninja()
console.log(ninja.name)         //拥有Person上的属性、方法
console.log(ninja.dance())      //拥有Person原型上的属性、方法

/*
linzx
linzx
*/

第二个例子
'use strict'
var util = require('util')

function Person() {
    this.name = 'linzx'
}

Person.prototype.dance = function () {
    return 'linzx'
}

function Ninja() {}
// Ninja.prototype = new Person()
//
// Object.defineProperty(Ninja.prototype, 'constructor', {
//     enumerable: false,
//     value: Ninja,
//     writable: true
// })

util.inherits(Ninja, Person)
var ninja = new Ninja()
console.log(ninja.name)         //不拥有Person上的属性、方法
console.log(ninja.dance())      //拥有Person原型上的属性、方法

/*
undefined
linzx
*/