Last active
August 8, 2021 18:46
-
-
Save daedalius/b92bb57b68f21d4b2260fd0a37bcd13b to your computer and use it in GitHub Desktop.
[RU] Прототипное наследование от __proto__ до функции inherit. ⚡️ Добро пожаловать. Снова.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/********************************************************** | |
object {}, __proto__, Object, Object.prototype, Object.create(protoToSet) | |
**********************************************************/ | |
test('У простого object есть прототип который можно получить через вызов Object.getPrototypeOf(obj)', () => { | |
const object = {} | |
expect(Object.getPrototypeOf(object)).toBeDefined() | |
// У всех объектов есть прототип (который так же __proto__) | |
}) | |
test('К прототипу object можно получить доступ через __proto__', () => { | |
const object = {} | |
expect(Object.getPrototypeOf(object)).toStrictEqual(object.__proto__) | |
// obj.__proto__ устаревшая форма обращения, но наглядная и поддерживается актуальными браузерами | |
// Object.getPrototypeOf(obj) актуальная форма, но менее наглядна | |
}) | |
test('У простого object в прототипе ссылка на Object.prototype', () => { | |
const object = {} | |
expect(Object.prototype === Object.getPrototypeOf(object)) | |
}) | |
test('У простого object метод toString определен в прототипе', () => { | |
const object = {} | |
expect(object.toString === Object.getPrototypeOf(object).toString) | |
}) | |
test('У простого object в прототипе прототипа лежит null', () => { | |
const object = {} | |
expect(object.__proto__.__proto__).toBe(null) | |
}) | |
test('У простого object можно заменить прототип новым объектом, свойства которого будут доступны из object', () => { | |
const object = {} | |
object.__proto__ = { propertyInPrototype: 'value of propertyInPrototype' } | |
expect(object.propertyInPrototype).toBe('value of propertyInPrototype') | |
// Поиск свойства в объекте осуществляется по цепочке прототипов пока не дойдет до null | |
}) | |
test('Можно расширить Object.prototype и новое свойство появится во всех новых и старых объектах (которые не изменяли свой прототип)', () => { | |
const oldObject = {} | |
Object.prototype.newProperty = 'new property value' | |
const newObject = {} | |
expect(oldObject.newProperty).toBe('new property value') | |
expect(newObject.newProperty).toBe('new property value') | |
// Это одно из самых мощных средств прототипного наследования | |
}) | |
test('При попытке заменить Object.prototype получаем исключение', () => { | |
expect( | |
jest.fn(() => { | |
Object.prototype = { newProperty: 'new value' } | |
}) | |
).toThrow() | |
}) | |
test('Можно при создании объекта сразу назначить ему прототип вызовом Object.create(objToProto)', () => { | |
const newObject = Object.create({ protoProperty: 'proto property value' }) | |
expect(Object.prototype.hasOwnProperty.call(newObject, 'protoProperty')).toBe(false) | |
expect(newObject.protoProperty).toBe('proto property value') | |
}) | |
/********************************************************** | |
constructor, delete, this | |
**********************************************************/ | |
test('При конструировании объекта через функцию, в экземпляре будет доступно свойство constructor, ссылающееся на функцию которая сконструировала объект', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
const instance = new SomeClass() | |
expect(instance.constructor).toStrictEqual(SomeClass) | |
// При создании функции у неё появляется prototype по умолчанию. Он состоит из одного поля - constructor. | |
}) | |
test('Имея любой экземпляр можно сконструировать ему подобные вызовом constructor', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
const instance = new SomeClass() | |
const anotherInstance = new instance.constructor() | |
expect(anotherInstance instanceof SomeClass).toBe(true) | |
}) | |
test('Менять свойство constructor у существующих экземпляров не имеет смысла и не влияет даже на instanseOf', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
const instance = new SomeClass() | |
instance.constructor = String | |
expect(instance instanceof SomeClass).toBe(true) | |
// instanceof смотрит на цепочку __proto__ | |
expect(instance.__proto__.constructor).toStrictEqual(SomeClass) | |
expect(instance.__proto__.__proto__.constructor).toStrictEqual(Object) | |
}) | |
describe('Сломать instanceof можно c двух концов:', () => { | |
test('от функции-конструктора: изменением prototype у функции-конструктора', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
const instance = new SomeClass() | |
SomeClass.prototype = new Object() | |
expect(instance instanceof SomeClass).toBe(false) | |
}) | |
test('от экземпляра: изменением prototype у instance.__proto__.constructor.prototype', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
const instance = new SomeClass() | |
instance.__proto__.constructor.prototype = new Object() | |
expect(instance instanceof SomeClass).toBe(false) | |
}) | |
// Оба способа меняют один объект - prototype у функции-конструктор | |
// Алгоритм работы instanceof | |
// * Взять объект obj и функцию fn. Пока не достигнут конец цепочки прототипов: | |
// * * сравнивать очередной __proto__ у объекта и свойство fn.prototype | |
// * * если они эквивалентны - вернуть true, иначе идти дальше или вернуть false | |
}) | |
test('Оператор delete не работает на цепочке прототипов явно', () => { | |
const a = { x: 1 } | |
const b = { x: 2 } | |
Object.setPrototypeOf(b, a) | |
expect(b.x).toStrictEqual(2) | |
// Удаление непосредственно из объекта b пройдет успешно | |
delete b.x | |
expect(b.x).toStrictEqual(1) | |
// При повторном удалении ничего не произойдет даже при наличии такого свойства в прототипе | |
delete b.x | |
expect(b.x).toStrictEqual(1) | |
// Теперь удаление произойдет т.к. выполняется явно у прототипа | |
delete b.__proto__.x | |
expect(b.x).toStrictEqual(undefined) | |
}) | |
test('this не имеет отношения к цепочке прототипов', () => { | |
const base = { | |
method() { | |
this.property = 'value' | |
}, | |
} | |
const child = {} | |
child.__proto__ = base | |
child.method() | |
expect(base.property).toBeUndefined() | |
expect(child.property).toBe('value') | |
// Неважно, где находится метод: в объекте или его прототипе. | |
// При вызове метода, this всегда - объект перед точкой. | |
// Также нужно понимать, что шаринг свойств имеет негативную сторону | |
// Для методов это норма, но данные обычно не требуется шарить между всеми экземплярами | |
// Поэтому есть рекомендация хранить в цепочке прототипов только функции | |
}) | |
/********************************************************** | |
Fn.prototype | |
**********************************************************/ | |
test('При установки функции-конструктору свойства prototype, оно станет прототипом экземпляров', () => { | |
function SomeClass() { | |
this.someProp = 'some value' | |
} | |
SomeClass.prototype = { someOtherProp: 'some other value' } | |
const instance = new SomeClass() | |
expect(instance.someOtherProp).toBe('some other value') | |
}) | |
test('Горячая замена prototype у функции-конструктора повлияет только на новые экземпляры', () => { | |
function SomeClass() {} | |
SomeClass.prototype = { someProp: 'some value' } | |
const firstInstance = new SomeClass() | |
SomeClass.prototype = { someOtherProp: 'some other value' } | |
const secondInstance = new SomeClass() | |
expect(firstInstance.someProp).toBe('some value') | |
expect(firstInstance.someOtherProp).toBeUndefined() | |
expect(secondInstance.someProp).toBeUndefined() | |
expect(secondInstance.someOtherProp).toBe('some other value') | |
// Так же перестанет работать instanceof для firstInstance | |
expect(firstInstance instanceof SomeClass).toBe(false) | |
expect(secondInstance instanceof SomeClass).toBe(true) | |
}) | |
/********************************************************** | |
Наследование | |
**********************************************************/ | |
const inherit = (childClass, parentClass) => { | |
// Классы в JS реализованы через функции | |
// static-свойства записываются напрямую в функцию (функция тут выступает как обычный объект) | |
// Копируем static-члены parent в child через установку __proto__ | |
// Это ничего не ломает по иерархиям обычных полей т.к. не влияет на prototype функции | |
// static-члены так же можно скопировать вручную | |
Object.setPrototypeOf(childClass, parentClass) | |
// for (const prop in parentClass) if (Object.prototype.hasOwnProperty.call(parentClass, prop)) childClass[prop] = parentClass[prop]; | |
// Создаем временную функцию-конструктор | |
// Помимо своего существования она в экземпляре оставит ссылку на себя | |
// (не очень важно, просто следует стандартному поведению) | |
function __() { | |
// Закомментируй эту строку и посмотри какой из тестов упадет | |
this.constructor = childClass | |
} | |
// В её prototype складываем prototype родительского класса | |
// Таким образом при создании нового экземпляра класса child он получит prototype от parent: | |
// * цепочка прототипов | |
__.prototype = parentClass.prototype | |
// Устанавливаем prototype целевого childClass новым экземпляром временной функции-конструктор | |
childClass.prototype = new __() | |
} | |
function ParentClass(parentProp) { | |
this.parentProp = parentProp | |
} | |
ParentClass.staticParentProp = 'Static ParentClass Prop Value' | |
// IIEF нужна чтобы выполнить функцию inherit единожды. | |
// Иначе у ChildClass каждый раз будет новый прототип, а значит, проверка (child instanceof ChildClass) провалится | |
// Так же сюда передается супер-класс и сохраняется в замыкании | |
const ChildClass = (function (superClass) { | |
function _ChildClass(parentProp, childProp) { | |
// Вызываем контруктор супер-класса (ParentClass в данном случае) | |
superClass.apply(this, [parentProp]) | |
// Здесь создаем собственные свойства | |
this.childProp = childProp | |
} | |
inherit(_ChildClass, superClass) | |
return _ChildClass | |
})(ParentClass) | |
ChildClass.staticChildProp = 'Static ChildClass Prop Value' | |
// // Версия без замыкания. Работает все за исключением (child instanceof ChildClass) | |
// function ChildClass(parentProp, childProp) { | |
// inherit(ChildClass, ParentClass) | |
// ParentClass.apply(this, [parentProp]) | |
// this.childProp = childProp | |
// } | |
// ChildClass.staticChildProp = 'Static ChildClass Prop Value' | |
test('Наследованный объект имеет доступ к своим полям', () => { | |
const x = new ChildClass(2, 1) | |
expect(x.childProp).toBe(1) | |
}) | |
test('Наследованный объект имеет доступ к полям потомка', () => { | |
const x = new ChildClass(2, 1) | |
expect(x.parentProp).toBe(2) | |
}) | |
test('Экземпляр наследованного класса имеет доступ к своему конструктору', () => { | |
const x = new ChildClass(2, 1) | |
expect(x.constructor).toBe(ChildClass) | |
}) | |
test('Наследованный класс имеет доступ к статическим членам потомка', () => { | |
expect(ChildClass.staticParentProp).toBe('Static ParentClass Prop Value') | |
}) | |
test('Наследованный класс имеет доступ к своим статическим членам', () => { | |
expect(ChildClass.staticChildProp).toBe('Static ChildClass Prop Value') | |
}) | |
test('Экземпляр наследованного класса проходит проверку на instanseof ParentClass', () => { | |
expect(new ChildClass(2, 1) instanceof ParentClass).toBe(true) | |
}) | |
test('Экземпляр наследованного класса проходит проверку на instanseof ChildClass', () => { | |
const newChild = new ChildClass(2, 1) | |
expect(newChild instanceof ChildClass).toBe(true) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment