Сегодня мы с вами разберемся в следующем: что за метод такой Array.isArray(), как он устроен под капотом, что изменилось с ним после выхода ES6, почему он возвращает для Array.prototype значение true и еще много связанных с этим методом тем.
Метод isArray()
конструктора Array
был
добавлен начиная
с 5-ой версии стандарта ECMAScript. На страничке описания этого
метода на сайте
MDN написано:
Метод Array.isArray() возвращает true, если объект является массивом и false, если он массивом не является.
И действительно, данный метод хорошо подходит для проверки
различных значений на то, является ли это значение массивом. Однако
у него есть одна особенность (куда же без них). В случае, если
передать этому методу Array.prototype
, который
является объектом, то возвращается true
. При том,
что:
Array.prototype instanceof Array // falseObject.getPrototypeOf(Array.prototype) === Array.prototype // falseArray.prototype.isPrototypeOf(Array.prototype) // falseArray.prototype instanceof Object // trueObject.getPrototypeOf(Array.prototype) === Object.prototype // trueObject.prototype.isPrototypeOf(Array.prototype) // true
Такое неожиданное поведение может смутить не только рядового программиста на языке JavaScript, но и уже опытного бойца. Собственно это и побудило меня написать эту статью. Кто-то может сравнить это поведение со знаменитой особенностью JS:
typeof null === 'object' // true
Однако не надо спешить добавлять этот кейс в список wtfjs, потому что
этому (внезапно) есть логичное объяснение. Но сначала давайте
разберемся, зачем был создан метод isArray()
и что
скрыто у него под капотом.
Предыстория
До ES5 каноничным способом проверить, является ли объект
массивом, это использовать оператор instanseof
.
[] instanseof Array // true
Данный оператор проверяет содержит ли указанный объект (левый
операнд) в своей цепочке прототипов свойство prototype
переданного конструктора (правый операнд). Условно данную проверку
можно перезаписать следующим образом:
Object.getPrototypeOf([]) === Array.prototype // true
Однако, если разработчику приходится иметь дело с несколькими
пространствами (realm), что случается, когда разработка происходит
в нескольких iframe, каждый такой iframe имеет свой собственный
глобальный объект (window). Поэтому при проверке с помощью
instanseof Array
массива полученного из другого
пространства вернется false, так как конструктор Array одного
глобального объекта не равен Array другого глобального объекта.
В таком случае ушлые разработчики нашли способ, как можно
проверить объект на массив, не используя конструктор Array. Они
выяснили, что метод Object.prototype.toString()
выводит строку содержащую внутреннее свойство
[[Class]]
объекта. Так во многих библиотеках появилась
следующая функция для проверки массивов:
function isArray(obj) { return Object.prototype.toString.call(obj) === '[object Array]';}
Впоследствии данный метод добавили в спецификацию, как метод конструктора Array.
Array.isArray для Array.prototype
До ES6 внутреннее представление этого метода было именно
таким. Но почему для объекта
Arrray.prototype
метод
Object.prototype.toString()
возвращает [object
Array]
если:
Object.prototype.toString.call(Date.prototype) // [object Object]Object.prototype.toString.call(RegExp.prototype) // [object Object]
В спецификацию! В
ней про метод Array.isArray()
написано
следующее:
1. Если тип аргумента не является объектом то вернуть false.
2. Если значение внутреннего свойства [[Class]] переданного аргумента равно Array то вернуть true.
3. Вернуть false.
По этому же принципу для массивов работает метод
Object.prototype.toString()
. То есть получается,
что внутреннее свойство [[Class]]
объекта
Array.prototype
является Array? Не ошибка ли это?
Следует также сказать о реализации метода isArray()
в ES6. Несмотря на то, что выполняется этот метод также как и
раньше, внутренняя реализация этого метода существенно отличается.
В ES6 внутреннее свойство [[Class]]
больше не
используется и метод Object.prototype.toString()
внутренне теперь устроен совершенно
по-другому. Если использовать этот метод для массивов то
спецификация пишет следующее:
3. Пусть O это результат вызова ToObject(this value).
4. Пусть isArray это результат вызова isArray(O).
5. Если isArray равно true, то builtinTag равен Array.
...
Где isArray()
это внутренняя функция ES6 и именно
она вызывается при вызове метода Array.isArray()
вместо выполнения старого поведения. Полное
описание внутреннее метода isArray()
займет много
строк этой статьи, поэтому я скажу самое главное, а для любителей
почитать спеку оставлю ссылку.
Данный метод возвращает true
для тех объектов у
которых определен внутренний метод
[[DefineOwnProperty]]
, который отвечает за магию
массивов (это когда вы меняете количество элементов массива и это
влияет на изменение свойства length
и наоборот).
Возвращаясь к Array.prototype
мы получаем, что у
этого прототипа есть свойство [[DefineOwnProperty]]
.
Чудеса. Не верю. Пойдем проверять.
console.log(Array.prototype);// [constructor: f, concat: f, ..., length: 0, ..., __proto__: Object]
Хм. Как оказалось у нашего прототипа есть свойство
length
, несмотря на то, что в прототипе
(__proto__
) указан Object
. Но это еще
ничего не значит! Проверим его дескриптор.
console.log(Object.getOwnPropertyDescriptor(Array.prototype, 'length'));// {value: 0, writable: true, enumerable: false, configurable: false}
Все верно. Такой дескриптор имеет каждое свойство
length
у массивов. Но и это еще не все. Необходимо
проверить что прототип является Array exotic
object
console.log(Array.prototype.length); // 0Array.prototype[42] = 'I\'m array';Array.prototype[18] = 'I\'m array exotic object';console.log(Array.prototype.length); // 43Array.prototype.length = 20;console.log(Array.prototype[42]); // undefinedconsole.log(Array.prototype[18]); // 'I\'m array exotic object'
Выходит, что Array.prototype
это действительно
массив и никакой ошибки и нелогичности здесь нет. Давайте попробую
представить (как умею), как выглядит определение свойства
prototype
для конструктора Array
под
капотом.
Array.prototype = new Array();Object.assign(Array.prototype, {constructor() { ... }, concat() { ... }, ...});Object.setPrototypeOf(Array.prototype, Object.prototype);
Примерно таким образом можно создать прототип, который является
массивом, но не наследует ни одного метода от
Array.prototype
. Это также объясняет, почему у этого
объекта в свойстве [[Class]]
(которое инициируется при
создании экземпляра) было значение 'Array'
.
Другие объекты
Function, Date, RegExp
Прототипы конструкторов Date
и RegExp
представляют из себя обычные объекты (Object
), т.е. не
являются экземплярами своих собственных объектов, как это произошло
в случае с массивами.
Object.prototype.toString.call(Date.prototype); // [object Object]Object.prototype.toString.call(RegExp.prototype); // [object Object]
Однако Function.prototype
не является просто
объектом. В случае если вызвать метод
Object.prototype.toString()
для этого прототипа то
получим
Object.prototype.toString.call(Function.prototype); // [object Function]
Это значит, что Function.prototype
является
функцией и её можно вызвать.
Function.prototype() // undefined;
Такие дела)))
Примитивные объекты
В случае с использованием прототипов конструкторов примитивных
объектов (Boolean
, Number
,
String
) в методе
Object.prototype.toString
то получится следующее
Object.prototype.toString.call(Boolean.prototype); // [object Boolean]Object.prototype.toString.call(Number.prototype); // [object Number]Object.prototype.toString.call(String.prototype); // [object String]
В данном случае принцип такой же как и с массивами. Все эти
прототипы действительно являются экземплярами примитивных
конструкторов. У них есть соответствующее значение свойства
[[Class]]
и также они содержат другие внутренние
свойства для идентификации их как примитивных объектов
3. Пусть O это результат вызова ToObject(this value).
7. Иначе, если O является exotic String object то builtinTag равен String.
11. Иначе, если O имеет внутреннее свойство [[BooleanData]] то builtinTag равен Boolean.
12. Иначе, если O имеет внутреннее свойство [[NumberData]] то builtinTag равен Number.
Такое поведение сразу рождает в голове интересные примеры)))
String.prototype + Number.prototype + Boolean.prototype // '0false'(String.prototype + Boolean.prototype)[Number.prototype]; // 'f''Агент ' + Number.prototype + Number.prototype + '7'; // 'Агент 007'
Symbol.toStringTag
В случае, если применять метод
Object.prototype.toString()
к прототипам конструкторов
добавленных начиная с ES6, например Set
,
Symbol
, Promise
, то будет выводится
следующее:
Object.prototype.toString.call(Map.prototype); // [object Map]Object.prototype.toString.call(Set.prototype); // [object Set]Object.prototype.toString.call(Promise.prototype); // [object Promise]Object.prototype.toString.call(Symbol.prototype); // [object Symbol]
У всех таких прототипов нет внутренних свойств, которые могли бы
влиять на вывод Object.prototype.toString
, как у
массивов и примитивных объектов. Однако подобный вывод стал
возможен с появлением в языке стандартных символов, а именно
символа
@@toStringTag
. Его используют как свойство объекта
и в качестве значения передают строку которая будет выводится в
методе Object.prototype.toString()
. У всех объектов,
появившихся после ES5 такой метод определен в прототипе, поэтому мы
и имеем такой результат, хотя в действительности
ни Set.prototype
, ни Promise.prototype
не
являются объектами Set
и Promise
соответственно.
Также данное свойство можно определить в своих собственных
конструкторах и классах, чтобы управлять выводом метода
Object.prototype.toString()
.
Вывод
Array.prototype
является массивом в понимании
ECMAScript спецификации. И хотя наследуется он от объекта, его
внутренние свойства говорят, что является массивом, а значит метод
Array.isArray()
работает верно. Единственный
оставшийся у меня вопрос это, зачем было так делать. Зачем делать
их прототипа конструктора массив? Есть ли у вас какие-то
версии?
Источники и ссылки на почитать
- ES5 ссылка на спецификацию 5-ого стандарта ESMAScript.
- ES6 ссылка на спецификацию 6-ого стандарта ESMAScrip.t
- ECMAScript 6 для разработчиков | Закас Николас очень легкая для понимания книга, которая при этом очень подробно объясняет все нововведения в язык.
- Determining with absolute accuracy whether or not a JavaScript object is an array хорошая статья, объясняющая, что такое метод Array.isArray и зачем он нужен.