[JavaScript] Почему Array.isArray(Array.prototype) возвращает true?

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Авг-2020 22:35

Сегодня мы с вами разберемся в следующем: что за метод такой Array.isArray(), как он устроен под капотом, что изменилось с ним после выхода ES6, почему он возвращает для Array.prototype значение true и еще много связанных с этим методом тем.
Метод isArray() конструктора Array был добавлен начиная с 5-ой версии стандарта ECMAScript. На страничке описания этого метода на сайте MDN написано:
Метод Array.isArray() возвращает true, если объект является массивом и false, если он массивом не является.

И действительно, данный метод хорошо подходит для проверки различных значений на то, является ли это значение массивом. Однако у него есть одна особенность (куда же без них). В случае, если передать этому методу Array.prototype, который является объектом, то возвращается true. При том, что:
Array.prototype instanceof Array // false
Object.getPrototypeOf(Array.prototype) === Array.prototype // false
Array.prototype.isPrototypeOf(Array.prototype) // false
Array.prototype instanceof Object // true
Object.getPrototypeOf(Array.prototype) === Object.prototype // true
Object.prototype.isPrototypeOf(Array.prototype) // true

Такое неожиданное поведение может смутить не только рядового программиста на языке JavaScript, но и уже опытного бойца. Собственно это и побудило меня написать эту статью. Кто-то может сравнить это поведение со знаменитой особенностью JS:
typeof null === 'object' // true

Однако не надо спешить добавлять этот кейс в список wtfjs, потому что этому (внезапно) есть логичное объяснение. Но сначала давайте разберемся, зачем был создан метод isArray() и что скрыто у него под капотом.

Спойлер: Для тех, кто хочет знать ответ уже сейчас

SPL
Потому что Array.prototype это массив!

Предыстория
До 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); // 0
Array.prototype[42] = 'I\'m array';
Array.prototype[18] = 'I\'m array exotic object';
console.log(Array.prototype.length); // 43
Array.prototype.length = 20;
console.log(Array.prototype[42]); // undefined
console.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 и зачем он нужен.

===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_javascript, #_ecmascript, #_javascript
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 16-Май 14:08
Часовой пояс: UTC + 5