Return True

06. Array

function array(x,y) {
  return Array.isArray(x) && !(x instanceof Array)
     && !Array.isArray(y) &&  (y instanceof Array);
}

Хитрая задача, которая рассказывает о тяжкой судьбе массивов в Джаваскрипте.

Дело в том, что до ES5 единственным некривым способом убедиться, что перед нами массив, была проверка на instanceof. Однако, со временем вскрылась проблема, которая не была актуальной во времена написания спецификации ES3: несколько версий глобальных объектов.

Когда мы создаём новое окно, получаем оттуда какие-то массивы и проводим над ними операции, то они будто из другой вселенной:

> [] instanceof Array
true

> let w = window.open('javascript:window.arr=[]')
> w.arr
[]

> w.arr instanceof Array
false

А всё потому, что в разных окнах глобальные объекты разные. В текущем окне массив — экземпляр глобального класса Array, принадлежащего этому окну. В новом же окне или айфрейме будет создан уже экземпляр другого класса Array.

Потому до ES5 разработчики обычно шли на хитрость и использовали метод toString объектов, который, согласно спецификации, был единственным, что позволяло взглянуть на внутреннее свойство [[Class]]:

The value of a [[Class]] property is used internally to distinguish different kinds of built-in objects. Note that this specification does not provide any means for a program to access that value except through Object.prototype.toString.

— 8.6.2 Internal Properties and Methods

Работало просто:

> Object.prototype.toString.call([]) === "[object Array]"
true

Однако, это больше похоже на хак, чем на легальный способ проверки. Да и он многословен. Плюс приходится надеяться на то, что ни Object.prototype.toString, ни Function.prototype.call не будут как-то изменены сторонними инструментами.

И вот, пришёл ES5 и всё починил.


В ES5 добавили Array.isArray, который делал ровно то же самое, что и проверка через Object.prototype.toString, только под капотом:

If the value of the [[Class]] internal property of arg is "Array", then return true.

— 15.4.3.2 Array.isArray(arg)

Сам Object.prototype.toString же сильно не изменился. Разве что в него добавили проверки на undefined и null, чтобы получалось такое:

> Object.prototype.toString.call(null)
"[object Null]"

Таким образом, Array.isArray стал легитимным способом проверки объекта «на массивность». Поскольку он сравнивал название внутреннего свойства, а не объекты, то исправил проблему кросс-фреймовых массивов:

> [] instanceof Array
true

> let w = window.open('javascript:window.arr=[]')
> w.arr
[]

> Array.isArray(w.arr)
true

Причём его нельзя обмануть даже подменой прототипа:

> let arr = []
> arr.__proto__ === Array.prototype
true

> arr.__proto__ = null
> arr.__proto__ === Array.prototype
false

> Array.isArray(arr)
true

Потому что __proto__ перезаписывает внутреннее поле [[Prototype]], а не [[Class]]. Более того, несмотря на то, что методы у этого массива больше не работают (ведь их нет в прототипе), он всё равно ведёт себя как массив:

> arr.length
0

> arr.push(1)
TypeError: arr.push is not a function

> arr[0] = 1
> arr.length
1

Отчасти с этим связан тот факт, что прототип массива — тоже массив. Это может быть не совсем очевидно:

> Object.prototype.toString.call(Array.prototype)
"[object Array]"

> Array.prototype.length
0

> Array.prototype.push(1)
> Array.prototype[0]
1

Такое не работает для других прототипов:

> Object.prototype.toString.call(Date.prototype)
"[object Object]"

> Date.prototype.getTime()
TypeError: this is not a Date object

Все остальные объекты работают как обычные инстансы Object. И только у массива есть связь между нумерованными полями и свойством length.

Эта связь между нумерованными полями и свойством length настолько типична для массивов, что в ES6 Array.isArray больше не проверяет [[Class]], а проверяет именно наличие этой связи, реализованной с помощью внутренних методов. Более того, в спеке вообще больше нет понятия [[Class]].

Тем не менее, несмотря на такое поведение Array.prototype, у него в цепочке прототипов нет Array, а потому:

> Array.prototype instanceof Array
false

И это одно из возможных решений первой части задачи.


Чтобы найти решение для второй части, нужно разобраться, как работает instanceof. Если имеем Value instanceof Target, то, опуская некоторые детали, алгоритм в ES6 таков:

  1. Если у Target есть метод @@hasInstance, то:

    1. Вызываем его, передавая в него Value.

    2. Полученное значение приводим к Boolean и возвращаем в качестве результата.

  2. Если метода нет, то:

    1. Находим Target.prototype.

    2. Сверяем его по очереди с Value.[[Prototype]], Value.[[Prototype]].[[Prototype]] и так далее.

    3. Если на очередном этапе проверки, какой-то прототип в цепочке Value равен null, возвращаем false.

    4. Если в цепочке прототипов Value находим искомый Target.prototype, возвращаем true.

    5. Иначе возвращаем false.

Исторически так сложилось, что браузеры имплементировали свойство __proto__, с помощью которого можно получить доступ к прототипу объекта (то есть, к [[Prototype]]). До ES6 его даже не было в спецификации, но сейчас оно там описано.

И вот с его помощью вполне можно «обмануть» instanceof:

> let notArr = { __proto__: Array.prototype }

> notArr instanceof Array
true

Получается true, потому что в цепочке прототипов notArr есть Array.prototype. При этом с точки зрения Array.isArray массива тут, ясное дело, нет:

> Array.isArray(notArr)
false

Ведь такой объект не работает как массив: нет связи между нумерованными свойствами и полем length.

Получаем решение:

array(Array.prototype,{__proto__:Array.prototype})

43 символа! В таблице, конечно, результаты получше:

Топ по задаче array

Но и мы только начали.


Ещё раз посмотрев на то, как работает instanceof, мы можем сократить решение на 13 символов.

Поскольку instanceof проверяет прототипы переданного в левой части операнда по цепочке, то мы можем вместо Array.prototype использовать какой-то объект, в цепочке прототипов которого есть этот самый Array.prototype. А именно — инстанс обычного массива.

Вуа-ля:

> ({__proto__:[]}) instanceof Array
true

Получаем решение:

array(Array.prototype,{__proto__:[]})

Уже 30 символов!


Однако, дальше не понятно, куда двигаться. Можно было бы попробовать поиграться с @@hasInstance и как-то модифицировать Array. Но вот такое решение «в лоб» не заработает:

> array([],1,Array[Symbol.hasInstance]=x=>''+x!='')
false

Никто не говорил, что нельзя передавать третий параметр, когда функция его не ожидает. Это же Джаваскрипт.

Здесь мы проверяем, приводится ли переданный объект к пустой строке. Как мы помним, пустой массив приводится, а 1 превращается в строку "1".

Не заработает же это решение, потому что у встроенных функций нельзя просто так переопределять @@hasInstance, ибо оно не для записи.

Вот так можно:

array([],1,Object.defineProperty(Array,Symbol.hasInstance,{value:x=>''+x!=''}))

Но это аж 72 символа! Никуда не годится.

Если продолжать пытаться обманывать instanceof, то можно вместо Array.prototype использовать что-то покороче, чего не будет в цепочке прототипов Array, но что при этом будет массивом. Опять же, решение «в лоб» — взять массив и обнулить у него прототип:

array(x=[],{__proto__:[]},x.__proto__=null)
  

И про то, что нельзя определять переменные прямо в момент вызова функции, тоже никто не упоминал. Джаваскрипт! Воруй-убивай!

Однако, это 36 символов, и не понятно, куда тут короче.


Потому можно пойти другим путём. Как понятно из примеров выше, никто нам не запрещает модифицировать глобальные объекты, передавать третий аргумент, определять переменные прямо во время передачи параметров. А раз можно всё, то почему бы просто не поменять реализацию Array.isArray?

Мы можем не пытаться обмануть instanceof, а действительно передать два значения, где первое не будет массивом, а второе будет. И дополнительно к этому изменить Array.isArray. Например, как-нибудь так:

array(0,[],Array.isArray=x=>!x)

Здесь мы первым аргументом передаём 0, который по мнению instanceof точно не Array. А вторым передаём массив, который точно Array. А вот сам метод Array.isArray меняем так, чтобы он, получив 0 вернул true, а получив массив вернул false.

И вот у нас есть решение в 24 символа.

Сразу напрашивается ещё одна хитрая оптимизация. Мы можем всунуть переопределение Array.isArray прямо внутрь массива! Ведь содержимое этого массива нам не принципиально.

array(0,[Array.isArray=x=>!x])

Избавились от запятой и получили решение в 23 символа.


В найденном выше решении сложно что-то сократить. 0 и запятую сократить вряд ли получится. Array.isArray тоже никак не сократить. Вот жаль, что Джаваскрипт — это не какая-нибудь Кложа, и в нём нет функции вроде not! Было бы так удобно! Можно было бы передавать не анонимную функцию, а существующую:

array(0,[Array.isArray=not])

А хотя постойте. not и правда нет. Но ведь есть всякие другие глобальные функции. И одна из самых коротких — isNaN! Правда, работает она в нашем случае наоборот:

> isNaN(0)
false

> isNaN([Array.isArray=isNaN])
true

Но и мы не лыком шиты! Поменяем местами аргументы, делов-то:

array(Array.isArray=isNaN,[])

Вуа-ля! Получили решение в 22 символа. В таблице рекордов, правда, нашлись пользователи с решениями в 14 и 20 символов:

Топ по задаче array

Но один из них говорит, что считерил, да и оба решения не верифицированы, потому возможно, что и первый тоже как-то что-то подхачил. А может быть и нет. Попробуйте сами найти более короткое решение, вдруг у вас получится.