Углубленный принцип наследования JavaScript

внешний интерфейс GitHub JavaScript
Углубленный принцип наследования JavaScript

Эта статья была впервые опубликована на личномGithub, вопросы/fxxk приветствуются.

ES6изclassДостаточно ли хорошо вы использовали синтаксический сахар? Тогда, если вы вернетесь кES5Шерстяная ткань? Эта статья будет продолжением предыдущей«Пустой JavaScript-прототип для всего»Вопросы в конце статьи如何用 JavaScript 实现类的继承расширить:

Благодаря этой статье вы узнаете:

  1. Как использоватьJavaScriptимитировать частные переменные в классе;
  2. Узнайте об общемJavaScriptМетоды наследования, принципы, их преимущества и недостатки;
  3. достичь относительно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Вопрос:

  1. Как добитьсяprivate方法Шерстяная ткань?
  2. Может ли это быть реализовано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)

Затем посмотрите на цепочку прототипов:

Идеально! Цепочка прототипов вышла, и проблема «кажется» идеально решенной! Но есть еще очевидные проблемы:

  1. Свойства создаются в цепочке прототипов (в целом не рекомендуется)
  2. фальсификация без разрешения__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')
}

Но у этого есть более очевидные недостатки:

  1. Родительский класс был произведен преждевременно, что привело к невозможности принять динамические параметры подкласса;
  2. Свойства по-прежнему создаются в прототипе. На этом этапе экземпляры нескольких подклассов будут иметь общие свойства одного и того же родительского класса. В конце концов, они будут влиять друг на друга!

Примеры недостатков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')

Хотя этот метод компенсирует некоторые недостатки двух вышеупомянутых методов, некоторые проблемы все же существуют:

  1. Подкласс по-прежнему не может передавать динамические параметры родительскому классу!
  2. Дважды вызывается конструктор родительского класса.

Очевидно, что этот метод дважды выполняет конструктор родительского класса, так что это не тот способ, которым мы в конечном счете хотим наследоваться.

прототипное наследование

Прототипное наследование на самом деле правильно类式继承Своего рода инкапсуляция, но ее уникальность в том, что она определяет чистый промежуточный класс следующим образом:

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)

Чтобы просмотреть результаты в браузере:

Кратко объясните:

  1. Подкласс наследует свойства и методы родительского класса, при этом свойства не создаются в цепочке прототипов, поэтому несколько подклассов не используют одно и то же свойство.
  2. Подклассы могут передавать динамические параметры суперклассу!
  3. Конструктор родительского класса только выполняется!

Хороший! Это метод наследования, который нам нужен. Однако ложка дегтя все же есть:

  • Если подкласс хочет добавить метод к прототипу, он должен быть добавлен после наследования, иначе метод исходного прототипа будет перезаписан. В этом случае, если есть уже два класса, справиться непросто.

Итак, мы можем немного оптимизировать его:

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 написали это, что я считаю наиболее разумным методом наследования:

  1. использоватьReflectвместоObject;
  2. использоватьReflect.getPrototypeOfзаменитьob.__ptoto__;
  3. использоватьReflect.ownKeysчитать все перечисляемые/неперечислимые/символические свойства;
  4. использоватьReflect.getOwnPropertyDescriptorпрочитать дескриптор атрибута;
  5. использовать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Другие классы не имеют отношений наследования.

Суммировать

  1. мы можем использоватьfunctionимитировать класс;
  2. JavaScriptНаследование классов основано на прототипе, совершенном методе наследования, и его процесс наследования довольно сложен;
  3. Хотя рекомендуется использовать его непосредственно в реальном производствеES6Наследование, но все же рекомендуется иметь глубокое понимание внутреннего механизма наследования.

Не по теме

Ставлю пасхалку в конце, почему особо подчеркиваю в паразитарном наследовании композицииenumerableКак насчет этого дескриптора свойства, потому что:

  • ES6изclass中,默认所有类的方法是不可枚举的! 😅

Выше, полный текст конца)

Примечание: Эта статья является личным резюме, и в некоторых выражениях могут быть пропуски.Если вы обнаружите, что в этой статье чего-то не хватает, во избежание недоразумений, пожалуйста, не стесняйтесь указать на это в комментариях или дать мне совет.issue, спасибо~