Return True

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, как у ребят в топе:

Топ результатов по задаче instance2

Значит, надо попробовать поискать другие варианты. Например, обязательно ли нам завязываться на 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, то:

  1. Если Target не «callable», то вернётся false.

  2. Если у 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 символов!