[JavaScript] Почему Array.isArray(Array.prototype) возвращает true?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Сегодня мы с вами разберемся в следующем: что за метод такой 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
===========
Похожие новости:
- [Open source, Системное администрирование, JavaScript, IT-инфраструктура] Решаем практические задачи в Zabbix с помощью JavaScript
- [Разработка веб-сайтов, JavaScript, Интерфейсы, ReactJS] Concurrent Mode в React: адаптируем веб-приложения под устройства и скорость интернета
- [Разработка веб-сайтов, JavaScript] Мои любимые трюки в JavaScript (перевод)
- [Разработка веб-сайтов, CSS, JavaScript, Клиентская оптимизация] Оптимизация производительности фронтенда. Часть 1. Critical Render Path
- [Разработка веб-сайтов, JavaScript, Тестирование веб-сервисов] Утреннее шоу с Lucas F. Costa: JS, CS и тестирование веб-приложений
- [Python, JavaScript, Программирование] Разбор статьи из журнала «Код» (Яндекс Практикум)
- [JavaScript, Интерфейсы, ReactJS, TypeScript] Когда и CRA мало. Доклад Яндекса
- [CMS, JavaScript, Распределённые системы] Создание собственной Headless CMS и интеграция с блогом (перевод)
- [Ajax, PHP, MySQL, JavaScript, jQuery] Пишем комментарии для сайта на чистом PHP + MySQL + Ajax
- [JavaScript, Java] Вы решили стать разработчиком. Почему нужно учить javascript, а не java?
Теги для поиска: #_javascript, #_javascript, #_ecmascript, #_javascript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:39
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Сегодня мы с вами разберемся в следующем: что за метод такой 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() работает верно. Единственный оставшийся у меня вопрос это, зачем было так делать. Зачем делать их прототипа конструктора массив? Есть ли у вас какие-то версии? Источники и ссылки на почитать
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:39
Часовой пояс: UTC + 5