Эта статья была впервые опубликована на личном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, спасибо~