javascript继承

230 阅读5分钟

Javascript继承

你从祖先手里继承的遗产要努力利用,才能安享。--《浮士德》

javascript中谈继承,我们讨论的是两个对象(类)之间的关系:子对象拥有了父对象的属性和方法。不管何时,「复制」都是好方法,浅复制或深复制设情况而定,都能使一个对象拥有另一个对象的属性和方法;除此之外,javascript继承主要利用其「原型链」的方法来实现。本文假定读者了解构造函数、实例、原型以及它们之间的关系,我们来看看是怎么完成继承的。

原型链

每个实例对象(object )都有一个私有属性(称之为 _proto_)指向它的原型对象。该原型对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。原型链给与继承的启示在于我们可以改变「对象的原型指针」,指向被继承的对象,并通过原型链访问到。

原型实例继承

function Fruit() {
  this.category = 'fruit'
}

Fruit.prototype.showName = function () {
  console.log(this.name)
}

function Apple(name, color) {
  this.name = name
  this.color = color
}

var fruit = new Fruit()

Apple.prototype = fruit // *
Apple.prototype.constructor = Apple // **

var apple = new Apple('apple', 'red');

console.log(apple.category) // fruit
apple.showName() // apple
console.log(apple.constructor) // Apple

带*号是重点函数。看第一行,首先找到要继承的对象的原型,即apple的原型Apple.prototype,将其指向被继承实例fruit。当访问apple.category时,可通过原型链访问到fruit.category,值是'fruit',不是undefined;当访问showName方法时,apple没有,fruit也没有,最终在fruit的原型Fruit.prototype找到。所以一行代码完成了‘继承’。

第二行是因为构造函数的prototype有内置属性constructor,它的值指向构造函数本身,即Apple.prototype.constructor === Apple 为 true。我们将Apple.prototype重新赋值,它的constructor属性也丢失,需要手动将其设定回去

ps. 如果不设定回去,apple.constructor的值就是Fruit:同样是沿着原型链,apple-->Apple.prototype(fruit)--> Fruit.prototype,指向Fruit。

此种方法完成的继承最大的不足有两点:

  • 每次继承都要有形成一个实例,如var fruit = new Fruit(),这是一个开销
  • 父类实例的属性,如fruit.category出现在原型链中,被每个子类实例apple继承;假如父类实例的属性是对象或数组,则原型链中两个对象的属性会相互影响,如下
function Fruit() {
  this.colors = ['red', 'green', 'yellow']
}

function Apple(name) {
  this.name = name
}
var fruit = new Fruit()
Apple.prototype = fruit
Apple.prototype.constructor = Apple

var apple = new Apple('apple');
apple.colors.push('blue')
console.log(apple.colors) // 'red', 'green', 'yellow', 'blue'

var apple2 = new Apple()
console.log(apple2.colors) // 'red', 'green', 'yellow', 'blue'

我们可以看到apple2的colors也是4个,因为fruit的colors是引用类型,所有apple实例都共享这一属性。

为解决上述问题,人们提出了两个方法:借助构造函数及直接原型继承

借用构造函数

function Fruit() {
  this.category = 'fruit'
}

function Apple(name, color) {
  Fruit.apply(this, arguments) // **
  this.name = name
  this.color = color
}

var apple = new Apple('apple', 'red');

console.log(apple.category) // fruit

我们知道,构造函数的this指向它的实例,此为apple。我们利用函数apply或call指定this的能力,在构造函数中执行父级构造函数,即Fruit.apply(this, arguments),结果就是子类复制了父类的属性。

直接原型继承

function Fruit() {
  this.category = 'fruit'
}

Fruit.prototype.showName = function () {
  console.log(this.name)
}

function Apple(name, color) {
  Fruit.apply(this, arguments) // *
  this.name = name
  this.color = color
}

Apple.prototype = Fruit.prototype // **
Apple.prototype.constructor = Apple 

var apple = new Apple('apple', 'red');

console.log(apple.category) // fruit
apple.showName() // apple

这一次我们不继承父类实例,直接继承父类原型,解决了无谓的新建父类实例的开销。这种情况下,父类的属性没有继承,好在我们通过「借助构造函数」已解决。此模式的缺点在于:Apple.prototype和Fruit.prototype会相互影响。解决的方法在于形成一个空的构造函数。

空构造函数

function Fruit() {
  this.category = 'fruit'
}

Fruit.prototype.showName = function () {
  console.log(this.name)
}

function Apple(name, color) {
  Fruit.apply(this, arguments) 
  this.name = name
  this.color = color
}

function extend(Child, Parent) {  // **
  function Foo() {} // 空的函数,嫁接原先直接原型赋值
  F.prototype = Parent.prototype 
  var foo = new Foo()
  Child.prototype = foo // 子元素再指向嫁接的元素
  Child.prototype.constructor = Child
  Child.uber = Parent.prototype
}

extend(Apple, Fruit)

var apple = new Apple('apple', 'red');

console.log(apple.category) // fruit
apple.showName() // apple

我们创建了一个空的构造函数,新建一个空的实例,由于都是空的,并不费太多性能。并且解决了直接原型继承的问题。此方法非常有效,ECMAScript直接创建Object.create(obj)实现上述功能。

Object.create(obj)


var fruit = {
  category: 'fruit',
  showCategory: function() {
    console.log(this.category)
  }
}

var apple = Object.create(fruit)

console.log(apple.category) // fruit
apple.showCategory() // fruit

Object.create会创建一个对象,以参数对象为原型。这样我们就不需要烦人的原型对象、实例、构造函数等概念。与Object.create()类似的还有Object.setPrototypeOf(obj, prototype)其中obj是要继承的子类,prototype是子类的原型,这里不再详述。

ES6 class

class Fruit {
  constructor(category) {
    this.category = category
  }
  showCategory() {
    console.log(this.category)
  }
}

class Apple extends Fruit {
  appleShowCategory() {
    super.showCategory()  // super代表父类
  }
}

var apple = new Apple('fruit')
apple.appleShowCategory()  // fruit

ES6通过extends就能直接继承!class里面constructor里面的属性就是原先构造函数的属性,class里面的方法就是原先构造函数prototype里面的方法。对比下面原来的方法,就会发现ES6使用起来简单许多。


function Fruit(category) {
  this.category = category
}
Fruit.prototype.showCategory = function() {
  console.log(this.category)
}

function Apple(category) {
  Fruit.call(this, category)
}

function extend(Child, Parent) {
  function F() {} 
  F.prototype = Parent.prototype 
  var foo = new F()
  Child.prototype = foo
  Child.prototype.constructor = Child
  Child.uber = Parent.prototype
}

extend(Apple, Fruit)

var apple = new Apple('fruit');
apple.showCategory()

总结

JavaScript的继承,看似复杂,有多种模式。细究原理,你会发现都是「原型继承」。这种继承,也可以称为委托。因为实际上没有获得继承对象的功能,而是在没办法解决的时候,交由原型去处理--也就是委托给原型。跟事件委托有些类似。了解到js继承跟传统OO语言继承方式的不同,也有助于加深我们对javascript的了解。到了ES6 class写法,继承写起来已变得很简单,但我们不要忘了背后的进化史,没有黑魔法,都是对语言的应用罢了。