Глубокое понимание цепочки прототипов и наследования

JavaScript Примечания
Глубокое понимание цепочки прототипов и наследования

предисловие

JavaScript — это объектно-ориентированный язык, все объекты наследуют свойства и методы от прототипа, так что же такое прототип? Как добиться наследования между объектами?

Эта статья поможет вам глубже понять прототипы в JavaScript. Заинтересованные разработчики могут прочитать эту статью.

Принципиальный анализ

Далее давайте шаг за шагом проанализируем отношения между прототипом и объектом.

прототип объекта

Мы используемfunctionключевое слово для создания функции, памятиprototypeОбъект свойств, указывающих на функциюпрототип объекта,Следующим образом:

function Person() {
    
}
Person.prototype // {constructor: Person(), __proto__}

В приведенном выше коде мы создали функцию с именем Person:

  • Свойство прототипа указывает на объект-прототип Person (каждый объект JS, кроме null, связан с другим объектом при его создании, и этот связанный объект является прототипом)
  • Каждый объект «наследует» свойства от прототипа
  • Объект-прототип содержитconstructorа также__proto__Атрибуты.

image-20210310173710555

Давайте нарисуем картину, чтобы описатьPersonа такжеprototypeОтношение между

image-20210310195733848

Функция, вызываемая оператором new,Конструктор, рекомендуется писать имена конструкторов с заглавной буквы.

Связь между экземплярами функций и объектами-прототипами

В прошлой главе мы разъяснилиКонструктора такжепрототип объектаотношения, давайте посмотримЭкземпляр функцииа такжепрототип объектаОтношение между.

мы используем операторnewСоздано из предыдущей главыPersonСоздается экземпляр функции и получается экземпляр человека Код выглядит следующим образом:

// 实例化对象
const person = new Persion();

В прошлой главе мы знаем, что объект-прототип имеет 2 свойства, среди которых__proto__— это свойство, которое есть у каждого объекта JavaScript, кроме null, и оно указывает на объект-прототип конструктора объекта.

Далее докажемperson.__proto__ли иPersion.prototypeРавно, код выглядит следующим образом:

function Person() {
    
}
const person = new Persion();
console.log("函数实例的__proto__指针指向构造函数的原型对象: ", person.__proto__ === Person.prototype);

Результат выполнения следующий:

image-20210312172907318

Помимо использования__proto__Чтобы получить доступ к объекту-прототипу, мы также можем использовать Object.getPrototypeOf() для его получения.

Доказав, что они равны, объедините конструктор и объект-прототип, чтобы узнать взаимосвязь между ними тремя, следующим образом:

image-20210310202939966

Когда мы создаем экземпляр конструктора, конструктор также создается для этого экземпляра.__proto__свойство, это свойство является указателем на объект-прототип конструктора.

Все объекты экземпляра, созданные одним и тем же конструктором__proto__Его свойства направлены на конструкторы объектов-прототипов, свойства и методы, поэтому все экземпляры объектов являются общими функциями конструктора объекта-прототипа, поэтому после изменения свойства или метода объекта-прототипа все объекты будут затронуты экземплярами.

Мы думаем о такой проблеме, так как каждый объект JS, кроме null, имеет__proto__свойство, то Person также является объектом, который также содержит__proto__собственность, так куда же она указывает?

Ответ очевиден, как мы сказали выше:__proto__Указатель на прототип объекта конструктора.

Покажем пример:

function Person() {}
Person.__proto__ === Person.constructor.prototype // true
  • В приведенном выше коде мы использовалиconstructorсвойство, указывающее на конструктор объекта, который мы подробно объясним в следующей главе.

Результат выполнения следующий:

image.png

Связь между объектами-прототипами и конструкторами

В прошлой главе мы проанализировали объект-прототип__proto__Направление, давайте проанализируем дальшеconstructorуказывает на. Каждый объект-прототип имеетconstructorсвойство, которое указывает на конструктор объекта.

Далее докажемPerson.prototype.constructorли иPersonРавно, код выглядит следующим образом:

function Person() {

}

const person = new Person();
console.log("原型对象与构造函数相等: ", Person.prototype.constructor === Person);

Результат выполнения следующий:

image-20210310211445102

Доказав, что они равны, мы объединяем конструктор, экземпляр функции и объект-прототип, чтобы узнать взаимосвязь между ними четырьмя, следующим образом:

image-20210310212117231

Чтобы получить прототип объекта, помимо доступа к его прототипу, мы также можем использовать Object.getPrototypeOf() для его получения.

Порядок чтения свойств экземпляра

При чтении свойств в экземпляре, если он не найден, он будет искать свойства в прототипе объекта, если не найден, он найдет прототип прототипа, и он найдет самый верхний уровень.

Далее приведем пример, подтверждающий вышеприведенное утверждение:

function Person() {

}
Person.prototype.name = "原型上的name属性";
const person = new Person();
person.name = "实例上的name属性";
console.log(person.name) // 实例上的name属性

delete person.name;
console.log(person.name); // 原型上的name属性

delete Person.prototype.name;
console.log(person.name); // undefined

Давайте проанализируем приведенный выше пример:

  • Добавлен атрибут имени в прототип
  • Добавлен атрибут name для экземпляра
  • На данный момент значением атрибута name является атрибут name экземпляра.
  • Удален атрибут имени в экземплярах
  • На этом этапе он будет искать атрибут имени в прототипе, поэтому значением является атрибут имени в прототипе.
  • Убран атрибут имени в прототипе
  • В этот момент он найдет значение имени прототипа прототипа.Прототип прототипа не имеет атрибута имени, поэтому возвращает undefined

В приведенном выше анализе мы не нашли атрибут name в прототипе прототипа Person, так что же является прототипом прототипа Person? Распечатаем его в консоли Google Chrome, как показано ниже:

image-20210310222010988

Как показано в результате на приведенном выше рисунке, прототип прототипа Person является объектом, что доказывает, что прототип также является объектом, тогда мы можем создать его самым примитивным способом, код выглядит следующим образом:

const object = new Object();
object.name = "对象中的属性";
console.log(object.name); // 对象中的属性
console.log("object实例的__proto__指向Object的实例对象", object.__proto__ === Object.prototype);
console.log("Object的原型对象与构造函数相等", Object.prototype.constructor === Object);

Результат выполнения следующий:

image-20210312175303383

После знания того, что прототип также является объектом, в сочетании с тем, что мы доказали выше, связь между ними следующая:

image-20210310224603680

Сеть прототипов

Из предыдущего анализа мы знаем, что все основано на Object, так что же является прототипом Object? ответ нулевой

Проверяем его на консоли Google Chrome, и результат такой:

image-20210310231037617

Объединяя вышеизложенное, их окончательное соотношение выглядит следующим образом:

image-20210310232450892

Цепная структура, образованная оранжевыми линиями на рисунке,Сеть прототипов.

Перепишите объект-прототип

Когда мы реализуем какую-то функциональность, мы часто переписываем весь объект-прототип с литералом объекта, который содержит все свойства и методы.

Следующим образом:

Person.prototype = {
    name: "神奇的程序员",
    age: "20",
    job: "web前端开发",
    sayName: function () {
        console.log(this.name);
    }
}
  • Направьте прототип Person на новый объект
  • Прототип имеет три свойства и один метод
  • Свойство конструктора не существует в объекте

Поскольку в переопределенном объекте нет свойства конструктора, его свойство конструктора будет указывать на Object.

Проверим, код такой:

console.log("Person的原型对象的构造函数与Person构造函数相等", Person.prototype.constructor === Person)

Результат выполнения следующий:

image-20210312211047400

Если значение конструктора очень важно, то нужно намеренно изменить указатель конструктора на конструктор Код такой:

Person.prototype = {
    name: "神奇的程序员",
    age: "20",
    job: "web前端开发",
    sayName: function () {
        console.log(this.name);
    },
    constructor: Person
}
console.log("Person的原型对象与Person构造函数相等", Person.prototype.constructor === Person)

Результат выполнения следующий:

image-20210311000952986

Наследование цепочки прототипов

В предыдущей главе, посвященной принципиальному анализу, на итоговой схематической диаграмме мы можем интуитивно увидеть, как выглядит цепочка прототипов.Далее давайте рассмотрим конкретную концепцию цепочки прототипов.

  • Каждый конструктор имеет прототип объекта
  • Все объекты-прототипы содержат указатель на конструктор (constructor)
  • Каждый экземпляр конструктора содержит внутренний указатель на объект-прототип (__proto__)
  • Если объект-прототип сделать равным экземпляру другого конструктора, объект-прототип будет содержать указатель на прототип другого конструктора.
  • Соответственно прототип другого конструктора также содержит указатель на другой конструктор
  • Если другой прототип является экземпляром другого конструктора, указанное выше отношение остается в силе.
  • Таким образом формируется цепочка экземпляров и прототипов, оранжевая линия, которую мы видим на схематической диаграмме Это основная концепция цепочки прототипов.

Далее мы используем пример для объяснения наследования цепочки прототипов.Код выглядит следующим образом:

function Super() {
    this.property = true;
}
Super.prototype.getSuperValue = function() {
    return this.property;
}
function Sub() {
    this.subProperty = false;
}

// Sub原型指向Super实例,constructor被重写,指向Super
Sub.prototype = new Super();
Sub.prototype.getSubValue = function () {
    return this.subProperty;
}

let sub = new Sub();
console.log("获取Super的属性值", sub.getSuperValue());
console.log("sub实例的原型对象等于Sub构造函数的原型对象", sub.__proto__ === Sub.prototype);
console.log("Sub构造函数的原型对象的原型对象等于Super构造函数的原型对象", Sub.prototype.__proto__ === Super.prototype)
console.log("Sub构造函数的原型对象constructor指向Super的构造函数", Sub.prototype.constructor === Super)

Результаты приведены ниже:

image-20210312215707154

  • Во-первых, мы создали функцию с именем Super и добавили внутрь свойство с именем property со значением true.
  • Впоследствии на прототип объекта Super добавилиgetSuperValueметод, который возвращаетpropertyАтрибуты
  • Впоследствии мы создали функцию под названием Sub internal, добавив свойство с именем subProperty, значение равно false
  • Затем мы указываем объект-прототип Sub на экземпляр Super.На этом этапе реализовано наследование, и прототип Sub будет иметь методы прототипа Super.
  • Впоследствии мы добавили к объекту прототипа SubgetSubValueметод, который возвращаетsubPropertyАтрибуты
  • Наконец, мы создаем экземпляр объекта Sub, который образует цепочку прототипов с объектом Super, что соответствует отношениям, о которых мы говорили в основном анализе.

Затем мы нарисуем вышеуказанное содержание анализа на диаграмме, чтобы лучше понять его, как показано ниже (оранжевая линия — это цепочка прототипов):

image-20210311112200899

существующие проблемы

Когда мы используем цепочку прототипов для реализации наследования, если мы наследуем ссылочный тип объекта-прототипа, то этот ссылочный тип будет общим для всех экземпляров.

Далее мы проиллюстрируем эту проблему на примере:

function Super() {
    this.list = ["a","b","c"];
}

function Sub() {

}
Sub.prototype = new Super();
const sub1 = new Sub();
sub1.list.push("d");
console.log(sub1.list);
const sub2 = new Sub();
console.log(sub2.list);

В приведенном выше коде:

  • Сначала объявляются два конструктора Super и sub.
  • Затем укажите прототип объекта Sub на экземпляр Super для реализации наследования.
  • Затем создайте экземпляр подобъекта, чтобы получитьsub1пример
  • Добавить элемент d в список экземпляров sub1
  • В этот момент элементы массива списка экземпляра sub1[ 'a', 'b', 'c', 'd' ]
  • Затем снова создайте экземпляр Sub объекта, чтобы получитьsub2пример
  • В этот момент элементы массива списка экземпляра sub2[ 'a', 'b', 'c', 'd' ]

Результаты приведены ниже:

image-20210311141113229

Проблема очевидна, мы не добавляем элементы в массив списка sub2, мы хотим, чтобы его значение было определено на прототипе Super["a","b","c"].

Поскольку атрибут списка, определенный в конструкторе Super, является ссылочным типом, он используется совместно во время создания экземпляра.[ 'a', 'b', 'c', 'd' ]

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

Наследование конструктора

В конструкторе подкласса мы можем использоватьcallСкопируйте все свойства и методы конструктора родительского класса в текущий конструктор, чтобы после создания экземпляра изменение свойств и методов было скопированным содержимым, которое не повлияет на содержимое в конструкторе родительского класса.

Далее, давайте проиллюстрируем приведенное выше утверждение на примере:

function Super() {
    this.list = ["a","b","c"];
}

function Sub() {
    Super.call(this)
}
const sub1 = new Sub();
sub1.list.push("d");
console.log("sub1" ,sub1.list);
const sub2 = new Sub();
console.log("sub2", sub2.list);

Приведенный выше код, пример предыдущего раздела, который мы следуем, где разговора с измененной частью:

  • Мы используем вызов в конструкторе Sub, чтобы скопировать свойства и методы в Super для реализации наследования.

  • Поскольку атрибуты копируются в текущий экземпляр каждый раз, когда они создаются, элементы, добавленные в sub1, не повлияют на sub2.

Результаты приведены ниже:

image-20210312112758516

существующие проблемы

Мы используем конструктор для реализации наследования, и мы не можем наследовать методы и свойства на прототипе родительского класса, поэтому возможность повторного использования функции будет потеряна.

Продолжим в качестве примера взять код из предыдущей главы:

function Super() {
    this.list = ["a","b","c"];
}

Super.prototype.newList = [];

function Sub() {
    Super.call(this)
}
const sub1 = new Sub();
console.log("sub1" ,sub1.newList);
  • Мы добавили в суперпрототипnewListАтрибуты
  • не существует при доступе в экземпляре sub1

Результаты приведены ниже:

image-20210311160013667

наследование композиции

После анализа первых двух глав мы знаем, что метод наследования цепочки прототипов может наследовать свойства и методы объекта-прототипа, а наследование конструктора может наследовать свойства и методы функции-конструктора, они дополняют друг друга, поэтому мы объединят свои силы. Вставай, дело сделанонаследование композиции, а также прекрасно компенсируют соответствующие им недостатки.

Далее мы используем пример для объяснения наследования композиции:

// 组合继承
function Super(name) {
    this.name = name;
    this.list = ["a","b","c"];
}
Super.prototype.getName = function () {
    return this.name;
}
function Sub(name, age) {
    // 构造函数继承,第二次调用父类构造函数
    Super.call(this,name);
    this.age = age;
}
// 原型链继承,第一次调用父类构造函数
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);

В приведенном выше коде:

  • Сначала создайтеSuperФункция принимает параметр имени, а в конструкторе есть два атрибута: имя и список.
  • Впоследствии мыSuperдобавить объект-прототипgetNameМетод, возвращающий значение атрибута name в Super
  • Затем мы создаемSubФункция принимает два параметра: имя, возраст, добавляет атрибут возраста в конструктор и наследует атрибуты и методы в конструкторе родительского класса.
  • Затем мы переписываем прототип объекта конструктора Sub в экземпляр Super и модифицируем конструктор, чтобы он указывал на
  • Впоследствии мыSubдобавить объект-прототипgetAgeМетод, возвращающий свойство age в Sub
  • Наконец, мы создаем два экземпляра конструктора Sub для проверки унаследованного метода.

Результаты приведены ниже:

image-20210311175629506

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

Когда мы реализуем составное наследование, мы дважды вызываем конструктор родительского класса.

При первом вызове конструктора родительского класса:

  • Мы переписываем объект-прототип Sub, чтобы он указывал на экземпляр Super.
  • На этом этапе свойства и методы экземпляра конструктора родительского класса назначаютсяSub.prototype

Этот звонок, где мы находимсяНаследование цепочки прототиповКак упоминалось в главе, подкласс унаследовал свойства и методы конструктора родительского класса, а также свойства и методы объекта-прототипа.

При вызове конструктора родительского класса во второй раз:

  • Внутри конструктора Sub мы используемcallбудуSuperСвойства и методы назначаются экземплярам Sub.
  • Когда цепочка прототипов ищет свойства, свойства в экземпляре блокируют свойства в цепочке прототипов.

Поэтому при втором вызове нам не нужно назначать свойства и методы в конструкторе экземпляру конструктора Sub, что бессмысленно.

Далее давайте посмотрим на оптимизированное наследование композиции:

function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}

Super.prototype.getName = function () {
    return this.name;
}

function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}

// 创建一个中间函数,用于继承Super的原型对象
function F() {

}
// 将F的原型对象指向Super的原型对象
F.prototype = Super.prototype;
// 将Sub的原型对象指向F的实例
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);

В приведенном выше коде:

  • Сначала мы создали промежуточную функциюF
  • Впоследствии объект-прототип F был переписан, и на объект-прототип F было прямо указано.Superпрототип объекта
  • Наконец, мы указываем объект-прототип Sub на экземпляр F, тем самым реализуя наследование цепочки прототипов.

Его эффективность заключается в том, что конструктор Super вызывается только один раз при создании экземпляра, а цепочка прототипов остается неизменной.

Результаты приведены ниже:

image-20210311212111885

Оптимизированное наследование композиции, также известное какнаследование паразитарного состава, в приведенном выше коде реализации мы используем промежуточную функцию для реализации наследования цепочки прототипов, эту промежуточную функцию также можно использоватьObject.create()Вместо этого принципы их реализации одинаковы.

Затем при переопределении объекта-прототипа конструктора Sub мы можем написать:Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})

Изменить прототип объекта указывает на наследование реализации

В предыдущей главе мы реализовали наследование цепочки прототипов с помощью промежуточной функции. Мы также можем напрямую передать объект-прототип подкласса через__proto__Атрибут указывает на объект-прототип родительского класса.Таким образом, объект-прототип подкласса не меняется, поэтомуconstructorСвойство также указывает на конструктор родительского класса.

Далее, давайте проиллюстрируем приведенное выше утверждение на примере:

function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}

Super.prototype.getName = function () {
    return this.name;
}

function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}
// 修改Sub构造函数的原型对象指向改为Super的原型对象
Sub.prototype.__proto__ = Super.prototype;
Sub.prototype.getAge = function () {
    return this.age;
}

const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);

В приведенном выше коде:

  • Мы используем__proto__Свойство изменяет объект прототипа Sub, чтобы он указывал на

существуетПринципиальный анализглава, мы знаем, что все, кромеnullвнешние объекты javascript имеют__proto__Свойство по умолчанию указывает на объект-прототип этого объекта, поэтому мы можем изменить объект-прототип, на который он указывает через это свойство.

Мы также можем использовать ES6Object.setPrototypeOf()метод для изменения прототипа объекта.

Затем код в приведенном выше примере может быть изменен на:Object.setPrototypeOf(Sub.prototype, Super.prototype)

Статическое наследование методов для конструкторов

Добавьте метод непосредственно в конструктор, этот методстатический метод.

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

мы можем пройтиObject.setPrototypeOf()Методы реализуют наследование статических методов.

Далее объясним это на конкретном примере:

function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}

Super.prototype.getName = function () {
    return this.name;
}

// 添加静态方法
Super.staticFn = function () {
    return "Super的静态方法";
}

function Sub(name, age) {
    // 继承Super构造函数中的数据
    Super.call(this, name);
    this.age = age;
}

// 修改Sub的原型指向
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 继承父类的静态属性与方法
Object.setPrototypeOf(Sub, Super);

Sub.prototype.getAge = function () {
    return this.age;
}

console.log(Sub.staticFn());
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());

Результаты приведены ниже:

image-20210312000444311

Остальная часть приведенного выше кода такая же, как и в предыдущем примере, давайте проанализируем различия:

  • Сначала мыSuperКонструктор добавляетstaticFnстатический метод
  • Затем мы проходимObject.setPrototypeOf(Sub, Super)функция наследуетSuperСтатические свойства и методы конструктора

На данный момент мы добились идеального наследования, которое также является базовой реализацией синтаксического сахара класса ES6.

синтаксический сахар класса

Новый был добавлен в ES6classмодификатор, мы используемclassПосле того, как модификатор создаст два объекта, мы можем использоватьextendsключевое слово для реализации наследования. Его основная реализация - это то, о чем мы говорили выше.наследование паразитарного составакомбинироватьСтатическое наследование методов для конструкторовбыть реализованным.

Далее, давайте посмотрим на пример, приведенный в предыдущей главе, как использовать класс для реализации, код выглядит следующим образом:

class Super {
    constructor(name) {
        this.name = name;
        this.list = ["a","b","c"];
    }

    getName() {
        return this.name;
    }
}
// 向Super添加静态方法
Super.staticFn = function () {
    return "Super的静态方法";
}

class Sub extends Super{
    constructor(name, age) {
        super(name);
        this.age = age;
    }

    getAge() {
        return this.age;
    }
}

console.log(Sub.staticFn())
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());

Результаты приведены ниже:

image-20210312004416434

кодовый адрес

Весь пример кода из этой серии статей можно найти по адресу:js-learning

напиши в конце

Эта статья является второй статьей в серии "Изучение принципов JS". Пожалуйста, перейдите к полному маршруту этой серии:Изучение принципов JS (1) 》Планирование маршрута обучения

  • Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь 😊
  • Эта статья была впервые опубликована на Наггетс, перепечатка без разрешения запрещена 💌