基于原型链的对象继承
基于原型链的对象继承
前言
A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法,这是面向对象编程很重要的一个方面,也对代码复用非常有用
大部分面向对象的编程语言,都是通过 “类”(class)实现对象的继承,而 JavaScript 则是通过原型对象(prototype)实现对象的继承,因此本文将介绍原型链的继承机制以及构造函数的继承方法
原型链
原型链是建立在构造函数的原型对象上的,即 prototype
属性,同时依赖于所有对象都拥有的 __proto__
属性以及原型对象上的 constructor
属性
一、构造函数的缺点
在了解原型对象前,我们必须知道为什么需要原型对象
在 ES5.1 中,由构造函数创建的实例对象,其自身的属性或方法的来源有两个:
Info
- 继承自构造函数:
所有通过该构造函数创建的实例对象都拥有的属性和方法(公共属性/方法) - 自定义赋值: 个别实例对象通过自定义赋值定义自身特有的属性和方法(私有属性/方法)
通过这两个来源我们可以发现,实例对象的 属性/方法 要么只能全盘接收来自构造函数的,要么只能自己定义
那当有一个 属性/方法 只需要在某几个实例对象中使用时,通过构造函数继承会使其他实例对象添加不必要的 属性/方法,而每个实例对象都进行自定义赋值显然不明智,因此我们可得出构造函数的缺点:
Info
- 继承自构造函数的属性和方法全部直接定义在实例对象的内部,无论该实例对象是否需要
- 即便各实例对象继承的 属性/方法 完全一样,但它们使用全等符号
===
却不相等,因为它们各自引向的内存地址不一样
上述缺点最直观的表现就是会造成系统内存资源的浪费,同时我们可推论出,实例对象所调用的继承自构造函数的属性和方法,没必要都定义在实例对象内部,只要能调用就可以
因此,针对构造函数的缺点的一个解决思路就是,将公共的属性或方法提取出来,放到一个对象中,让所有实例对象都可自主调用,从而实现共享属性或方法,节省系统资源,而这个对象,就是构造函数的 prototype
属性
ES2015 引入 class 关键字
ES5.1 生成实例对象的传统方法是使用构造函数,而在其他面向对象语言中则是使用 "类" 作为对象模板,但 ES5.1 并没有"类"的概念,且构造函数的写法与传统的面向对象语言(如 C++,Java)差异很大
因此 ES2015 提供了更接近传统语言的写法,引入了关键字 class
作为对象的模板:
- 类是 “特殊的函数”(
typeof
返回function
),也可简单认为类就是构造函数的另外一种写法 - 基本上,ES2015 的
class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已 - ES2015 中的类也是基于原型继承的,但由 ES2015 定义的类的某些语法语义,未与 ES5 类相关的语义共享
二、原型对象(prototype 属性)
JavaScript 继承机制的设计思想,是使原型对象的所有属性和方法,都能被实例对象共享,即如果属性和方法定义在原型对象上,那么所有实例对象就能共享,这样不仅节省了内存,还体现了实例对象之间的联系
因此 JavaScript 规定,每个对象的构造函数都有一个 prototype
属性,指向一个对象,即原型对象
Tips
- 函数本身也是一种对象,普通函数的
prototype
属性很少用到
但构造函数在生成实例时,其prototype
属性会自动成为实例对象的原型对象,换言之,prototype
属性必须要有实例对象生成才是指向原型对象 - 原型对象的属性并不是实例对象自身的属性,即实例对象可调用且不需要拥有
- 只要修改原型对象,变动就立刻会体现在所有实例对象上
prototype
属性就是用于定义所有实例对象共享的属性和方法,这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象
三、__proto__ 属性
在 JavaScript 中,每个对象都有一个 __proto__
属性,指向其构造函数的 prototype
属性
也就是说,对象的 __proto__
属性 与其构造函数的 prototype
属性是等价的
let obj = new Object()
obj.__proto__ === Object.prototype // true
__proto__
属性为调用对象属性/方法时的查找机制提供一个方向,它也是原型链建立的基础
非标准属性
__proto__
是一个非标准属性,实际开发中不能使用,只用于内部指向原型对象 prototype
,若需获取对象原型应使用 Object.getPrototypeOf()
四、constructor 属性
该属性定义在 prototype
属性上,默认指向 prototype
对象所在的构造函数,这意味着 constructor
属性可以被所有实例对象继承,继而通过该属性得知某个实例对象由哪一个构造函数产生的
function Father() {
this.a = 1
this.b = 2
}
Father.prototype.constructor === Father // true
let son = new Father()
son.constructor === Father // true
// 注意:因为实例对象son本身没有constructor属性,是继承自构造函数的prototype属性
// 所以实际是读取 Father.prototype.constructor
Tips
对象的属性__proto__
也有 constructor
属性,但因其为非标准属性,此处不讨论
与原型对象同步修改
constructor
属性表示原型对象与构造函数之间的关联关系
如果原型对象,即构造函数的 prototype
属性被赋值修改,那么 constructor
属性将不再指向原来的构造函数,而是修改后的对象的构造函数
因此,修改原型对象时,一般要同步修改 constructor
属性的指向
// 修改原型对象的两种方法:
function Constr(name) {
this.name = name
}
// 方法一:若以整个对象赋值,需重新定义constructor属性的指向
Constr.prototype = {
constructor: Constr,
method1: function () {}
}
// 或者赋值后单独重新定义constructor属性的指向:
Constr.prototype.constructor = 新原型对象
// 方法二:不直接赋值更改原型对象,而是添加原型对象的方法(推荐)
Constr.prototype.method1 = function () {}
// 利用方法二可扩展内置对象的功能,比如给数组添加求和的方法
Array.prototype.sum = function () {
let sum = 0
for (let i = 0; i < this.length; i++) {
sum += this[i]
}
return sum
}
Tips
在复杂的对象继承场景中,如果不清楚修改原型对象后的构造函数是哪个,可使用 constructor
属性的 name
属性获取修改后的构造函数名称
五、原型链
prototype
、__proto__
、constructor
属性就是原型链的三大元素
在 JavaScript 中,每个对象都拥有一个原型对象作为模板,从中继承属性和方法
而原型对象也可能拥有它自身的原型,并从中继承方法和属性,这样一层一层、以此类推,这种关系常就称为原型链(prototype chain)
原型链是建立在对象的构造函数的 prototype
属性上,而并非实例对象本身,它解释了为何一个对象可调用定义在其他对象中的属性和方法(继承机制)
原型链顶端
所有对象的原型最终都可上溯到 Object.prototype
,即 Object
构造函数的 prototype
属性
也就是说,所有对象都继承了 Object.prototype
的属性,这就是所有对象都有 valueOf()
和 toString()
方法的原因
Object.prototype
的原型是 null
:null
没有任何属性和方法,也没有自己的原型,因此,原型链的尽头就是 null
属性/方法
调用优先级
实例对象的实例对象调用自身没有的某个属性或方法:
- JavaScript 引擎会到原型对象去寻找该属性或方法,如果找不到就到原型的原型,直到最顶层的
Object.prototype
还是找不到,就返回undefined
- JavaScript 引擎会到原型对象去寻找该属性或方法,如果找不到就到原型的原型,直到最顶层的
实例对象调用自身拥有的某个属性或方法:
- 如果原型链中有同名的,则优先读取对象自身的属性或方法,称为 “覆盖”(overriding)
- 如果原型链中没有同名的,直接读取对象自身的属性或方法
Tips
所寻找的属性在越上层的原型对象,对性能的影响越大,如果寻找某个不存在的属性,将会遍历整个原型链
六、原型链图解

构造函数的继承
让一个构造函数继承另一个构造函数的需求很常见(本质也是实现对象的继承)
ES5.1 继承限制
在 ES 5.1 中,原生构造函数是无法继承的,即不能以内置对象作为父构造函数
比如,不能自己定义一个 Array 的子构造函数,即便没报错,继承后的子构造函数的实例根本不能调用原生构造函数的属性或方法,因为根本没继承到
ES2015 类继承
ES2015 的 class
继承使用的是 extends
关键字,用于继承另一个类
一、整体继承
整体继承表示子构造函数同时继承:
- 父构造函数自身的属性和方法
- 父构造函数的原型对象
因此继承步骤也大致分为两步:
Info
步骤一:继承父构造函数自身的属性和方法
- 在子构造函数中调用父类构造函数,继承父类的实例属性/方法
- 通过
call
方法指定父构造函数中的this
的运行环境为当前子构造函数的实例
Info
步骤二:继承父构造函数的原型对象
方法 1:以父构造函数原型对象作为原型,创造新的子构造函数原型并赋值给子构造函数的原型对象Son.prototype = Object.create(Father.prototype)
方法 2:通过 new
创建新的父类构造函数实例并将其赋值给子构造函数的原型对象 Son.prototype = new Father()
Tips
子构造函数的原型改变了,上述两种方法都必须将子构造函数的
constructor
属性指向原来的构造函数:Son.prototype.constructor = Son
如果直接将
父构造函数的原型对象
赋值给子构造函数的原型对象
,后续子构造函数原型对象
新增属性/方法或操作其constructor
属性,会同步改变父构造函数的原型对象
示例
function Father() {
this.x = 1
this.y = 2
}
// 继承父构造函数自身属性/方法
function Son() {
// 参数中的this指向子构造函数的实例对象
Father.call(this)
}
// 继承父构造函数的原型对象
Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son
// instanceof会对父类和子类的构造函数都返回true
let h1 = new Son()
h1 instanceof Father // true
h1 instanceof Son // true
二、单独继承某个属性/方法
只需在调用父构造函数自身或其原型对象上的属性/方法时,通过 call
或 apply
方法改变 this
指向即可
示例
Son.prototype.Getsum = function () {
Father.prototype.Getsum.call(this)
// 子类其他代码
}
// 子类继承了父类的Getsum方法后,还可以部署自己的代码
多重继承
JavaScript 中一个对象无法直接继承多个对象,但通过一些方法可实现,与构造函数的继承类似,下面以继承两个对象为例
Info
步骤一:
在子构造函数中同时调用需继承的两个父构造函数,同时通过 call
方法修改 this
指向,继承父构造函数自身的属性/方法
Info
步骤二:
先继承 父构造函数1
的原型对象,再通过 Object.assign()
方法复制 父构造函数2
的原型对象到子构造函数的原型对象中
同步修改 constructor 属性
子构造函数的原型改变了,constructor
属性也要指回子构造函数自己
示例
function M1() {
this.hello = 'hello'
}
function M2() {
this.world = 'world'
}
function S() {
M1.call(this)
M2.call(this)
}
// 继承 M1 原型对象
S.prototype = Object.create(M1.prototype)
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype)
// 同步修改子构造函数的constructor
S.prototype.constructor = S
var s = new S()
s.hello // 'hello'
s.world // 'world'
子类 S
同时继承了父类 M1
和 M2
,这种模式又称为 Mixin(混入)