RadiomM
文章22
标签13
分类1
原型、原型链和继承

原型、原型链和继承


因为离职的原因,最近一直在复习以前的面试基础知识,碰巧之前对原型的理解其实只有个大概,对这里面的知识不是完全的理解,所以写这篇文章来总结一下。(主要是面试的时候,肚子里有墨水,但是不知道如何表达)

什么是原型

其实总结原型主要是下面三个方面:

  • 构造函数拥有一个prototype属性,指向构造函数的原型对面,原型对象的construtor指向构造函数本身
  • 实例对象拥有一个__proto__属性
  • 实例对象的__proto__指向的是构造函数的prototype属性

下面开始写代码来验证:

构造函数拥有prototype属性

1
2
3
4
5
6
7
8
9
10
function Dog(name, age) {
this.name = name
this.age = age

Dog.prototype.eat = function () {
console.log('吃东西')

Dog.prototype.drink = function () {
console.log('喝东西')
}

这里我写了一个名为Dog的函数,并在原型上定义了两个方法,分别为eatdrink方法。那么输出Dog的prototype对象是怎么样的呢?
Dog的prototype图

可以看到,Dog的prototype的属性里有我刚才定义的两个方法而且还有一个construtor属性以及__proto__。此时看construtor明显的是指向
Dog函数本身,展开__proto__属性时,可以看到这里有更多的属性,同时可以看到construtor指向的是Object,很明显的可以出,函数Dog的构造函数是Object。
这里不得不提一嘴,JS本身是提供了很多的原生构造函数,它们分别是下面几种:

  • Object
  • Number
  • String
  • Boolean
  • RegExp
  • Error
  • Function
  • Array
  • Date

那么说这个有什么用呢?第一个当然是为了说明为什么上面的Dog函数的__proto__的construtor会指向Object,第二个就是看下面例子:

1
2
3
4
5
6
7
const str = 'www.baidu.com'

console.log(str.__proto__)

const num = 123

console.log(num.__proto__)

将上面代码复制输出后,你会看到StringNumber构造函数,之所以会看到,其实是JS的一种隐式转换,访问一些特殊属性的时候,JS内部会转化,上述代码实则可以到成下面的转化:

1
2
3
str.__proto__ => new String(str).__proto__  

num.__proto__ => new Number(num).__proto__

那么再回到Dog函数,接下来。加入以下代码:

1
2
3
4
let dog = new Dog('哈巴', 3)

console.log(dog.__proto__ === Dog.prototype) // true

经过上述的几个例子,其实都可以基本的说明的原型的三个特点。

什么是原型链

所谓的原型链就是实例化对象的__proto__指向构造函数的prototype,然后构造函数的__proto__指向他的构造函数。以上面的例子就是下面的关系图:
Dog关系图

原型链就是这种指向的关系,这种关系的指向一般最高的是构造函数Object,而构造函数Object的__proto__是null。

值得注意的一点是Function的prototype是一个函数,但是Function的prototype的prototype是没有的,即是undefined

原型的作用(继承)

说了那么多,那么原型到底有什么用呢?让我们再回到Dog函数,我实例化了一个dog对象,那么执行下面的代码:

1
2
3
dog.eat() // 吃东西

dog.drink() // 喝东西

很明显,我并没有dog对象中定义到这两个函数,那么它是怎么找到这个方法而且去执行的呢?其实就是原型链的作用,JS在执行这代码的时候,会在dog对象中找,找不到的时候就会跟随着dog的__proto__去找,直到找不到为止,就会报错。很明显我们是可以直到dog的__proto__是等于构造函数Dog的prototype的,所以之前在Dog函数定义的两个方法,只要是用构造函数Dog实例化的对象都是可以使用的。

试着输出下面的代码:

1
2
3
4
5
6
console.log(dog.__proto__)

console.log(Dog.prototype)

console.log(dog.__proto === Dog.prototype)

很明显实例化对象dog继承了构造函数Dog的prototype上的方法,那么这个时候可以去探究另外一个问题了,函数怎么继承另外一个函数的方法呢?

借助call或apply函数

先实现下面的代码:

1
2
3
4
function Father() {
this.arr = [1,2,3]
this.name = 'ydw
}

接下来我们要实现Son函数继承Father函数,如下:

1
2
3
4
function Son() {
Father.call(this)
this.age = 20
}

这样写的话,可以继承在Father函数上定义的东西,但是如果没有在Father函数上定义的东西就不能继承,就比如Father原型链上的其他属性或者方法都不能继承。

原型链上的继承

1
2
3
4
5
function Son() {
this.age = 20
}

Son.prototype = new Father()

使用上其实没有什么问题,但是由于是将Son函数的原型赋值到实例化Father的对象,所以会出现到一个问题:

1
2
3
4
5
6
7
8
9
const  son1 = new Son()
const son2 = new Son()


son1.arr.push(4)

console.log(son1.arr) // [1,2,3,4]

console.log(son2.arr) // [1,2,3,4]

因为是共用一个对象,所以Son函数实例化的对象实际上都是共用了一个Father函数实例化的对象。

解决办法如下:

1
2
3
4
5
6
7
function Son() {
this.age = 20
Father.call(this)
}

Son.prototype = Father.prototype

这种办法就是组合继承办法,当然这不是最好的,因为是直接赋值的Father的prototype,所以Son函数的construtor会指向的是Father函数。所以需要优化一下.

1
2
3
4
5
6
7
8

function Son() {
this.age = 20
Father.call(this)
}

Son.prototype = Object.create(Father.prototype)
Son.prototype = Son

至于Object.create的用法参考MDN的用法介绍。

总结

基本上原型、原型链以及原型继承基本就是这些东西,说难的其实不难,更多是理解。