查看原文
其他

再看 JavaScript 继承

  • 作者:Hyuain

  • https://juejin.im/post/5e43bb2df265da571670fd8b

学完了整个 JavaScript 基础篇章之后,发现自己对继承的理解有了好几次了翻天覆地的变化(扶额),于是就写了这样一篇文章阐述目前自己对继承的看法。其实是初到掘金,复制了一篇之前自己写的文章(笑)。

本文将着重讨论基于原型的继承,也会简单写一下如何用 class 继承。

Key Points

  • 在 JavaScript 中,函数 Function 也是一种 对象 Object

  • 关于函数

    • 所有函数都自带 prototype

    • prototype 中自带 constructor

    • constructor 里面的东西就是函数的内容

    • 构造函数首字母大写(约定俗成)

  • 对象.__proto__===其构造函数.prototype

〇、简单解释

首先,关于函数也是一种对象这个说法,我们在后面(也许是别的文章中)会有相关的说明,这里先记住这个结论即可;

其次是关于函数的几个描述,我们可以做几个实验来验证一下:

  1. let arr = [1, 2, 3]

  2. let obj = { name: 'Harvey', age: '22'}

  3. let fn = function(){ console.log('hi') }

  4. 复制代码

打印出 arr objfn 之后就可以看到,比起别的对象,函数确实是比较特别的,他天生就带有一个 prototype 属性,而且 prototype 中的 constructor 就是这个函数本身。

  1. arr.prototype === undefined // true

  2. obj.prototype === undefined // true

  3. fn.prototype.constructor === fn // true

  4. 复制代码

我们也可以再验证一下最后一句话:

  1. function Person(){}

  2. let me = new Person

  3. me.__proto__ === Person.prototype // true

  4. 复制代码

做了这几个小实验之后,我们进入正题来讨论。

一、原型链

原型链的精髓其实就是刚才已经提到过的一句话:

对象. __proto__ === 其构造函数. prototype

当然这样说比较抽象,我们可以展开对普通对象、数组(代表了比较特殊的对象,比如日期等),以及函数来分别进行讨论。

普通对象的原型链

普通对象的原型是 Object

这句话要从以下几点来理解:

  1. 创建一个对象可以按这种方式写: letobj=newObject({name:'Harvey',age:'22'})

  2. Object 实际上是一个构造函数,他构造了 obj

  3. obj.__proto__===Object.prototype

因此我们可以简单表示一下这个普通对象的原型链:

obj->Object.prototype

数组的原型链

数组的原型是 Array

这句话要从以下几点来理解:

  1. 创建一个数组可以按这种方式写: letarr=newArray(1,2,3)

  2. Array 实际上是一个构造函数,他构造了 arr

  3. arr.__proto__===Array.prototype

事实上,我们还会发现:

  1. arr.__proto__.__proto__ === Object.prototype // true

  2. Array.prototype.__proto__ === Object.prototype // true

  3. 复制代码

也就是说,一个数组的原型链要稍微复杂一些:

arr->Array.prototype->Object.prototype

函数的原型

函数的原型是 Function

这句话要从以下几点来理解:

  1. 创建一个数组可以按这种方式写: letfn=newFunction((),{console.log('hi')})

  2. Function 实际上是一个构造函数,他构造了 fn

  3. fn.__proto__===Function.prototype

也就是说,一个函数的原型链也要稍微复杂一些:

fn->Function.prototype->Object.prototype

修改原型链

通过直接修改 __proto__ 就可以达到修改原型链的目的

  1. let obj1 = { a: 1 }

  2. let obj2 = { b: 2 }

  3. let obj3 = { c: 3 }

  4. obj2.__proto__ = obj1

  5. obj3.__proto__ = obj2

  6. 复制代码

这样,他们的原型链就变成了:obj3->obj2->obj1->Object.prototype

但是这种方法是不推荐的,我们更推荐使用 Object.create() 方法,他的使用方法如下:

  1. let obj1 = { a: 1 }

  2. let obj2 = Object.create(obj1)

  3. obj2.b = 2

  4. let obj3 = Object.create(obj2)

  5. obj3.c = 3

  6. 复制代码

这样与上面直接修改 __proto__ 效果基本是一样的

二、继承

这里我们要明确一点,平时大家所说的继承(或者说类的继承),其实更多的是一种 狭义的继承。他指的 不是 我们按照上面的方式 单纯对原型链进行的修改而是 一种在 构造函数之间 的,在 prototype 之间的继承。

比如我们说 Array 继承了 Object

  1. // Array 继承了 Object:

  2. Array.prototype.__proto === Object.prototype

  3. 复制代码

而不说 arr 继承了 Array,哪怕出现了原型链:

  1. // 我们不说 arr 继承了 Array

  2. arr.__proto__ === Array.prototype

  3. 复制代码

也不说 obj2 继承了 obj1,哪怕出现了 __proto__

  1. // 我们也不说 obj2 继承了 obj1

  2. obj2.__proto === obj1

  3. 复制代码

那么问题来了,这种在 构造函数之间的继承 应该怎么写呢,怎样才能得到像 ArrayObject 的这种关系呢?

第一步:使用 call 来调用父类构造函数

  1. // 定义父类

  2. function Person(姓名) {

  3. this.姓名 = 姓名

  4. }

  5. Person.prototype.自我介绍 = function() {

  6. console.log(`你好,我是 ${this.姓名}`)

  7. }

  8. 复制代码


  9. // 尝试定义一个子类,来继承 Person

  10. function Student(姓名, 学号){

  11. Person.call(this, 姓名) // 调用父类构造函数

  12. this.学号 = 学号

  13. }

  14. 复制代码

这一步是为了让 new子类 创建出来的对象拥有与 new父类 一样的属性。

在这个例子中,就是为了让 newStudtent 创建出来的对象,拥有 Person 中的 姓名 属性。

好,我们现在来尝试创建一个 Student 对象 小明

  1. let 小明 = new Student('小明', 123456)

  2. 复制代码


  3. 小明.姓名 // '小明'

  4. 小明.学号 // 123456

  5. 小明.自我介绍() // Uncaught TypeError: 小明.自我介绍 is not a function

  6. 复制代码

小明 如何拿到 学号 ?

newStudent('小明',123456) 的时候,系统会去调用 Student 函数,并且把 小明 这个对象作为 this 传进去;

相当于在 Student 函数中执行了 小明.学号=123456

小明 如何拿到 姓名 ?

同样,系统调用 Student 函数,看到了 Person.call(this,姓名)(相当于 Person.call(小明,'小明')),意思是让 Person 中的 this小明,并且传一个参数 '小明'Person

然后将会调用 Person 函数,在 Person 中执行 this.姓名=姓名(相当于 小明.姓名='小明')。

为什么 小明 不能使用 自我介绍 ?

因为可以看到, 小明 这个对象中没有 自我介绍 属性,他的 __proto__(也就是 Student.prototype) 中也没有,因此他找不到 自我介绍

第二步:建立原型链

那么我们怎样才能让 小明 能够进行 自我介绍呢?想到了几种写法,我们来一一分析一下:

1、直接将 自我介绍 放在 小明 这个对象实例上

  1. 小明.自我介绍 = function(...){...}

  2. 复制代码

但是既然每个人都需要 自我介绍 ,那么我们单独修改 小明 这样一个对象就没有意义。

2、将 自我介绍 放在 小明 这一类对象实例上

  1. Student = function() {

  2. this.自我介绍 = Person.prototype.自我介绍

  3. }

  4. 复制代码

这样最终其实是可以实现相同的效果的。不过这样就需要每个函数都单独写一下,有时候也不太方便。

3、将 自我介绍 放在 小明 这一类对象实例的 __proto__ 上

众所周知, 小明 也可以访问到 小明.__proto__ 上的函数,所以这好像也是可行的。

既然 小明.__proto__===Student.prototype,那要不我们这样写:

  1. Student.prototype = Person.prototype

  2. 复制代码

这样 小明就可以使用在 Person.prototype 上的 自我介绍 了。

但是!如果这个时候 Student 想要给自己的 prototype 加一个新方法,怎么办?我们知道因为只是复制了地址,如果修改了 Student.prototypePerson.prototype 也将被修改,这显然是我们不愿意看到的。难道要用深拷贝?orz

4、将 自我介绍 放在 小明 这一类对象实例的 __proto__ 的 __proto__ 上

众所周知, 小明 也可以访问到 小明.__proto__.__proto__ 上的函数,所以这也是可行的。

既然 小明.__proto__===Student.prototype,那么也就是说我们要实现:

  1. Student.prototype.__proto__ = Person.prototype

  2. 复制代码

这就是我们的终极解决方案啦,是不是看起来有点眼熟?

  1. Array.prototype.__proto__ === Object.prototype // true

  2. 复制代码

对了!这就是原型链!这就是我们所说的继承!是不是有点感觉了?

当然,刚才也说了,我们最好用下面这种写法:

  1. Student.prototype = Object.create(Person.prototype)

  2. 复制代码

回顾一下到现在我们做了什么?

  1. 首先我们通过 call父级构造函数,来实现属性的继承,我们的 小明 有了 姓名

  2. 然后我们通过建立原型链,来实现方法的继承,我们的 小明 可以 自我介绍 了

看似已经结束,但是实际上还有一个隐藏的 Bug,我们接下来来解决这个 Bug。

第三步:解决 constructor 的问题

细心的你会发现(我们在最开始也说过了),我们的对象实例和构造函数中是有一个 constructor 属性的,比如:

  1. const arr = [1, 2]

  2. arr.__proto__.constructor === Array // true

  3. Array.prototype.constructor === Array // true

  4. 复制代码

但是, Student.prototype 中的 constructor 被刚才的那一番操作给搞没了,我们需要把它弄回来:

  1. Student.prototype.constructor = Student

  2. 复制代码

这样就完成了一波类的继承。

三、总结

看一波完整的代码:

  1. // 定义父类

  2. function Person(姓名) {

  3. this.姓名 = 姓名

  4. }


  5. // 定义父类的方法

  6. Person.prototype.自我介绍 = function() {

  7. console.log(`你好,我是 ${this.姓名}`)

  8. }


  9. // 定义子类

  10. function Student(姓名, 学号){

  11. Person.call(this, 姓名) // 调用父级构造函数,继承父类的属性

  12. this.学号 = 学号

  13. }


  14. // 建立原型链,继承父类的方法

  15. Student.prototype = Object.create(Person.prototype)


  16. // 解决 constructor 问题

  17. Student.prototype.constructor = Student


  18. // 定义子类的新方法

  19. Student.prototype.报数 = function() {

  20. console.log(`我的学号是 ${this.学号}`)

  21. }




  22. let 小红 = new Student('小红', 345678)

  23. 小红.自我介绍() // 你好,我是 小红

  24. 小红.报数() // 我的学号是 345678

  25. 复制代码

四、用 class 继承

既然他本身就是语法糖,我个人认为没必要搞那么细,其实本质跟上面的使用原型链的继承是一样的,搞清楚是怎么写的就好啦:

  1. // 定义父类

  2. class Person {

  3. constructor(姓名) { // 定义属性

  4. this.姓名 = 姓名

  5. }

  6. 自我介绍() { // 定义方法

  7. console.log(`你好,我是 ${this.姓名}`)

  8. }

  9. }


  10. // 定义子类

  11. class Student extends Person {

  12. constructor(姓名, 学号) {

  13. super(姓名) // 这里的 姓名 两个字要与父类中的一样,继承属性和方法

  14. this.学号 = 学号 // 定义新属性

  15. }

  16. 报数() { // 定义新方法

  17. console.log(`我的学号是 ${this.学号}`)

  18. }

  19. }




  20. let 小红 = new Student('小红', 345678)

  21. 小红.自我介绍() // 你好,我是 小红

  22. 小红.报数() // 我的学号是 345678

  23. 复制代码

看完两件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我两件小事
  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注公众号「前端开发博客」,持续为你推送精选好文

加入微信群👆,每日分享全网好文章!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存