再看 JavaScript 继承

学完了整个 JavaScript 基础篇章之后,今天又看了一下所谓的继承,发现我之前理解的继承跟大家一直提到的那个继承不是一回事,于是写一篇文章记录一下。
本文将着重讨论基于原型的继承,也会简单写一下如何用 class 继承。

Key Points

在讨论继承之前,我们需要知道这样几个东西,他们将会帮助我们理解继承:

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

  • 关于函数

    1. 所有函数都自带 prototype
    2. prototype 中自带 constructor
    3. constructor 里面的东西就是函数的内容
    4. 构造函数首字母大写(约定俗成)
  • 对象.__proto__ === 其构造函数.prototype

理解一下上面几句话。

首先,关于函数也是一种对象这个说法,我们在后面会有相关的说明;

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

let arr = [1, 2, 3]
let obj = { name: 'Harvey', age: '22'}
let fn = function(){ console.log('hi') }

console.dir(arr)
/*
{
  0: 1,
  1: 2,
  2: 3,
  length: 3,
  __proto__: Array(0)
}
*/

console.dir(obj)
/*
{
  name: "Harvey",
  age: "22",
  __proto__: Object
}
*/

console.dir(fn)
/*
{
  length: 0,
  name: "fn",
  arguments: null,
  caller: null,
  prototype: {
    constructor: ƒ ()
    __proto__: Object
  }
  __proto__: ƒ ()
}
*/

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

可以很清楚的看到,比起别的对象,函数确实是比较特别的,他天生就带有一个 prototype 属性,而且 prototype 中的 constructor 就是这个函数本身。

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

function Person(){}
let me = new Person
me.__proto__ === Person.prototype // true

知道了这样几个前置的知识,我们就可以往下讨论了。

原型链

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

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

普通对象的原型链

普通对象的原型是 Object

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

  1. 创建一个对象可以按这种方式写: let obj = new Object({ name: 'Harvey', age: '22' })
  2. Object 实际上是一个构造函数,他构造了 obj
  3. obj.__proto__ === Object.prototype

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

obj -> Object.prototype

数组的原型链

数组的原型是 Array

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

  1. 创建一个数组可以按这种方式写: let arr = new Array(1, 2, 3)
  2. Array 实际上是一个构造函数,他构造了 arr
  3. arr.__proto__ === Array.prototype

事实上,我们还会发现:

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

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

arr -> Array.prototype -> Object.prototype

等等,这意味着 Object.prototype 构造了 Array.prototype?这个问题我们稍后再谈。

函数的原型

函数的原型是 Function

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

  1. 创建一个数组可以按这种方式写: let fn = new Function( (), { console.log('hi') } )
  2. Function 实际上是一个构造函数,他构造了 fn
  3. fn.__proto__ === Function.prototype

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

fn -> Function.prototype -> Object.prototype

修改原型链

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

let obj1 = { a: 1 }
let obj2 = { b: 2 }
let obj3 = { c: 3 }
obj2.__proto__ = obj1
obj3.__proto__ = obj2

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

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

let obj1 = { a: 1 }
let obj2 = Object.create(obj1)
obj2.b = 2
let obj3 = Object.create(obj2)
obj3.c = 3

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

继承

这里我们就发现,如果我们按照上面的方法修改原型链,得到的原型链 obj3 -> obj2 -> obj1 -> Object.prototype 有点问题!

他不满足 对象.__proto__ === 其构造函数.prototype

这是当然的,因为 obj1 本来就不是 obj2 的构造函数。话说回来,obj1 根本就不是函数,他连 prototype 都没有。

所以说这里我们要明确一点,平时大家所说的继承(或者说类的继承),其实更多的是一种 狭义的继承

他指的 不是 我们按照上面的方式 单纯对原型链进行的修改而是 一种在 构造函数之间 的,有 prototype 存在的继承。

PS. 尽管在某些时候,我们使用 Object.create() 将原型链进行简单修改也被称为继承,但不是我们这里讨论的继承

那么问题来了,这种在 构造函数之间的继承 应该怎么写呢?

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

function Person(姓名) {
  this.姓名 = 姓名
}
Person.prototype.自我介绍 = function() {
  console.log(`你好,我是 ${this.姓名}`)
}

function Student(姓名, 学号){
  Person.call(this, 姓名)
  this.学号 = 学号
}

可以通过 console.dir() 来分别看看 PersonStudent

// Person
{
  name: "Person",
  prototype: {
    自我介绍: ƒ (),
    constructor: ƒ Person(姓名),
    __proto__: Object,
  },
  __proto__: ƒ () // Function.prototype
}

// Student
{
  name: "Student",
  prototype: {
    constructor: ƒ Student(姓名, 学号),
    __proto__: Object,
  },
  __proto__: ƒ () // Function.prototype
}

我们尝试创建一个 Student 对象 小明

let 小明 = new Student('小明', 123456)
/*
{
  姓名: "小明",
  学号: 123456,

  __proto__: {
    constructor: ƒ Student(姓名, 学号),
    __proto__: Object,
  }

}
*/

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

小明 如何拿到 学号

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

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

小明 如何拿到 姓名

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

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

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

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

第二步:建立原型链

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

我们再打印出 Student 来看看

// Student
{
  name: "Student",
  prototype: {
    __proto__:{
      自我介绍: ƒ (),
      constructor: ƒ Person(姓名),
      __proto__: Object,
    }
  },
  __proto__: ƒ () // Function
}

我们发现 prototype 里面的 constructor 不见了,并且里面的 __proto__ 变得跟 Person.prototype 一样了

这个时候再 new 一个 小花 看看:

let 小花 = new Student('小花', 234567)
/*
{
  姓名: "小花",
  学号: 234567,
  __proto__: {

    __proto__: {
      自我介绍: ƒ (),
      constructor: ƒ Person(姓名),
      __proto__: Object,
    }

  }
}
*/

小花.自我介绍()
// '你好,我是 小花'

小花现在会自我介绍了,但是还有个小问题,我们发现 小花.__proto__ 中的 constructor 不见了!我们得赶紧修复一下。

解决 constructor 的问题

Student.prototype.constructor = Student

看看现在 Student 的样子

// Student
{
  name: "Student",
  prototype: {
    constructor: ƒ Student(姓名, 学号),
    __proto__:{
      自我介绍: ƒ (),
      constructor: ƒ Person(姓名),
      __proto__: Object,
    }
  },
  __proto__: ƒ () // Function
}

最后请出我们的小红

let 小红 = new Student('小花', 345678)
/*
{
  姓名: "小红",
  学号: 345678,
  __proto__: {

    constructor: ƒ Student(姓名, 学号),

    __proto__: {
      自我介绍: ƒ (),
      constructor: ƒ Person(姓名),
      __proto__: Object,
    }

  }
}
*/

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

OK,一切正常。

总结

function Person(姓名) {
  this.姓名 = 姓名
}

Person.prototype.自我介绍 = function() {
  console.log(`你好,我是 ${this.姓名}`)
}

function Student(姓名, 学号){
  Person.call(this, 姓名) // 调用父级构造函数
  this.学号 = 学号
}

Student.prototype = Object.create(Person.prototype) // 建立原型链

Student.prototype.constructor = Student // 解决 constructor 问题
/*
{
  name: "Student",

  prototype: {

    constructor: ƒ Student(姓名, 学号),
    // Student.prototype 里面的 constructor 就是 Student

    __proto__:{
      // Student.prototype 里面的 __proto__ 是 Person.prototype
      自我介绍: ƒ (),
      constructor: ƒ Person(姓名),
      __proto__: Object,
    }

  },

  __proto__: ƒ () // Function
}
*/

Student.prototype.报数 = function() {
  console.log(`我的学号是 ${this.学号}`)
}

let 小红 = new Student('小红', 345678)
/*
{
  姓名: "小红"
  学号: 345678
  __proto__: {

    constructor: ƒ Student(姓名, 学号)
    // 小红 是由 Student 构造的
    // 小红.__proto__ 跟 Student.prototype 是一样的

    __proto__: {
      自我介绍: ƒ ()
      constructor: ƒ Person(姓名)
      __proto__: Object
    }

  }
}
*/

小红.自我介绍()
小红.报数()
// ‘你好,我是 小红’

用 class 继承

class Person {
    constructor(姓名) {
        this.姓名 = 姓名
    }
    自我介绍() {
        console.log(`你好,我是 ${this.姓名}`)
    }
}

class Student extends Person {
    constructor(姓名, 学号) {
        super(姓名) // 这里的 姓名 两个字要与父类中的一样
        this.学号 = 学号
    }
    报数() {
        console.log(`我的学号是 ${this.学号}`)
    }
}

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

小红.自我介绍()
小红.报数()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容