08. Instance2
function instance2(a,b,c) {
return a !== b && b !== c && a !== c
&& a instanceof b
&& b instanceof c
&& c instanceof a;
}
После предыдущей задачи понятно, каким будет ответ:
instance2(Object,Function,Proxy)
21 символ. Однако, это только в старом Хроме. А как нам заставить эту функцию вернуть true
в современном браузере?
Первые два аргумента нам известны: Object
и Function
. Третий же мы можем создать, например, подхачив @@hasInstance
:
instance2(Object,Function,o={},o[Symbol.hasInstance]=x=>!0)
Просто передаём объект, в который добавляем поле @@hasInstance
с функцией, которая возвращает true
.
Но можно и доработать. Во-первых, можем использовать динамическое вычисление ключа, и тогда переменная для объекта будет не нужна:
instance2(Object,Function,{[Symbol.hasInstance]:x=>!0})
Во-вторых, проверка instanceof
так устроена, что значение, возвращаемое из @@hasInstance
само приводится к булеву типу. А потому можем просто вернуть единицу:
instance2(Object,Function,{[Symbol.hasInstance]:x=>1})
43 символа! Не 21, конечно, но тоже неплохо.
У нас уже есть Object
и Function
, потому в качестве третьего аргумента можно взять функцию, но попробовать сделать так, чтобы она вела себя как объект. Следите за руками:
instance2(Object,Function,f=()=>{},Object.setPrototypeOf(f, Object))
Чтобы понять, что именно тут происходит, надо разобраться, как работает прототипное наследование в JS.
Когда мы создаём обычную анонимную функцию fn
, её цепочка прототипов выглядит следующим образом:
fn -> Function.prototype -> Object.prototype -> null
Это легко проверить:
> const fn = () => {}
> let proto = Object.getPrototypeOf(fn)
> proto === Function.prototype
true
> proto = Object.getPrototypeOf(proto)
> proto === Object.prototype
true
> proto = Object.getPrototypeOf(proto)
> proto === null
true
От первого прототипа у функции есть методы вроде apply
и call
. От второго, например, __proto__
(да, это свойство берётся из Object.prototype
, потому его нельзя удалить у объектов).
Затем, когда устанавливаем fn
прототип в виде Object
, получаем:
fn -> Object
Но при этом, у Object
ведь есть своя цепочка прототипов, а потому:
fn -> Object -> Function.prototype -> Object.prototype -> null
И вот, такая странная цепочка прототипов и «ломает» instanceof
. Посмотрим на это пошагово:
Object instanceof Function && Function instanceof fn && fn instanceof Object
С первой частью мы разобрались ещё в прошлой задаче, потому опускаем её.
Вторая часть:
Function instanceof fn
Здесь первым делом вычисляется fn.prototype
. Однако, у fn
нет такого поля, ведь мы его туда не записывали. А потому оно возьмётся из прототипа — из Object
. Это как раз то место, где происходит основная магия:
> fn.prototype === Object.prototype
true
А дальше по цепочке будут вычисляться прототипы Function
, и там будет Object.prototype
, и они будут равны и жить долго и счастливо.
Третья часть скорее всего уже очевидна:
fn instanceof Object
Object
превратится в Object.prototype
, а он есть в цепочке прототипов fn
, и потому это тоже true
.
Зная о существовании __proto__
мы можем ещё подсократить решение:
instance2(Object,Function,f=()=>{},f.__proto__=Object)
Кстати, тоже 43 символа, как и в случае с решением с @@hasInstance
. Можем даже сократить его до 40 символов, если заменим дублирование Object
на переменную:
instance2(o=Object,Function,f=()=>{},f.__proto__=o)
А ещё нам не принципиально, что именно возвращает функция в третьем аргументе, потому можем сократить её сигнатуру:
instance2(o=Object,Function,f=x=>0,f.__proto__=o)
38 символов! Куда дальше сжимать не совсем понятно, но вот, поскольку при присваивании возвращается правая часть выражения, то можем избавиться от одной из переменных:
instance2(f=x=>0,Function,f.__proto__=Object)
34 символа.
34 всё ещё не 26, как у ребят в топе:
Значит, надо попробовать поискать другие варианты. Например, обязательно ли нам завязываться на Function
? Что, если сделать три таких полуфункции-полуобъекта, которые мы раньше использовали в третьем аргументе?
instance2(a=x=>0,b=x=>0,c=x=>0,a.__proto__=Object,b.__proto__=Object,c.__proto__=Object)
Последние три аргумента можно сократить с помощью функции:
instance2(a=x=>0,b=x=>0,c=x=>0,f=x=>x.__proto__=Object,f(a),f(b),f(c))
А ещё, поскольку нам участь функций не очень важна, то f
можно вынести вперёд:
instance2(f=x=>x.__proto__=Object,b=x=>0,c=x=>0,f(f),f(b),f(c))
Тут не понятно, куда дальше сокращать. От __proto__
не избавиться, синтаксис функций не сократить. Правда, можно одну из функций заменить на Object
, если использовать f(f)
:
instance2(f=x=>x.__proto__=Object,f(f),c=x=>0,f(c))
Но это всё равно 40 символов. Если последние два аргумента на Function
заменить, и то короче будет:
instance2(f=x=>x.__proto__=Object,f(f),Function)
Наверное, это путь не туда.
А что, если как-то вычислить Function
? Раз у нас есть функция, то можем попробовать брать её конструктор:
instance2(f=x=>0,f.constructor,f.__proto__=Object)
Но это ещё многословнее, чем раньше. Зато можно получить упоротое, но красивое решение:
instance2(f=x=>x.constructor,o=f({}),f(f),f.__proto__=o)
Наверное, вычисление конструктора нам тоже не поможет.
Может быть использовать Proxy
? Конечно, так, как в старом Хроме мы не сможем его использовать. Но ведь мы можем действительно создать какой-то прокси-объект, который будет инстансом оригинального объекта, но при этом не будет равен ему по ссылке. Например:
instance2(Object,new Proxy(Object,{}),new Proxy(Object,{}))
К сожалению, от new
не избавиться, как и от второго аргумента. Но можем подсократить Object
:
instance2(o=Object,new Proxy(o,{}),new Proxy(o,{}))
Вторым аргументом Proxy
ждёт объект с определёнными ключами, но нам-то всё равно, что туда попадёт. Потому можно и так:
instance2(o=Object,new Proxy(o,o),new Proxy(o,o))
(o,o)
выглядит как анимешный смайлик, но, увы, решение получается в 38 символов. Вроде как мы не можем взять и сделать три прокси, потому что компактно их нагенерировать не получается:
instance2(...[1,2,3].map(x=>new Proxy(Object,{})))
39 символов.
instance2((f=_=>new Proxy(Object,f))(),f(),f())
36 символов.
Но, как и в прошлых решениях, можем заиспользовать Function
:
instance2(o=Object,Function,new Proxy(o,o))
А это уже 32 символа! Правда, что дальше делать, тоже не ясно. Object
уже сократили, Function
никуда не деть, вызов Proxy
тоже максимально сжат.
Надо искать другие пути. И наверное без Function
, ибо он слишком длинный.
Если не понятно, что делать, надо перечитать спецификацию. Раньше мы уже разбирались с тем, как работает instanceof
, но опускали некоторые «детали». Однако, эти детали сейчас важны.
Так, если имеем Value instanceof Target
, и при этом у Target
нет @@hasInstance
, то:
Если
Target
не «callable», то вернётсяfalse
.Если у
Target
есть[[BoundTargetFunction]]
, то дальше проверка начнётся с самого начала, но при этом вместоTarget
будет ужеTarget.[[BoundTargetFunction]]
.
Пункт про «вызываемость» Target
нас в данном случае не волнует (хотя и объясняет, почему мы в наших первых попытках изменяли прототип функции, а не объекта). А вот что волнует, так это то, что, судя по второму пункту, забинженные объекты тоже могут участвовать в проверке instanceof
. И проверка будет выполняться как раз относительно того объекта, который изначально был забинжен. Иными словами:
> ({}) instanceof Object.bind(null)
true
Но при этом, вот какое дело:
> Object.bind(null) === Object
false
А значит, вот это решение нам вполне подходит:
instance2(Object,Object.bind(null),Object.bind(null))
И оно неплохо сжимается. Сперва, ясное дело, уберём дублирование Object
:
instance2(o=Object,o.bind(null),o.bind(null))
И это уже 34 символа. А затем можем убрать null
в аргументах, поскольку его можно опускать:
instance2(o=Object,o.bind(),o.bind())
Вуа-ля! 26 символов!