Эта статья шаг за шагом визуализирует абстрактные концепции прототипа и наследования от поверхностного к глубокому, и каждая точка знаний сопоставляется с соответствующими примерами, чтобы сделать ее максимально популярной, и самое большое преимущество этой статьи: длинная (для более подробной информации) .
1. Прототип
Прежде всего, давайте поговорим о прототипах, но когда мы говорим о прототипах, мы должны начать с функций, потому что объекты-прототипы относятся к тому, что имеют функции.prototypeАтрибуты (поэтому нижеследующее иногда относится к прототипам, а иногда кprototype, они оба относятся к прототипам).
1.1 Функции
Когда дело доходит до функций, мы должны сначала иметь понятие:Функции также являются объектами и имеют такие же свойства, как и объекты.,Например:
function F(a, b) {
return a * b;
}
F.length // 2 指函数参数的个数
F.constructor // function Function() { [native code] }
typeof F.prototype // "object"
Из вышеизложенного мы можем видеть, что у функций есть такие же свойства, как и у объектов, и мы сосредоточимся на том,prototypeЭто свойство прототипа.
prototypeЭто тоже объект.Для более наглядного понимания вышеизложенное лично я понимаю так:
// F这个函数对象里有个prototype对象属性
F = {
prototype: {}
}
Давайте поговорим об этомprototypeСвойства объекта.
1.2 Свойства объекта-прототипа
prototypeэто объект со свойством по умолчанию в немconstructor, значение по умолчанию указывает на текущую функцию, мы по-прежнему используем функцию F для иллюстрации:
F = {
prototype: {
constructor: F // 指向当前函数
}
}
теперь, когдаprototypeявляется объектом, то мы также можем добавить к нему свойства, например:
F.prototype.name = 'BetterMan';
// 那F就变成如下:
F = {
prototype: {
constructor: F,
name: 'BetterMan'
}
}
prototypeДавайте сначала заложим основу, поговорим об объектах, а затем свяжем их вместе.
1.3 Создание объектов
Существует много способов создания объектов, эта статья для прототипов, поэтому я расскажу о том, как использовать конструкторы для создания объектов. надFФункция на самом деле является конструктором (имя конструктора по умолчанию написано с заглавной буквы для простоты различения), поэтому мы используем его для создания объектов.
let f = new F();
console.log(f) // {}
На данный момент получается "пустой" объект.Пройдемся по процессу создания объекта конструктором:
- создать новый объект;
- Назначить область видимости конструктора новому объекту, то есть указать this на новый объект (там же есть процесс, свойство __proto__ нового объекта указывает на свойство ptototype конструктора, о чем будет рассказано позже).
- Выполнить код внутри функции, то есть добавить свойства новому объекту.
- Возвращает новый объект (не нужно писать, возвращает это по умолчанию, это относится к новому объекту).
Давайте изменим этоFКонструктор:
function F(age) {
this.age = age;
}
повторное использованиеFдля создания экземпляра объекта:
let f1 = new F(18); // 18岁,别来无恙
console.log(f1); // {age: 18}
На самом деле мы получилиf1объект, который имеетageсвойства, а на самом деле толькоageимущество? Выше мы говорили о процессе создания объекта конструктором.Здесь мы создаем новый объект,затем добавляем к объекту атрибуты,а потом возвращаем новый объект.Мы все это видим.Есть тоже процесс,т.е. новый объект.__proto__свойство указывает на конструкторptototypeАтрибуты.
Распечатаем:
console.log(f1.__proto__); // {constructor: F}
Разве это неFконструкторprototypeобъект? Этот процесс указания эквивалентенf1.__proto__ === F.prototype, это важно понимать!
__proto__Мы можем назвать его неявным прототипом (не все браузеры поддерживают это свойство, поэтому его запустил Google), это удивительно, так как он указывает на прототип конструктора, то мы можем получить его, когда получим. не используйте этот метод для получения прототипа, другие методы будут представлены позже).
Ранее мы говорили, что конструкторprototypeв объектеconstructorАтрибут указывает на свою функцию, тогда мы используем__proto__Давайте проверим:
console.log(f1.__proto__.constructor); // F(age) {this.age = age;}
// 因为f1.__proto__ === F.prototype,所以上述就是指F.prototype.constructor
Ну не плохо, вроде нормально!
На данный момент это должно быть относительно легко понять, поэтому давайте посмотрим:
console.log(f1.constructor); // F(age) {this.age = age;}
Что это, черт возьми? Объект экземпляраf1Еще одинconstructorСвойства и прототипы конструкторовconstructorто же самое, что указывать на конструктор? Это довольно интересно.
Не совсем так должно бытьf1таинственные свойства__proto__указал наF.prototype, что эквивалентно указывающей ссылке, если вы хотите визуализировать его, его можно понимать какF.prototypeИмущество "общее" прибываетf1body, но это динамически "расшаривается", если задняя частьF.prototypeизменять,f1Свойства, которые являются «общими», также изменятся. Это важно понимать! Важные вещи сказаны трижды! Важные вещи сказаны трижды! Важные вещи сказаны трижды!
Затем давайте «визуализируем» код:
F = {
prototype: {
constructor: F
}
};
f1 = {
age: 18,
__proto__: { // 既然我们已经把这个形象化为"共享"属性了,那就再形象一点
constructor: F
}
}
// 更形象化:
f1 = {
age: 18, // 这个是f1对象自身属性
constructor: F // 这个是从原型上"共享"的属性
}
Поскольку мы говорим о динамических «общих» свойствах, давайте изменим конструкторprototypeвнешний вид недвижимостиf1Будет ли это следовать за изменением:
// 没改之前
console.log(f1.name); // undefined
// 修改之后
F.prototype.name = 'BetterMan';
console.log(f1); // {age: 18}
console.log(f1.name); // 'BetterMan'
А (читая второй тон А)... вроде бы то же самое, что я и думал, ноf1выше не виделnameСвойства, то есть мы можем получить их только из прототипа конструктораnameсвойства вместо того, чтобы ставитьnameСтановится собственным свойством экземпляра объекта. Говоря об этом, мы должны упомянуть собственные свойства объекта и свойства прототипа (свойства, производные от прототипа).
1.4 Собственные свойства объекта и свойства прототипа
Экземпляр объекта, который мы создалиf1, имеет свои свойстваage, и свойства, найденные в прототипеname, мы можем использоватьhasOwnPropertyметод проверки:
console.log(f1.hasOwnProperty('age')); // true 说明是自身属性
console.log(f1.hasOwnProperty('name')); // false 说明不是自身属性
Поскольку это свойство объекта, его можно добавлять и удалять, верно? Давай попробуем:
delete f1.age;
console.log(f1.age); // undefined
delete f1.name;
console.log(f1.name); // 'BetterMan'
Лоб,ageУдаление атрибута прошло успешно, но похожеnameОтклика нет, он относительно твердый, значит,f1Объекты могут управлять своими свойствами, любить удалять, любить, добавлять, ноnameАтрибуты получены из прототипа, они являются атрибутами других, и вы не имеете права их модифицировать.
По сути, мы обращаемся к объектуnameатрибут, движок js по очереди будет запрашиватьf1Все свойства на объекте, но это свойство не найдено, то оно будет созданоf1Ищите прототип конструктора объекта экземпляра (это связано с таинственным атрибутом __proto__, который связывает объект экземпляра с прототипом конструктора), а затем находите его (если он снова не будет найден, он уйдет в Глядя вверх, это включает в себя цепочку прототипов, о которой мы поговорим позже). найтиageимущество непосредственно вf1Как только вы найдете его, вам не придется искать где-либо еще.
К настоящему времени у всех должно быть общее представление о прототипе, но какая от него польза? Это очень полезно.Можно сказать, что мы используем его постоянно.Давайте продолжим.
2. Наследование
Говоря о прототипе, он должен быть неотделим от темы наследования.Когда дело доходит до наследования, очень живо, что такое наследование в режиме прототипа, наследование в режиме конструктора, наследование в режиме объекта, наследование в режиме копирования атрибутов, множественное наследование, паразитическое наследование, композиция Наследование, паразитарное составное наследование... какого черта? Так много, что это головная боль, чтобы смотреть на?
Я лично разделяю их на три метода: метод прототипа, метод конструктора и метод объекта, а затем другие методы наследования основаны на сочетании этих трех методов.Конечно, это только мое личное понимание.Приступим.
2.1 Цепочка прототипов
Говоря о наследовании, нужно сказать, что цепочка прототипов, потому что цепочка прототипов является основным методом наследования.
Кратко рассмотрим отношения между конструкторами, прототипами и экземплярами: у каждого конструктора есть объект-прототип, а объект-прототип содержит указатель на конструктор (constructor), а экземпляр содержит внутренний указатель на объект-прототип (__proto__). Так что, если мы сделаем объект-прототип равным другому объекту-экземпляру? Очевидно, что объект-прототип в этот момент будет содержать указатель надругой прототипуказатель (__proto__),соответствующий,другой прототиптакже содержит указатель на другой конструктор (constructor). что еслидругой прототипЭто еще один экземпляр объекта, тогда вышеуказанная связь все еще сохраняется, и такие прогрессивные слои составляют цепочку экземпляров и прототипов. Это так называемая цепочка прототипов, как показано на рисунке:
Не путайте здесь, вы должны понять этот отрывок, а затем посмотреть вниз, на самом деле, эточужой экземпляр объектаприсваивается прототипу нашего конструктора, который является первым слоем, а затем, есличужой экземпляр объектаРазве это не то же самое, если прототипом конструктора является экземпляр объекта другого человека? Это второй слой, и если будет еще третья сторона, то это будет еще один слой, который представляет собой цепочку прототипов, соединенных слой за слоем.
Ну а если вы это видите, значит, вы поняли вышеописанную «цепную ситуацию», тогда начинаем заниматься наследованием.
2.2 Наследование
Есть несколько форм наследования. Давайте сравним преимущества и недостатки из них один за другим.
Примечание. Поскольку в большинстве случаев наследование основано на прототипах и цепочках прототипов, когда оно зависит от других методов, я называю метод наследования таким образом, чтобы он не казался таким сложным.
1. На основе метода конструктора
Сначала мы определяем три конструктора:
// 构造函数A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 构造函数B
function B() {
this.name = 'B';
};
// 构造函数C
function C(width, height) {
this.name = 'C';
this.width = width;
this.height = height;
this.getArea = function() {
return this.width * this.height;
};
};
Попробуем наследование:
B.prototype = new A();
C.prototype = new B();
Вышеупомянутое немного знакомо, это концепция цепочки прототипов, упомянутая выше: прототип конструктора B присваивается экземпляру объекта конструктора A, а затем прототип C присваивается экземпляру объекта B конструктор.
Затем мы используем конструктор C для создания экземпляра объекта:
let c1 = new C(2, 6);
console.log(c1); // {name: "C", width: 2, height: 6, getArea: ƒ}
console.log(c1.name); // 'C'
console.log(c1.getArea()); // 12
console.log(c1.say()); // 'C'
c1на самом деле естьsayОтлично, поздравляю, как это удалось? Пройдемся по процессу:
- ①Прежде всего
CСоздается новый «пустой» объект; - ②Тогда это указывает на этот «пустой» объект;
- ③c1.__proto__ указывает на C.prototype;
- ④ Присвойте значение этому объекту, чтобы
name,width,height,getAreaЭти четыре самостоятельных свойства; - ⑤ Возвращаем этот объект, в этот момент получаем
c1объект экземпляра; - ⑥ Затем распечатайте
console.log(c1)а такжеconsole.log(c1.name),console.log(c1.getArea())легко понять; - ⑦Далее
console.log(c1.say()), вы должны найтиsayметод, сначала движок jsc1Я искал его, не нашел, тогдаc1.__proto__Эта загадочная ссылка наCпрототип конструктора, то просто идиC.prototypeпосмотрите вверх, то мы написаны сC.prototype = new B()да, то естьBНайдите экземпляр объекта конструктора или его нет, затем продолжите и передайтеnew B().__proto__идтиBпосмотри на прототип, тогда у нас написано сB.prototype = new A();, то есть идтиAНайдите созданный объект экземпляра, если нет, то запустите сноваAНайдите прототип конструктора, ОК! оказаться!
Процесс эквивалентен этому:c1 —→ C.prototype —→ new B() —→ B.prototype —→ new A() —→ A.prototype
Это упомянутый выше процесс наследования, основанный на методе-конструкторе, который на самом деле является поисковым процессом, но нашли ли вы что-нибудь?
У описанного выше метода есть две проблемы: первая проблема заключается в том, чтоconstructorуказывает на.
изначальноB.prototypeсерединаconstructorхорошо указывать, указыватьBда, но сейчасB.prototypeполностьюnew A()заменен, текущийB.prototype.constructorКому оно адресовано? Давайте посмотрим:
console.log(B.prototype.constructor); // ƒ A() {}
let b1 = new B();
console.log(b1.constructor); // ƒ A() {}
В это время мы обнаружили, что не толькоB.prototype.constructorнаправлениеA,четноеb1Кроме того, не забывайтеb1серединаconstructorсвойства такжеB.prototypeподелился, так что начальник(B) поменял, брат (b1), конечно, будет меняться динамически.
Но почему сейчас, почему указывают наAчто о? потому чтоB.prototypeбыл замененnew A(),Этоnew A()что там? давайте положимB.prototypeа такжеnew A()Визуально представить:
A = {
prototype:{
constructor: A
}
};
new A() = {
name: 'A',
say: function() {
return this.name;
},
constructor: A // 由__proto__的指向所共享得到的
}
B = {
prototype:{
constructor: B
}
};
// 这时把B.prototype换为new A(),那就变成了这样:
B = {
prototype:{
name: 'A',
say: function() {
return this.name;
},
constructor: A // 所以指向就变成了A
}
};
Итак, мы собираемся исправить это вручнуюB.prototype.constructorнаправление, то жеC.prototype.constructorтакже указывает на:
B.prototype = new A();
B.prototype.constructor = B;
C.prototype = new B();
C.prototype.constructor = C;
Первая проблема решена, вторая проблема: проблема эффективности.
Когда мы создаем объект с помощью конструктора, к нему добавляются его свойства. И это неэффективно, когда добавленные свойства на самом деле не меняются вместе с экземпляром. Например, в приведенном выше примереAКонструктор определяется следующим образом:
function A() {
this.name = 'A';
this.say = function() {
return this.name;
};
};
Эта реализация означает, что мы используемnew A()Каждый созданный экземпляр будет иметь совершенно новыйnameсвойства иsayсвойств и иметь независимое место для хранения в памяти. Таким образом, мы должны рассмотреть возможность включения этих свойств в прототип и сделать их общедоступными:
// 构造函数A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 构造函数B
function B() {};
B.prototype.name = 'B';
// 构造函数C
function C(width, height) { // 此处的width和height属性是随参数变化的,所以就不需要改为共享属性
this.width = width;
this.height = height;
};
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
Таким образом, некоторые свойства в экземпляре, созданном конструктором, больше не являются частными свойствами, а являются свойствами, которые могут использоваться совместно в прототипе.Теперь давайте попробуем:
let test1 = new A();
let test2 = new A();
console.log(test1.say === test2.say); // true 没改为共享属性前,它们是不相等的
Хотя это обычно более эффективно, но это только для неизменяемых свойств в экземпляре, поэтому при определении конструктора нам также необходимо учитывать, какие свойства подходят для совместного использования, а какие — для приватного (и должны быть унаследованы до исходного прототипа). используется конструктор расширений и исправлений).
2. Подход на основе прототипа
Как и было сделано выше, для эффективности мы должны попытаться добавить в прототип некоторые повторно используемые свойства и методы, чтобы мы могли завершить построение отношения наследования, только полагаясь на прототип, потому что свойства на прототипе все Он многоразовый, что также означает, что наследование от прототипа намного лучше, чем наследование от экземпляра, а так как свойства, которые нужно наследовать, все размещаются на прототипе, то зачем генерировать экземпляр для снижения эффективности, а потом уже из сгенерированного Например, как насчет наследования нежелательных частных свойств? Поэтому мы просто отбрасываем экземпляр и наследуем от прототипа:
// 构造函数A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 构造函数B
function B() {};
B.prototype = A.prototype; // 先继承,再进行constructor矫正和B.prototype的扩展
B.prototype.constructor = B;
B.prototype.name = 'B';
// 构造函数C
function C(width, height) { // 此处的width和height属性是随参数变化的,所以就不需要改为共享属性
this.width = width;
this.height = height;
};
C.prototype = B.prototype;
C.prototype.constructor = C; // 先继承,再进行constructor矫正和C.prototype的扩展
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
Что ж, это выглядит намного эффективнее и привлекательнее, а затем мы попробуем эффект:
let b2 = new B();
console.log(b2.say()); // 'C'
(⊙o⊙)… не то чтобы это нужно было распечатыватьBЭто правильно? Почему оно отличается от маленького совершенства во мне?
Предположительно каждый должен это увидеть, приведенный выше метод наследования фактически эквивалентенA、B、CВсе используют один и тот же прототип, что создает проблему эталона, с которой мы столкнемся позже.Cна прототипеnameсвойства были изменены, поэтому на этот разA,B,CАтрибут имени прототипа — «C», и в настоящее время он действительно контролируется другими.
Есть ли способ получить лучшее из обоих миров, я хочу эффективности, но я не хочу, чтобы меня контролировали другие, пощечина! Не лучше ли совместить эти два метода? !
3. Объединение метода конструктора и метода-прототипа
Я хочу быть быстрым, но я не хочу, чтобы меня контролировали другие. Как насчет третьей стороны, чтобы решить эту проблему? (как это странно звучит). Мы используем временный конструктор (поэтому его еще можно назвать временным конструктором) в качестве моста между ними, разрывая отношения между младшим братом и старшим братом (сломанные ноги), и тогда все могут эффективно сотрудничать:
// 构造函数A
function A() {};
A.prototype.name = 'A';
A.prototype.say = function() {
return this.name;
};
// 构造函数B
function B() {};
let X = function() {}; // 新建一个"空"属性的构造函数
X.prototype = A.prototype; // 将X的原型指向A的原型
B.prototype = new X(); // B的原型指向X创建的实例对象
B.prototype.constructor = B; // 记得修正指向
B.prototype.name = 'B'; // 扩展
// 构造函数C
function C(width, height) { // 此处的width和height属性是随参数变化的,所以就不需要改为共享属性
this.width = width;
this.height = height;
};
// 同上
let Y = function() {};
Y.prototype = B.prototype;
C.prototype = new Y();
C.prototype.constructor = C;
C.prototype.name = 'C';
C.prototype.getArea = function() {
return this.width * this.height;
};
Теперь попробуйте, как это работает:
let c3 = new C;
console.log(c3.say()); // C
стабильный! Таким образом, вместо того, чтобы напрямую наследовать свойства экземпляра, мы наследуем свойства, общие для прототипа, а также можем передаватьXа такжеYЭти два "пустых" конструктора свойств для размещенияA和Bотфильтровать атрибут non-shared (потому чтоnew X()в сравнении сnew A()сгенерированный экземпляр, потому чтоXпуст, поэтому результирующий объект не будет иметь приватных свойств, ноnew A()Могут быть частные свойства.Поскольку это частные свойства, их не нужно наследовать, поэтомуnew A()Будут проблемы с эффективностью и ненужное наследование свойств).
4. Объектный подход
Этот объектно-ориентированный метод на самом деле включает в себя несколько методов. Поскольку все они связаны с объектами, я буду называть их объектным методом. Ниже мы представим их один за другим:
① В способе получения объекта
function create(o) { // o是所要继承的父对象
function F() {};
F.prototype = o;
return new F(); // 返回一个实例对象
};
let a = {
name: 'better'
};
console.log(create(a).name); // 'better'
Этот метод принимает родительский объект и возвращает экземпляр для достижения эффекта наследования. это не младшая версияObject.create()? Желающие могут узнать об этом подробнее. Так что этот метод на самом деле следует назвать «прототипным наследованием», потому что он также основан на модификации прототипа, но связан с объектом, поэтому я классифицирую его как объектный метод, который лучше подходит для классификации.
②При копировании свойств объекта
// 直接将父原型的属性拷贝过来,好处是Child.prototype.constructor没被重置,但这种方式仅适用于只包含基本数据类型的对象,且父对象会覆盖子对象的同名属性
function extend(Child, Parent) { // Child, Parent都为构造函数
let c = Child.prototype;
let p = Parent.prototype;
for (let i in p) {
c[i] = p[i];
}
};
// 这种直接拷贝属性的方式简单粗暴,直接复制传入的对象属性,但还是存在引用类型的问题
function extendCopy(p) { // p是被继承的对象
let c = {};
for (let i in p) {
c[i] = p[i];
}
return c;
};
// 上面的extendCopy可称为浅拷贝,没有解决引用类型的问题,现在我们使用深拷贝,这样就解决了引用类型属性的问题,因为不管你有多少引用类型,全都一个个拷过来
function deepCopy(p, c) { // c和p都是对象
c = c || {};
for (let i in p) {
if (p.hasOwnProperty[i]) { // 排除继承属性
if (typeof p[i] === 'object') { // 解决引用类型
c[i] = Array.isArray(p[i]) ? [] : {};
deepCopy[p[i], c[i]];
} else {
c[i] = p[i];
}
}
}
return c;
}
③Способ копирования многообъектных свойств
// 这种方式就可以一次拷贝多个对象属性,也称为多重继承
function multi() {
let n = {},
stuff,
j = 0,
len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (let i in stuff) {
if (stuff.hasOwnProperty(i)) {
n[i] = stuff[i];
}
}
}
return n
};
④Способ поглощения свойств объекта и расширения
На самом деле этот метод следует называть «паразитическое наследование», это название на первый взгляд выглядит очень абстрактно, но на самом деле оно одно и то же, поэтому он также делится на объектный метод:
// 其实也就是在创建对象的函数中吸收了其它对象的属性(寄生兽把别人的xx吸走),然后对其扩展并返回
let parent = {
name: 'parent',
toString: function() {
return this.name;
}
};
function raise() {
let that = create(parent); // 使用前面我们写过的create函数
that.other = 'Once in a blue moon!'; // 今天学的,丑显呗一下
return that;
}
Не слишком ли много способов отношения к объектам? Но на самом деле все дело в свойствах объекта, понять это несложно, продолжим.
5. Заимствование конструктора
По сути, этот метод тоже можно отнести к методу-конструктору, но он относительно скользкий, поэтому вынесу его отдельно (это последний, обещаю).
Давайте поставим старую функцию, определенную раньшеAДостать и обжарить:
// 构造函数A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 构造函数D
function D() {
A.apply(this, arguments); // 这里就相当于借用A构造函数把A中属性创建给了D,即name和say属性
};
D.prototype = new A(); // 这里负责拿到A原型上的属性
D.prototype.name = 'D'; // 继承后再进行扩展
Заботятся ли эти два шага о собственных свойствах A и свойствах прототипа? Просто и идеально!
Подожди, кажется, что-то не так,A.apply(this, arguments)уже совершенныйAсобственные свойства сталиDсвои свойства, ноD.prototype = new A()опять такиAОдин раз наследовать собственные атрибуты излишне, поскольку нам нужны только атрибуты прототипа, нельзя ли просто скопировать его напрямую?
// 构造函数A
function A() {
this.name = 'A';
};
A.prototype.say = function() {
return this.name;
};
// 之前定义的属性拷贝函数
function extend2(Child, Parent) {
let c = Child.prototype;
let p = Parent.prototype;
for (let i in p) {
c[i] = p[i];
}
};
// 构造函数D
function D() {
A.apply(this, arguments); // 这里就相当于借用A构造函数把A中属性创建给了D,即name和say属性
};
extend2(D, A); // 这里就直接把A原型的属性拷贝给了D原型
D.prototype.name = 'D'; // 继承后在进行扩展
let d1 = new D();
console.log(d1.name); // 'A'
console.log(d1.__proto__.name) // undefined 这就说明了name属性是新建的,而不是继承得到的
(⊙o⊙)… на самом деле есть и другие методы наследования, я их пока не пишу из-за боязни обыграть, а на самом деле они приходят и уходят на основе прототипов, конструкторов и объектов, лично я это делаю .Они так классифицируются, в конце концов, семь секунд памяти не отложишь, 囧.
наконец
Написав это, я, наконец, сглотнул свой последний вздох... Ба, я вздохнул с облегчением. Спасибо за просмотр конца. Надеюсь, он будет вам полезен. Если что-то не так, пожалуйста, дайте мне еще совет. Если вам понравилось, обратите на это внимание, и в будущем он будет постоянно обновляться.