原型与继承

128 阅读4分钟

构造函数与实例

JavaScript 中,只要我们创建一个新函数,该函数就会创建一个 prototype 属性,这个属性指向函数的原型对象,原型对象包含一个 constructor 属性,这个属性又指回了这个构造函数本身。

function Person(){ }
Person.prototype.foo = 1
console.dir(Person)
console.dir(Person.prototype.constructor)

当我们使用 new 操作符调用构造函数创建一个实例时,实例会有一个 __proto__属性,如下图:

function Person(){ }
Person.prototype.name = 'xiaoming'
Person.prototype.age = '18'
Person.prototype.getName = function(){
  return this.name
}
console.dir(Person)
const p1 = new Person()
console.log(p1)

可以发现 new 调用构造函数后 ,实例的 __proto__ 指向了构造函数的 prototype

上面代码中__proto__prototypeconstructor 三者关系如下图:

原型链

我们通过打印一个 函数 发现 函数 本身也有 __proto__ 属性,而这个 __proto__ 指向的原型还有一个 __proto__属性

通过多个 __proto__ 可以把对象的原型链接成为原型链。所有函数的原型链最终都指向 Function.prototype, 所有对象的原型链最终都指向 Object.prototype, 而 Function.prototype__proto__ 指向 Object.prototypeObject.prototype__proto__ 指向 null

下图可以清晰的看到原型链的完整情况:

http://www.mollypages.org/tutorials/js.mp

继承

在面向对象语言的开发中,当对象a 需要借用 对象b 的能力时,通常会使用到 接口继承实现继承,比如 java 中我们可以定义 抽象类接口 的方式实现两种不同的继承,而js 由于没有函数签名,只支持实现继承,由于没有 的概念,所以主要通过 原型链 实现继承。

原型链继承

function Person(){ 
  this.name = 'xiaoming'
 }
Person.prototype.getName = function(){
  return this.name
}
function Worker(){ 
  this.age = '18'
}
Worker.prototype = new Person()
Worker.prototype.getAge = function(){
  return this.age
}
const worker = new Worker()
console.log(worker) // Worker{age : 18}
console.log(worker.getName()) // xiaoming
console.log(worker.getAge()) // 18

上面的代码中,我们将构造函数 Worker 的原型指向了 Person 的实例,这样我们就可以在 Worker 的实例中调用 getName 方法了,实现了继承 Person 的方法与属性,原型链如下图:

这里虽然实现了继承,但是存在一个问题:

Person 构造函数中的 name 属性跑到了 Worker 构造函数的原型对象中。

如果在 Person 构造函数中定义的是一个引用类型的变量,那么这个变量被添加到 Workerprototype 之后,就会被所有 Worker 创建的实例共享:

function Person(){ 
  this.arr = ['1']
}
function Worker(){ }
Worker.prototype = new Person()
const worker1 = new Worker()
const worker2 = new Worker()
worker1.arr.push('2')
console.log(worker1.arr) // [1,2]
console.log(worker2.arr) // [1,2]

借用构造函数

为了解决引用类型变量的共享问题,我们可以借用 callapply 方法

function Person(){ 
  this.arr = ['1']
}
function Worker(){ 
  Person.call(this)
}
Worker.prototype = new Person()
const worker1 = new Worker()
const worker2 = new Worker()
worker1.arr.push('2')
console.log(worker1.arr) // [1,2]
console.log(worker2.arr) // [1]

在调用 Worker 构造函数时,我们直接为每个实例添加了 arr 属性,那么当实例在获取 arr 属性时,首先找的就是自身的 arr 而不会去找原型对象上那个共享的 arr

寄生组合继承

虽然不会共享属性了,但是现在 Worker 构造函数上依然又 arr 这个属性,我们并不需要它。

实际上我们需要的只是 Personprototype,我们可以提取 Personprototype ,然后生成一个只含有 prototype 的构造函数

function extractProto(obj){
  function F(){}
  F.prototype = obj
  return new F()
}
function Person(){ 
  this.arr = ['1']
}
function Worker(){ 
  Person.call(this)
}
Worker.prototype = extractProto(Person.prototype)

console.log(Worker.prototype)

Object.create() 同样可以实现我们定义的 extractProto 的行为

function Person(){ 
  this.arr = ['1']
}
function Worker(){ 
  Person.call(this)
}
Worker.prototype = Object.create(Person.prototype)

console.log(Worker.prototype)

constructor 的问题

一个构造函数在正常情况下,它的原型对象中的 constructor 属性指向本身,但是我们上面的代码在实现继承之后,constructor 的指向都出现了问题:

function Person(){ }
function Worker(){ }
Worker.prototype = new Person()
console.log(Person.prototype.constructor) //ƒ Person(){ }
console.log(Worker.prototype.constructor) //ƒ Person(){ }

这里的 Worker.prototype.constructor 指向了 Person 构造函数,这是因为现在 Worker.prototype 指向了 Person 构造函数的实例,当调用 Worker.prototype.constructor时,实际上去寻找了 Person 构造函数的实例上的 constructor 属性,没有找到接着往 Person.prototype 上找,找到了 Person.prototype.constructor 就是 Person 构造函数。

因此我们在重新赋值构造函数的 prototype 之后,需要将其 constructor 指向正确的构造函数:

function Person(){ }
function Worker(){ }
Worker.prototype = new Person()
Worker.prototype.constructor = Worker
console.log(Person.prototype.constructor) //ƒ Person(){ }
console.log(Worker.prototype.constructor) //ƒ Worker(){ }

class 与 extend 实现继承

ES6中有了 classextend 关键字 , 但其本质上只是 prototype 的语法糖

  • prototype 实现继承
function Person(name) {
  this.name = name
}
Person.prototype.getName = function() {
  console.log(this.name)
}

function Worker(name, age) {
  Person.call(this, name)
  this.age = age
}
Worker.prototype = Object.create(Person.prototype)
Worker.prototype.getAge = function(){
  console.log(this.age)
}
Worker.prototype.constructor = Worker

const worker = new Worker('xiaoming', 18)

worker.getName() // xiaoming
worker.getAge() // 18
  • class 实现继承
class Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    console.log(this.name)
  }
}
class Worker extends Person {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  getAge(){
    console.log(this.age)
  }
}
let worker = new Worker('xiaoming', 18)
worker.getName() // xiaoming
worker.getAge() // 18

上面分别用 prototypeclass 实现了继承,class 相比 prototype 更加直观,class 实现的方式也不存在 constructor 的问题。


原文地址

参考