title: 对象原型链
categories: Javascript
tag:

  • 原型链
    date: 2021-11-22 10:16:34

类和对象

当我们编写如下代码的时候,我们会如何来称呼这个 Person 呢?

  • 在 JS 中 Person 应该被称之为是一个构造函数;
  • 从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象 p1、p2;
  • 如果从面向对象的编程范式角度来看,Person 确实是可以称之为类的;

面向对象的特性

面向对象有三大特性:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态;

那么这里我们核心讲继承。

那么继承是做什么呢?

  • 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

那么 JavaScript 当中如何实现继承呢?

  • 不着急,我们先来看一下 JavaScript 原型链的机制;
  • 再利用原型链的机制实现一下继承

原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。

  • 我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

image-20211128201637440

Object 原型

那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型proto属性呢?

我们会发现它打印的是 [Object: null prototype] {}

  • 事实上这个原型就是我们最顶层的原型了
  • 从 Object 直接创建出来的对象的原型都是 [Object: null prototype] {}。

那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是 null,也就是已经是顶层原型了;
  • 特殊二:该对象上有很多默认的属性和方法;
  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. console.log(obj.__proto__) //Object
  6. console.log(obj.__proto__.__proto__) //null

11_JS原型链和继承 - 图2

那么创建 Object 对象的内存图

11_JS原型链和继承 - 图3

那么如果我们让 obj 的原型指向新的对象呢?

  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. obj.__proto__ = {
  6. address: '北京市'
  7. }
  8. console.log(obj.__proto__) //{address: '北京市'}
  9. console.log(obj.__proto__.__proto__) //Object
  10. console.log(obj.__proto__.__proto__.__proto__) //null

此时在内存中就是以下形式

11_JS原型链和继承 - 图4

Object 是所有类的父类

原型链最顶层的原型对象就是 Object 的原型对象

11_JS原型链和继承 - 图5

继承

通过原型链实现继承

为什么需要继承?

查看以下代码,重复性较高。老师和学生都需要吃饭,跑步,不同的是学生需要学习,老师需要教书。我们可以使用继承。抽取公共类,创建学生类和教师类即可。

  1. function Student(name, age, sno) {
  2. this.name = name
  3. this.age = age
  4. this.sno = sno
  5. }
  6. Student.prototype.running = function () {
  7. console.log(this.name + 'running')
  8. }
  9. Student.prototype.eating = function () {
  10. console.log(this.name + 'eating')
  11. }
  12. Student.prototype.studying = function () {
  13. console.log(this.name + 'studying')
  14. }
  15. function Teacher(name, age, title) {
  16. this.name = name
  17. this.age = age
  18. this.title = title
  19. }
  20. Teacher.prototype.running = function () {
  21. console.log(this.name + 'running')
  22. }
  23. Teacher.prototype.eating = function () {
  24. console.log(this.name + 'eating')
  25. }
  26. Teacher.prototype.teaching = function () {
  27. console.log(this.name + 'teaching')
  28. }

那么我们现在使用原型链进行继承

  1. //父类
  2. function Person() {
  3. this.name = 'why'
  4. }
  5. Person.prototype.eating = function () {
  6. console.log(this.name + ' eating')
  7. }
  8. //子类
  9. function Student() {
  10. this.sno = 11
  11. }
  12. //使用原型链继承
  13. Student.prototype = new Person()
  14. Student.prototype.studying = function () {
  15. console.log(this.name + ' studying~')
  16. }
  17. var stu = new Student()
  18. console.log(stu.name)
  19. stu.eating()

11_JS原型链和继承 - 图6

原型链继承的弊端

但是目前有一个很大的弊端:某些属性其实是保存在 p 对象上的;

  1. 第一,我们通过直接打印对象是看不到这个属性的;
  1. console.log(stu) //Student {sno: 11}
  1. 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
    本来只有小明才有这个朋友,但是小红也会有这个朋友。他们之间会相互影响
  2. 第三,不能给 Person 传递参数,因为这个对象是一次性创建的(没办法定制化);

盗用构造函数继承

  1. //父类
  2. //父类
  3. function Person(name, age, friends) {
  4. //这里没有传,这个this是stu1或者stu2。不是person创出来的对象
  5. this.name = name
  6. this.age = age
  7. this.friends = friends
  8. }
  9. Person.prototype.eating = function () {
  10. console.log(this.name + ' eating')
  11. }
  12. //子类
  13. function Student(name, age, friends, sno) {
  14. Person.call(this, name, age, friends)
  15. this.sno = sno
  16. }
  17. //使用原型链继承
  18. // Student.prototype = new Person()
  19. Student.prototype.studying = function () {
  20. console.log(this.name + ' studying~')
  21. }
  22. ////第三个弊端解决,可以传参数了
  23. var stu1 = new Student('why', 22, ['gwk'], 111)
  24. var stu2 = new Student('dh', 18, ['gwk'], 112)
  25. //第一个弊端解决
  26. console.log(stu1) //Student {name: 'why', age: 22, friends: Array(1), sno: 111}
  27. //第二个弊端解决
  28. stu1.friends.push('lucy')
  29. console.log(stu1.friends) //(2) ['gwk', 'lucy']
  30. console.log(stu2.friends) //['gwk']
  31. stu1.eating() //TypeError: stu1.eating is not a function

缺点:

使用 call 和 apply 借用其他构造函数的成员, 可以解决给父构造函数传递参数的问题, 但是获取不到父构造函数原型上的成员.也不存在共享问题,也就是不能获取父构造函数方法。

组合原型链和构造函数继承

借用构造函数+原型链继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函

数或者称之为经典继承或者称之为伪造对象):

  • steal 是偷窃、剽窃的意思,但是这里可以翻译成借用;

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数.

  • 因为函数可以在任意的时刻被调用;
  • 因此通过 apply()和 call()方法也可以在新创建的对象上执行构造函数;
  1. //父类
  2. function Person(name, age, friends) {
  3. this.name = name
  4. this.age = age
  5. this.friends = friends
  6. }
  7. Person.prototype.eating = function () {
  8. console.log(this.name + ' eating')
  9. }
  10. //子类
  11. function Student(name, age, friends, sno) {
  12. Person.call(this, name, age, friends)
  13. this.sno = sno
  14. }
  15. //使用原型链继承
  16. Student.prototype = new Person()
  17. Student.prototype.studying = function () {
  18. console.log(this.name + ' studying~')
  19. }
  20. ////第三个弊端解决,可以传参数了
  21. var stu1 = new Student('why', 22, ['gwk'], 111)
  22. var stu2 = new Student('dh', 18, ['gwk'], 112)
  23. //第一个弊端解决
  24. console.log(stu1) //Student {name: 'why', age: 22, friends: Array(1), sno: 111}
  25. //第二个弊端解决
  26. stu1.friends.push('lucy')
  27. console.log(stu1.friends) //(2) ['gwk', 'lucy']
  28. console.log(stu2.friends) //['gwk']

11_JS原型链和继承 - 图7

内存中展示如下

11_JS原型链和继承 - 图8

这个组合借用构造函数继承也有弊端

组合继承存在什么问题呢?

组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。

  1. 一次在创建子类原型的时候;
  2. 另一次在子类构造函数内部(也就是每次创建子类实例的时候);

另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性

  1. 一份在当前的实例自己里面(也就是 stu1 本身的),另一份在子类对应的原型对象中(也就是stu1.__proto__里面);
  2. 当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;

上述介绍了原型链继承,借用构造函数继承。

原型链继承

  1. 不能传递参数。
  2. 访问创建的对象时属性不会展示完全。
  3. 引用属性会被共享。原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改

借用构造函数继承

  1. 获取不到方法

借用构造函数+原型链继承

虽然解决了原型链继承的问题,但是也暴露出了新的问题

  1. 会调用两次父类构造函数,性能不好
  2. 在实例的原型上也会多出属性。每个新实例都有父类构造函数的副本,臃肿。

原型式继承函数

这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON 的创立者)在 2006 年写的一篇文章说起: Prototypal Inheritance in JavaScript(在 JS 中使用原型式继承)

在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.

  • 为了理解这种方式,我们先再次回顾一下 JavaScript 想实现继承的目的:重复利用另外一个对象的属性和方法.

最终的目的:student 对象的原型指向了 person 对象;

在构造函数组合原型链中,我们是使用 new Person()新建一个对象,这个这个方法不好,这样子一共会调用两次构造函数。因为我们可以直自己创建,并且把这个对象的原型指向 Person

第一种方案

  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. //原型式继承第一种方案
  6. function createObject(o) {
  7. //新建一个构造函数
  8. function Fn() {}
  9. // 原型指向传入的父类对象
  10. Fn.prototype = o
  11. //新建一个实例
  12. var newObj = new Fn()
  13. return newObj
  14. }
  15. // info的原型指向了obj
  16. var info = createObject(obj)
  17. console.log(info) //{}
  18. console.log(info.__proto__) //{name: 'why', age: 18}
  19. console.log(info.__proto__.__proto__) //Object

第二种方案,使用ObjectsetPrototypeOf()

  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. //原型式继承第二种方案,使用setPrototypeOf
  6. function createObject(o) {
  7. var newObj = {}
  8. Object.setPrototypeOf(newObj, o)
  9. return newObj
  10. }
  11. // info的原型指向了obj
  12. var info = createObject(obj)
  13. console.log(info) //{}
  14. console.log(info.__proto__) //{name: 'why', age: 18}
  15. console.log(info.__proto__.__proto__) //Object

第三种方案,可以直接Object.create()

  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. //原型式继承
  6. var info = Object.create(obj)

11_JS原型链和继承 - 图9

原型式继承

  1. //原型式继承
  2. Student.prototype = Object.create(Person.prototype)

寄生式继承

  1. var personObj = {
  2. age: 18,
  3. running: function () {
  4. console.log('running')
  5. }
  6. }
  7. // 寄生式继承
  8. function createStudent(name) {
  9. var stu = Object.create(personObj)
  10. stu.name = name
  11. stu.studying = function () {
  12. console.log('studying~')
  13. }
  14. return stu
  15. }
  16. var stuObj1 = createStudent('why')
  17. var stuObj2 = createStudent('dhh')

寄生组合式继承

现在我们来回顾一下之前提出的比较理想的组合继承

  • 组合继承是比较理想的继承方式, 但是存在两个问题:
  • 问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
  • 问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.

事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉.

  • 你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中属性和方法复制一份到了子类型中. 所以父类型本身里面的内容, 我们不再需要.
  • 这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法.
    能不能直接让子类型的原型对象 = 父类型的原型对象呢?
  • 不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改.
  • 我们使用前面的寄生式思想就可以了.
  1. //父类
  2. function Person(name, age, friends) {
  3. //这里没有传,这个this是stu1或者stu2。不是person创出来的对象
  4. this.name = name
  5. this.age = age
  6. this.friends = friends
  7. }
  8. Person.prototype.eating = function () {
  9. console.log(this.name + ' eating')
  10. }
  11. //子类
  12. function Student(name, age, friends, sno) {
  13. Person.call(this, name, age, friends)
  14. this.sno = sno
  15. }
  16. //寄生组合式继承
  17. Student.prototype = Object.create(Person.prototype)
  18. Student.prototype.studying = function () {
  19. console.log(this.name + ' studying~')
  20. }
  21. var stu1 = new Student('why', 22, ['gwk'], 111)
  22. var stu2 = new Student('dh', 18, ['gwk'], 112)
  23. console.log(stu1)
  24. console.log(stu1.__proto__)
  25. stu1.studying()
  26. stu1.eating()

但是由于他没有 constructor。就会向上找,找到 Person.但是这个应该是 Student 才对

11_JS原型链和继承 - 图10

因此最终代码为

  1. //父类
  2. function Person(name, age, friends) {
  3. //这里没有传,这个this是stu1或者stu2。不是person创出来的对象
  4. this.name = name
  5. this.age = age
  6. this.friends = friends
  7. }
  8. Person.prototype.eating = function () {
  9. console.log(this.name + ' eating')
  10. }
  11. //子类
  12. function Student(name, age, friends, sno) {
  13. Person.call(this, name, age, friends)
  14. this.sno = sno
  15. }
  16. //寄生组合式继承
  17. Student.prototype = Object.create(Person.prototype)
  18. Object.defineProperty(Student.prototype, 'constructor', {
  19. enumerable: false,
  20. configurable: true,
  21. writable: true,
  22. value: Student
  23. })
  24. Student.prototype.studying = function () {
  25. console.log(this.name + ' studying~')
  26. }
  27. var stu1 = new Student('why', 22, ['gwk'], 111)
  28. var stu2 = new Student('dh', 18, ['gwk'], 112)
  29. console.log(stu1)
  30. // console.log(stu1.__proto__)
  31. stu1.studying()
  32. stu1.eating()

11_JS原型链和继承 - 图11

为了通用性,直接封装一个函数

  1. function inheritPrototype(subType, superType) {
  2. subType.prototype = Object.create(superType.prototype)
  3. Object.defineProperty(subType.prototype, 'constructor', {
  4. enumerable: false,
  5. configurable: true,
  6. writable: true,
  7. value: subType
  8. })
  9. }

对象的其他方法

hasOwnProperty

对象是否有某一个属于自己的属性(不是在原型上的属性)

当我们创建一个对象的时候,我们可以发现有些属性是在 obj 上面的,有些属性是在自己上面的

  1. var obj = {
  2. name: 'why',
  3. age: 18
  4. }
  5. var info = Object.create(obj, {
  6. address: {
  7. value: '北京市',
  8. enumerable: true
  9. }
  10. })
  11. console.log(info) //{address: '北京市'}
  12. console.log(info.__proto__) //{name: 'why', age: 18}

那么就有一个方法。hasOwnProperty()可以验证

  1. console.log(info.hasOwnProperty('address')) //true
  2. // name是info原型上的,不是他自己的
  3. console.log(info.hasOwnProperty('name')) //false

in/for in 操作符

判断某个属性是否在某个对象或者对象的原型上

  1. console.log('address' in info) //true
  2. console.log('name' in info) //true

instanceof

用于检测构造函数的 pototype,是否出现在某个实例对象的原型链上

  1. function Person() {}
  2. function Student() {}
  3. function inheritPrototype(subType, superType) {
  4. subType.prototype = Object.create(superType.prototype)
  5. Object.defineProperty(subType.prototype, 'constructor', {
  6. enumerable: false,
  7. configurable: true,
  8. writable: true,
  9. value: subType
  10. })
  11. }
  12. inheritPrototype(Student, Person)
  13. var stu = new Student()
  14. //对象 instanceof 函数
  15. //这是Student构造函数的实例
  16. console.log(stu instanceof Student) //true
  17. //Student继承于Person
  18. console.log(stu instanceof Person) //true
  19. //最顶层的原型
  20. console.log(stu instanceof Object) //true

isPrototypeOf

用于检测某个对象,是否出现在某个实例对象的原型链上

  1. //对象.isPrototypeOf 对象
  2. console.log(obj.isPrototypeOf(info)) //true
  3. console.log(Object.prototype.isPrototypeOf(obj)) //true

instanceof的区别

11_JS原型链和继承 - 图12

11_JS原型链和继承 - 图13

原型继承关系

对象-函数-原型之间的关系

首先需要知道的一点是

当我们写了一个function Foo() {}

等同于var Foo = new Function()

而 Foo 会有一个显示原型对象,test.prototype

Foo 是一个函数,那么他会有一个显示原型对象:Foo.prototype

  • 来自于:创建了一个函数,Foo.prototype={constructor:Foo}

Foo 是一个对象,那么他会有一个隐式原型对象:Foo.__proto__

  • 来自于:new Function() , Foo.__proto__=Function.prototype
  • Function.prototype={constructor:Function}
  1. function Foo() {}
  2. console.log(Foo.prototype == Foo.__proto__) //false
  3. console.log(Foo.prototype.constructor) //Foo
  4. console.log(Foo.__proto__.constructor) //Function

我们可以画内存图为

11_JS原型链和继承 - 图14

  1. console.log(Function.prototype === Function.__proto__) //true

从以上代码可知,Function 是很特殊的。他的显示原型对象等于隐式原型对象