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 throughObject.prototype.toString
.
Работало просто:
> 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 ofarg
is"Array"
, then returntrue
.
Сам 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 таков:
Если у
Target
есть метод@@hasInstance
, то:Вызываем его, передавая в него
Value
.Полученное значение приводим к Boolean и возвращаем в качестве результата.
Если метода нет, то:
Находим
Target.prototype
.Сверяем его по очереди с
Value.[[Prototype]]
,Value.[[Prototype]].[[Prototype]]
и так далее.Если на очередном этапе проверки, какой-то прототип в цепочке
Value
равенnull
, возвращаемfalse
.Если в цепочке прототипов
Value
находим искомыйTarget.prototype
, возвращаемtrue
.Иначе возвращаем
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 символа! В таблице, конечно, результаты получше:
Но и мы только начали.
Ещё раз посмотрев на то, как работает 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 символов:
Но один из них говорит, что считерил, да и оба решения не верифицированы, потому возможно, что и первый тоже как-то что-то подхачил. А может быть и нет. Попробуйте сами найти более короткое решение, вдруг у вас получится.