Эта статья была впервые опубликована на личномGithub, вопросы/fxxk приветствуются.
ES6изclassДостаточно ли хорошо вы использовали синтаксический сахар? Тогда, если вы вернетесь кES5Шерстяная ткань? Эта статья будет продолжением предыдущей«Пустой JavaScript-прототип для всего»Вопросы в конце статьи如何用 JavaScript 实现类的继承расширить:
Благодаря этой статье вы узнаете:
- Как использовать
JavaScriptимитировать частные переменные в классе; - Узнайте об общем
JavaScriptМетоды наследования, принципы, их преимущества и недостатки; - достичь относительно
fancyизJavaScriptМетод наследования.
Кроме того, если вы полностью понимаете终极版继承Вы поймете, что два разговора о основных знаниях, но также сможете показать, что у вас есть хорошийJavaScriptОснование.
Добрый
Давайте рассмотримES6 / TypeScript / ES5Обозначение класса для сравнения. Во-первых, мы создаемGithubUserкласс, который имеетloginметод и статический методgetPublicServices, получитьpublicСписок методов:
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:
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属性/方法Шерстяная ткань?
наследовать
Пользователи, использующие Nuggets, должны знать, что мы можем использовать их напрямую.GithubВойдите, затем, в сочетании с предыдущим разделом, если мы создадимJuejinUserнаследоватьGithubUser,ТакJuejinUserи его экземпляры можно назватьGithubизloginметод. Сначала напишите это простоеJuejinUserДобрый:
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)
Затем посмотрите на цепочку прототипов:
Идеально! Цепочка прототипов вышла, и проблема «кажется» идеально решенной! Но есть еще очевидные проблемы:
- Свойства создаются в цепочке прототипов (в целом не рекомендуется)
- фальсификация без разрешения
__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)
до сих пор не решен类式继承некоторые проблемы.
P.S. Лично я думаю
原型继承а также类式继承Должны быть непосредственно классифицированы как своего рода наследство! Но есть много мошенников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)
Чтобы просмотреть результаты в браузере:
Кратко объясните:
- Подкласс наследует свойства и методы родительского класса, при этом свойства не создаются в цепочке прототипов, поэтому несколько подклассов не используют одно и то же свойство.
- Подклассы могут передавать динамические параметры суперклассу!
- Конструктор родительского класса только выполняется!
Хороший! Это метод наследования, который нам нужен. Однако ложка дегтя все же есть:
- Если подкласс хочет добавить метод к прототипу, он должен быть добавлен после наследования, иначе метод исходного прототипа будет перезаписан. В этом случае, если есть уже два класса, справиться непросто.
Итак, мы можем немного оптимизировать его:
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описание:
- The
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Перечислимые методы в цепочке прототипов, кроме того, если сам подкласс унаследован от класса, указанное выше наследование не будет соответствовать требованиям.
Наследование Ultimate Edition
Чтобы сделать код более понятным, я используюES6Некоторые из API написали это, что я считаю наиболее разумным методом наследования:
- использовать
ReflectвместоObject; - использовать
Reflect.getPrototypeOfзаменитьob.__ptoto__; - использовать
Reflect.ownKeysчитать все перечисляемые/неперечислимые/символические свойства; - использовать
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, спасибо~