本文首发于我的 Github,欢迎 issue / fxxk。git
ES6
的class
语法糖你是否已经用得是否炉火纯青呢?那若是回归到ES5
呢?本文,将继续上一篇 《万物皆空之 JavaScript 原型》 篇尾提出的疑问如何用 JavaScript 实现类的继承
来展开阐述:github
经过本文,你将学到:typescript
JavaScript
模拟类中的私有变量;JavaScript
继承方法,原理及其优缺点;fancy
的JavaScript
继承方法。此外,若是你彻底明白了文末的终极版继承
,你也就懂了这两篇所要讲的核心知识,同时,也能说明你拥有不错的JavaScript
基础。浏览器
咱们来回顾一下ES6 / TypeScript / ES5
类的写法以做对比。首先,咱们建立一个GithubUser
类,它拥有一个login
方法,和一个静态方法getPublicServices
, 用于获取public
的方法列表:app
class GithubUser {
static getPublicServices() {
return ['login']
}
constructor(username, password) {
this.username = username
this.password = password
}
login() {
console.log(this.username + '要登陆Github,密码是' + this.password)
}
}
复制代码
实际上,ES6
这个类的写法有一个弊病,实际上,密码password
应该是Github
用户一个私有变量,接下来,咱们用TypeScript
重写一下:函数
class GithubUser {
static getPublicServices() {
return ['login']
}
public username: string
private password: string
constructor(username, password) {
this.username = username
this.password = password
}
public login(): void {
console.log(this.username + '要登陆Github,密码是' + this.password)
}
}
复制代码
如此一来,password
就只能在类的内部访问了。好了,问题来了,若是结合原型讲解那一文的知识,来用ES5
实现这个类呢?just show you my code
:post
function GithubUser(username, password) {
// private属性
let _password = password
// public属性
this.username = username
// public方法
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
}
// 静态方法
GithubUser.getPublicServices = function () {
return ['login']
}
复制代码
值得注意的是,咱们通常都会把
共有方法
放在类的原型上,而不会采用this.login = function() {}
这种写法。由于只有这样,才能让多个实例引用同一个共有方法,从而避免重复建立方法的浪费。测试
是否是很直观!留下2
个疑问:优化
private方法
呢?protected属性/方法
呢?用掘金的用户都应该知道,咱们能够选择直接使用 Github
登陆,那么,结合上一节,咱们若是建立了一个 JuejinUser
来继承 GithubUser
,那么 JuejinUser
及其实例就能够调用 Github
的 login
方法了。首先,先写出这个简单 JuejinUser
类:ui
function JuejinUser(username, password) {
// TODO need implementation
this.articles = 3 // 文章数量
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
}
复制代码
因为ES6/TS
的继承太过直观,本节将忽略。首先概述一下本文将要讲解的几种继承方法:
看起来不少,咱们一一论述。
由于咱们已经得知:
若经过
new Parent()
建立了Child
,则Child.__proto__ = Parent.prototype
,而原型链则是顺着__proto__
依次向上查找。所以,能够经过修改子类的原型为父类的实例来实现继承。
第一直觉的实现以下:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
}
function JuejinUser(username, password) {
this.articles = 3 // 文章数量
JuejinUser.prototype = new GithubUser(username, password)
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
console.log(juejinUser1)
复制代码
在浏览器中查看原型链:
诶,不对啊,很明显 juejinUser1.__proto__
并非 GithubUser
的一个实例。
实际上,这是由于以前咱们为了可以在类的方法中读取私有变量,将JuejinUser.prototype
的从新赋值放在了构造函数中,而此时实例已经建立,其__proto__
还还指向老的JuejinUser.prototype
。因此,从新赋值一下实例的__proto__
就能够解决这个问题:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
}
function JuejinUser(username, password) {
this.articles = 3 // 文章数量
const prototype = new GithubUser(username, password)
// JuejinUser.prototype = prototype // 这一行已经没有意义了
prototype.readArticle = function () {
console.log('Read article')
}
this.__proto__ = prototype
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
console.log(juejinUser1)
复制代码
接着查看原型链:
Perfect!原型链已经出来,问题“好像”获得了完美解决!但实际上仍是有明显的问题:
__proto__
,致使 juejinUser1.__proto__ === JuejinUser.prototype
不成立!从而致使 juejinUser1 instanceof JuejinUser
也不成立😂。这不该该发生!细心的同窗会发现,形成这种问题的根本缘由在于咱们在实例化的时候动态修改了原型,那有没有一种方法能够在实例化以前就固定好类的原型的refernce
呢?
事实上,咱们能够考虑把类的原型的赋值挪出来:
function JuejinUser(username, password) {
this.articles = 3 // 文章数量
}
// 此时构造函数还未运行,没法访问 username 和 password !!
JuejinUser.prototype = new GithubUser()
prototype.readArticle = function () {
console.log('Read article')
}
复制代码
可是这样作又有更明显的缺点:
举例说明缺点2
:
function GithubUser(username) {
this.username = 'Unknown'
}
function JuejinUser(username, password) {
}
JuejinUser.prototype = new GithubUser()
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
const juejinUser2 = new JuejinUser('egoist', 'xxx', 0)
// 这就是把属性定义在原型链上的致命缺点,你能够直接访问,但修改就是一件难事了!
console.log(juejinUser1.username) // 'Unknown'
juejinUser1.__proto__.username = 'U'
console.log(juejinUser1.username) // 'U'
// 卧槽,无情地影响了另外一个实例!!!
console.log(juejinUser2.username) // 'U'
复制代码
因而可知,类式继承
的两种方式缺陷太多!
经过 call()
来实现继承 (相应的, 你也能够用apply
)。
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
}
function JuejinUser(username, password) {
GithubUser.call(this, username, password)
this.articles = 3 // 文章数量
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1.username) // ulivz
console.log(juejinUser1.username) // xxx
console.log(juejinUser1.login()) // TypeError: juejinUser1.login is not a function
复制代码
固然,若是继承真地如此简单,那么本文就没有存在的必要了,本继承方法也存在明显的缺陷—— 构造函数式继承
并无继承父类原型上的方法。
既然上述两种方法各有缺点,可是又各有所长,那么咱们是否能够将其结合起来使用呢?没错,这种继承方式就叫作——组合式继承
:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
}
function JuejinUser(username, password) {
GithubUser.call(this, username, password) // 第二次执行 GithubUser 的构造函数
this.articles = 3 // 文章数量
}
JuejinUser.prototype = new GithubUser(); // 第二次执行 GithubUser 的构造函数
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
复制代码
虽然这种方式弥补了上述两种方式的一些缺陷,但有些问题仍然存在:
本方法很明显执行了两次父类的构造函数,所以,这也不是咱们最终想要的继承方式。
原型继承其实是对类式继承
的一种封装,只不过其独特之处在于,定义了一个干净的中间类,以下:
function createObject(o) {
// 建立临时类
function f() {
}
// 修改类的原型为o, 因而f的实例都将继承o上的方法
f.prototype = o
return new f()
}
复制代码
熟悉ES5
的同窗,会注意到,这不就是 Object.create 吗?没错,你能够认为是如此。
既然只是类式继承
的一种封装,其使用方式天然以下:
JuejinUser.prototype = createObject(GithubUser)
复制代码
也就仍然没有解决类式继承
的一些问题。
PS:我我的以为
原型继承
和类式继承
应该直接归为一种继承!但无赖众多JavaScript
书籍均是如此命名,算是follow legacy
的标准吧。
寄生继承
是依托于一个对象而生的一种继承方式,所以称之为寄生
。
const juejinUserSample = {
username: 'ulivz',
password: 'xxx'
}
function JuejinUser(obj) {
var o = Object.create(obj)
o.prototype.readArticle = function () {
console.log('Read article')
}
return o;
}
var myComputer = new CreateComputer(computer);
复制代码
因为实际生产中,继承一个单例对象的场景实在是太少,所以,咱们仍然没有找到最佳的继承方法。
看起来很玄乎,先上代码:
// 寄生组合式继承的核心方法
function inherit(child, parent) {
// 继承父类的原型
const p = Object.create(parent.prototype)
// 重写子类的原型
child.prototype = p
// 重写被污染的子类的constructor
p.constructor = child
}
// GithubUser, 父类
function GithubUser(username, password) {
let _password = password
this.username = username
}
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
// GithubUser, 子类
function JuejinUser(username, password) {
GithubUser.call(this, username, password) // 继承属性
this.articles = 3 // 文章数量
}
// 实现原型上的方法
inherit(JuejinUser, GithubUser)
// 在原型上添加新方法
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1)
复制代码
来浏览器中查看结果:
简单说明一下:
Nice!这才是咱们想要的继承方法。然而,仍然存在一个美中不足的问题:
因此,咱们能够将其优化一下:
function inherit(child, parent) {
// 继承父类的原型
const parentPrototype = Object.create(parent.prototype)
// 将父类原型和子类原型合并,并赋值给子类的原型
child.prototype = Object.assign(parentPrototype, child.prototype)
// 重写被污染的子类的constructor
p.constructor = child
}
复制代码
但实际上,使用Object.assign
来进行copy
仍然不是最好的方法,根据MDN的描述:
Object.assign()
method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.其中有个很关键的词:enumerable
,这已经不是本节讨论的知识了,不熟悉的同窗能够参考 MDN - Object.defineProperty 补习。简答来讲,上述的继承方法只适用于copy
原型链上可枚举的方法,此外,若是子类自己已经继承自某个类,以上的继承将不能知足要求。
为了让代码更清晰,我用ES6
的一些API,写出了这个我所认为的最合理的继承方法:
Reflect
代替了Object
;Reflect.getPrototypeOf
来代替ob.__ptoto__
;Reflect.ownKeys
来读取全部可枚举/不可枚举/Symbol的属性;Reflect.getOwnPropertyDescriptor
读取属性描述符;Reflect.setPrototypeOf
来设置__ptoto__
。源代码以下:
/*! * fancy-inherit * (c) 2016-2018 ULIVZ */
// 不一样于object.assign, 该 merge方法会复制全部的源键
// 无论键名是 Symbol 或字符串,也不论是否可枚举
function fancyShadowMerge(target, source) {
for (const key of Reflect.ownKeys(source)) {
Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source, key))
}
return target
}
// Core
function inherit(child, parent) {
const objectPrototype = Object.prototype
// 继承父类的原型
const parentPrototype = Object.create(parent.prototype)
let childPrototype = child.prototype
// 若子类没有继承任何类,直接合并子类原型和父类原型上的全部方法
// 包含可枚举/不可枚举的方法
if (Reflect.getPrototypeOf(childPrototype) === objectPrototype) {
child.prototype = fancyShadowMerge(parentPrototype, childPrototype)
} else {
// 若子类已经继承子某个类
// 父类的原型将在子类原型链的尽头补全
while (Reflect.getPrototypeOf(childPrototype) !== objectPrototype) {
childPrototype = Reflect.getPrototypeOf(childPrototype)
}
Reflect.setPrototypeOf(childPrototype, parent.prototype)
}
// 重写被污染的子类的constructor
parentPrototype.constructor = child
}
复制代码
测试:
// GithubUser
function GithubUser(username, password) {
let _password = password
this.username = username
}
GithubUser.prototype.login = function () {
console.log(this.username + '要登陆Github,密码是' + _password)
}
// JuejinUser
function JuejinUser(username, password) {
GithubUser.call(this, username, password)
WeiboUser.call(this, username, password)
this.articles = 3
}
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
// WeiboUser
function WeiboUser(username, password) {
this.key = username + password
}
WeiboUser.prototype.compose = function () {
console.log('compose')
}
// 先让 JuejinUser 继承 GithubUser,而后就能够用github登陆掘金了
inherit(JuejinUser, GithubUser)
// 再让 JuejinUser 继承 WeiboUser,而后就能够用weibo登陆掘金了
inherit(JuejinUser, WeiboUser)
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1)
console.log(juejinUser1 instanceof GithubUser) // true
console.log(juejinUser1 instanceof WeiboUser) // true
复制代码
最后用一个问题来检验你对本文的理解:
inherit(A, B, C ...)
, 实现类A
依次继承后面全部的类,但除了A
之外的类不产生继承关系。function
来模拟一个类;JavaScript
类的继承是基于原型的, 一个完善的继承方法,其继承过程是至关复杂的;ES6
的继承,但仍建议深刻了解内部继承机制。最后放一个彩蛋,为何我会在寄生组合式继承中尤为强调enumerable
这个属性描述符呢,由于:
ES6
的class
中,默认全部类的方法是不可枚举的!😅以上,全文终)
注:本文属于我的总结,部分表达可能会有疏漏之处,若是您发现本文有所欠缺,为避免误人子弟,请放心大胆地在评论中指出,或者给我提 issue,感谢~