JS 中的继承 | 图解红皮书

关于构造函数、原型和实例的说明

在《JavaScript 高级程序设计》中讲到了构造函数、原型和实例的关系,以及原型链是如何形成的。

重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。

这段话出现的名词太多了,乍一看会绕不过来,其实就是讲了两个事情。

  1. 构造函数、原型和实例的关系 前置知识:在 JS 中有 7 种基本类型, 还有一种复杂数据类型叫 Objecttypeof 操作符可以用来确定变量的数据类型,除了 7 种基本类型和 Object 之外,还可以确定出一个特殊的复杂类型------Function。 一般来说 Object 实例都会有 __proto__ 这个属性,一般称为原型指针 ,而指向的内容就被称为原型。对于 Function 实例,则会有 prototype 这个属性,回指向它的原型对象。 【为了更好的区分,之后对于 Object 实例的 __proto__ 属性我们称为 "原型指针",构造函数的 prototype 属性称为构造函数的"原型对象"】 构造函数就是 Function 对象,它有 prototype 属性指向原型对象,而原型对象会有一个特别的 constructor 属性,指回这个构造函数。实例则拥有大部分 Object 实例都有的 __proto__ 指针指向构造函数的原型对象。 可以理解成下面这幅图,构造函数和原型对象是互相绑定的关系。而实例则会与原型对象建立一个联系,表示一种"类"的关系。

  2. 原型链怎么来的 上面提到的原型也是一个 Object 实例,所以实际上它也是有 __proto__ 属性的,也就是说,它也会有自己的原型指针,指向另一个原型对象。接下来就像书里说的,如果这个新出现的原型对象,又指向另一个原型对象,那么就构成了一条原型链。 到这里大致会对整个原型链有个初步的了解,那么原型链的这个终点,是什么意思呢?Object.prototype 和 最终的 null 是什么?

JS 的两个内置构造函数 Object 和 Function

Object

如果将构造函数 + 原型对象 这样的组合看作一个"类"的话,那 Object 构造函数 + Object.prototype 就可以被理解为"根类",也就是万事万物的起源,所有的类都直接或间接的继承自它。事实上确实如此,所有"类"以及所有实例的原型链,最终的归宿都是 Object.prototype 这个对象,而最终的最终 Object.prototype 的原型指针指向 null。 知道了这点后,我们可以在图上补上 Object 构造函数,它的原型对象是 Object.prototype

Function

前面提到,Function 是 JS 中一个特殊的复杂类型,它也是属于复杂类型,所以它也是"继承"自根类 Object 的,那么我们可以在图中加上 Function 类,并且画出它的原型关系。Function 类继承自 Object 类,因此它们之间的原型链关系是 Function.prototype 指向 Object.prototype (红色)。

Object 和 Function

将它们作为类理解的同时,也不要忘记 ObjectFunction 本质上也是一个对象(除了基本类型之外都是对象),也就是所有函数对象的 __proto__ 指针都会指向 Function.prototype ,所以有下面这样的关系。 图中有三个函数对象 ConstructorObjectFunction ,前两者的原型指针指向 Function.prototype 这没问题,可以理解为函数对象都是从 Function 构造来的。对于 Function 这个函数对象来说,它也有自己的原型指针,但是特别的它指向自己的原型对象(先有鸡还是先有蛋 🤣)。 所以最终的最终还是 FunctionObject 作为基础,一起撑起了 JS 中所有的对象,他俩可以形成一个闭环,最终结束在一个 NULL。

继承

原型链

基本思想

原型链继承的主要思想就是利用原型搜索机制,通过原型继承多个属性和方法。

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。《JavaScript 高级程序设计》

实现

具体实现就是通过修改子类构造函数的原型对象,来实现与父类的连结。

js 复制代码
function SuperType(){}
function SubType(){}
SubType.prototype = new SuperType()
  1. 定义父类

  2. 定义子类

  3. 修改子类构造函数的原型对象为一个父类的实例

    修改子类构造函数的原型对象后,使用子类构造函数创建实例,这个实例的原型指针会指向新的原型对象。这样,子类实例就可以通过原型链,继承父类实例上的属性和方法,进一步可以继承父类原型对象上的属性和方法。

问题

  1. 原型对象上的属性会在所有实例间共享
  2. 子类在实例化时不能给父类构造函数传参 本质上这两个原因就是因为子类的原型对象是一个固定的父类实例,而它会影响着所有的子类实例,属于是牵一发而动全身。

盗用构造函数

基本思想

为了解决原型对象的属性在实例间混用的情况,考虑为每个实例都创建自己的实例,而这些属性都是在父类构造函数中实现的,所以有了"盗用构造函数"这个方法。在子类构造函数中利用 call、apply 来修改函数调用的上下文,将属性都赋给子类实例。

实现

js 复制代码
function SuperType(){
	this.color = ['red','blue','yellow']
}
function SubType(){
	SuperType.call(this)
}

问题

  1. 没有原型链的关系,子类和父类从原型关系上看就像是两个单独的类。

  2. 没有原型链,无法继承原型对象上的方法,子类必须在自己的构造函数或者原型上再次定义方法。

组合继承

基本思想

结合以上两种方式的优势,就是利用原型链来继承方法,用盗用构造函数来继承属性。

实现

js 复制代码
function SuperType(){}
function SubType(){
	SuperType.call(this)
}
SubType = new SuperType()

原型式

这是一种特殊的继承方式,旨在离开类的概念,直接实现对象与对象之间的继承。可能更符合 JS 的设计,本身就没有类的概念,更多的是对象之间的关系。

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。《你不知道的 JavaScript (上卷)》

实现

给出了一个方法,接受一个对象,可以返回一个以传入对象为原型的新对象。

js 复制代码
function create(obj){
	function F(){}
	F.prototype = obj
	return new F()
}

ECMAScript 5 中增加了 Object.create() 方法将原型式继承进行了实现,接受两个参数,第一个参数作为原型,第二个参数可以定义额外的新属性。

问题

和原型链类似的问题,这样子生成的实例都会共享原型上的属性。

寄生式

基本思想

同样是基于对象的继承,在原型式基础上对对象进行了增强。

实现

js 复制代码
function createAnthor(obj){
	const clone = create(obj)
	clone.sayHi = function(){ // 以某种方式增强这个对象
		console.log('hi')
	}
	return clone
}

问题

很明显可以看出,这里的增强是很"定制化"的,本质上与构造函数创建对象类似。

寄生式组合

基本思路

组合继承的问题

组合式继承最大的问题是调用了两次父类的构造函数,一次是改变子类原型对象的时候,创建了一个父类实例,另一次是在子类构造函数中调用。这样就会让父类的实例属性变成子类的原型属性,并且所有子类实例都会拥有一份相同的属性。 例如:

js 复制代码
function SuperType(name){
	this.name = name
}
function SubType(name){
	SuperType.call(this,name)
}
SubType.prototype = new SuperType()

在这里子类实例和子类原型对象上都有 `name` 这个属性,而实际上这个属性不应该出现在原型对象上。

解决方法

为了解决这个问题,需要思考如何让原型真的像一个"原型"。其实有一个现成的真"原型",就是父类原型对象,在继承时只要照莫照样"抄"过来就行了,而这个抄的方法就在之前提过的原型式中。以父类原型对象为原型,克隆一个实例出来,那么这个实例就和父类原型对象有了"继承"关系。 同时针对继承属性这点,还是使用盗用构造函数的方式来实现。

实现

js 复制代码
function inherit(SubType, SuperType){
	const prototype = create(SuperType.prototype)
	prototype.constructor = SubType
	SubType.prototype = prototype
}

function SuperType(){}
function SubType(){
	SuperType.call(this)
}
inherit(SubType,SuperType)

这样只调用了一次父类构造函数,并且也不会让子类原型上有多于的属性。

总结

JS 中实现继承的基础就是原型链机制,提供了一种方法属性复用的方式。 红皮书中提到的六种继承方式可以分为两大类,基于类的和基于对象。基于类的继承方法中,原型链方法会出现原型属性在实例间共享的问题,盗用构造函数继承则无法实现原型方法的继承。因此提出了一种结合两者优点的方法------组合继承,通过原型链来继承原型上的方法,通过盗用构造函数能够让实例拥有自己的属性。但是组合继承会有一个问题,就是用一套属性在原型和实例上同时存在,造成了一定的冗余,所以提出了寄生式组合继承,保留盗用构造函数继承属性的做法,剔除掉子类原型上多余的属性,使用的方法是利用寄生式创造一个父类原型的副本,作为子类的原型。寄生式保证了子类和父类原型之间是有连接的,同时也避免在子类原型上保存冗余的属性。

疑问

  1. 原生式的实际的场景是什么呢?不定义类实现对象间的信息共享有什么好处呢?
  2. 为什么寄生组合叫寄生组合?怎么看这个方法和寄生式没啥关系,也不知道是不是我对寄生式的理解有什么问题。
相关推荐
非著名架构师7 分钟前
js混淆的方式方法
开发语言·javascript·ecmascript
银氨溶液39 分钟前
MySql数据引擎InnoDB引起的锁问题
数据库·mysql·面试·求职
多多米100541 分钟前
初学Vue(2)
前端·javascript·vue.js
敏编程1 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱1 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑1 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8561 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习1 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer2 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长2 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端