JS进阶之原型、原型链、原型继承

120 阅读5分钟

  携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情 >>

1. 原型

  • 原型:prototype(显示原型)、proto(隐世原型)。
  • 构造器:constructor。

每个对象以及函数等等都有自己的原型,在它的属性中都存在一个 proto 的属性,这个属性指向的就是它们自己的原型对象。

下面这个例子中,先创建了一个 Parent() 构造函数,然后 new 了个实例 p1,而这个实例 p1 的 proto 原型中就包含 constructor 构造器属性,这个构造器永远指向本身实例的构造函数,这里可以看见, 它指向了 Parent()。

而 Parent() 函数的 proto 下的 construcor 属性则是指向了 Object。(每个原型的最终指向都是 Object。)

现在往 Parent 的原型上添加一个 sex 属性。可以发现,这个属性会被实例 p1 继承。其实可以说 prototype 存在的目的就是被实例继承。

现在修改 p1 的 sex 属性会发现实例 p1 的 sex 属性被修改,但是构造函数 Parent 上的 sex 没有变化的。

输出 p1,可以发现 p1 上有三个属性,它的原型 Parent 上只有 sex 一个属性,且这两个同名属性,在 p1 使用时,用的是 p1 自身的属性,而不是继承而来的。

现在在为 Parent 的原型上添加一个 id 属性。并输出 p1.id 会发现输出的就是 0。

虽然 p1 中没有这个属性,但是 p1 会在原型中查找的。当在原型中查找到对应的属性时,就会直接使用原型上的属性。

在调用实例的某一个属性时,js 会先从实例的祖先原型上进行查找是否有这个属性,如果有就会拿到这个属性。拿到后还会向上继续查找同名属性,如果查找到了,就会替换这个属性的值。

这里就可以得出两个结论:

  1. 实例上的属性与继承属性同名时会使用实例自身的属性。
  2. 属性的查找规则,是从下往上,先从原型上查找,再去查找自身。

\

Tips:

  1. proto 与 constructor 属性是对象所独有的。
  2. prototype 属性是函数独有的。

2. 原型链

构造函数与实例以及一直到最后 Object.prototype 之间形成的一条链就是原型链。

在上一个例子中,从 p1 实例到最终 Object 的指向过程,就是一个原型链。

上个例子原型指向:p1 --> Parent --> Object。

2. 关于原型的一些问题

  1. 构造函数与原型、实例之间的关系?
  • 构造函数中有个 prototype 指向原型,原型中有个 constructor 指向构造函数。
  • 实例中则有个 proto 指向原型,且构造函数和实例之间通过原型产生关系,他们本身没有直接的联系。
  1. prototype 和 proto 的关系?
  • 所有的实例对象都拥有 _ proto_ 属性,它指向构造函数的 prototype 原型对象,最后指向 Object.prototype。
  • 所有的函数都同时拥有 proto 和 prototype 属性,函数的 proto 指向自己的函数实现,函数的 prototype 是一个对象,所以函数的 prototype 也有 proto 属性,指向 Object.prototype。
  1. 原型链最后的指向是什么?
  • 指向 Object.prototype.proto,且它的值是:null。

3. 继承

下面例子共用父类代码:

// 声明一个父构造函数
function Person(name, age){
  // 添加两个属性一个方法
  this.name = name
  this.age = age
  this.run = function(){
    console.log(`${this.name} is ${this.age}岁`)
  }
}

// 在父类原型上添加属性
Person.prototype.sex = '男'

// 在父类上添加方法
Person.prototype.work = function(){
  console.log(`${this.name} is ${this.sex} and ${this.age}岁`)
}

1. 对象冒充继承

但没法继承父类原型链上的属性和方法。

因为它并没有继承父类的原型,仅仅只是将父类在自身作用域中调用了一遍。(你以为你实例化的是 Man?其实是 Person。)

  • call()
// 子类
function Man(name, age){
  // 使用 call 在 Man 的上下文中调用 Person。
  // 这里如果不使用 call,那么 Person 的作用域将会在全局。
  // name 和 age 也无法在 Man 的原型上找到。
  Person.call(this, name, age)
}

// 实例化 man
var man = new Man('sally', 18)
// 实例化 person
var person = new Person('jack', 21)

// 输出:sally is 18
man.run()
// 输出:jack is 男 and 21岁
person.work()

// 报错:man.work is not a function
man.work()
// 报错:man.sex is not a undefined
console.log(man.sex)

2. 原型链继承

可以继承构造函数里面以及原型链上面的属性和方法,但实例化时候没法给父类传参。

  • 将父类添加到子类的原型上。
// 子类
function Man(name, age){}
// 将父类添加到子类的原型链上
Man.prototype = new Person()

// 就算实例化时传入参数也没用,父类无法接收。
var man = new Man('sally', 18)
// 实例化 person
var person = new Person('jack', 21)

// 输出:undefined is undefined 岁
man.run()
// 输出:jack is 男 and 21岁
person.work()

// 输出:undefined is 男 and undefined岁
man.work()
// 输出:男
console.log(man.sex)

3. 采用原型链继承和对象冒充继承相结合的方式继承(组合继承)

完善了原型链与对象冒充继承的缺点,但是耗费内存(继承过程调用了两次父类构造函数)。

  • call()
  • 将父类添加到子类的原型上。
// 子类
function Man(name, age){
  // 先使用 call 在子类中执行一遍
  Person.call(this, name, age)
}

// 再将父类加入子类的原型链
Man.prototype = new Person()
var man = new Man('sally', 18)
// 实例化 person
var person = new Person('jack', 21)

// 输出:sally is 18岁
man.run()
// 输出:jack is 男 and 21岁
person.work()

// 输出:sally is 男 and 18岁
man.work()
// 输出:男
console.log(man.sex)

4. 原型式继承

先创建一个函数,然后将这个函数的原型指向要继承的构造函数,最后将这个新函数当做一个新对象返回出去。

  1. 创建空函数。
  2. 将父类添加到这个空函数的原型上。
  3. 实例化这个空函数并返回出去。
// 将这个函数看做是个中间相函数,负责连接父类与子类
function content(obj){
  // 创建一个新函数
  function F(){}
  // 将传进来的父类指向这个新函数
  F.prototype = obj
  // new 这个新函数并返回出去
	return new F()
}

// new 一个父类
var man = new Person('sally', 18)
// 声明子类,并调用中间项函数将父类传进去
var man1 = content(man)
// 实例化 person
var person = new Person('jack', 21)

// 输出:sally is 18岁
man1.run()
// 输出:jack is 男 and 21岁
person.work()

// 输出:sally is 男 and 18岁
man1.work()
// 输出:男
console.log(man.sex)