Skip to content

Instantly share code, notes, and snippets.

@daedalius
Last active August 8, 2021 18:46
Show Gist options
  • Save daedalius/b92bb57b68f21d4b2260fd0a37bcd13b to your computer and use it in GitHub Desktop.
Save daedalius/b92bb57b68f21d4b2260fd0a37bcd13b to your computer and use it in GitHub Desktop.
[RU] Прототипное наследование от __proto__ до функции inherit. ⚡️ Добро пожаловать. Снова.
/**********************************************************
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