Встроенные прототипы JavaScript
Узнайте, как работают встроенные прототипы JavaScript: Object.prototype в корне цепочки, прототипы Array, Function и Number, заимствование методов через call и apply.
Изучаем встроенные прототипы
Встроенные прототипы в JavaScript — это объекты, которые такие конструкторы, как Array, Object, String, Number и Function, используют для того, чтобы предоставлять методы и свойства каждому создаваемому ими значению. Когда вы вызываете [1, 2, 3].map(...) или "hi".toUpperCase(), метод, который вы вызываете, не хранится в самом массиве или строке — он живёт в Array.prototype или String.prototype и находится при обходе цепочки прототипов.
На этой странице объясняется, где эти прототипы располагаются в цепочке, как их инспектировать, как заимствовать их методы для объектов, которые не являются настоящими массивами, и почему постоянное расширение встроенных прототипов не рекомендуется. Если вы только знакомитесь с тем, как работает сама цепочка, сначала прочитайте прототипное наследование.
Роль встроенных прототипов
Встроенные прототипы занимают центральное место в JavaScript: именно благодаря им любое литеральное значение сразу же снабжено полезными методами — без того, чтобы вы что-либо определяли. Понимание того, как они вписываются в цепочку прототипов, позволяет разбираться в сообщениях об ошибках, повторно использовать встроенные методы в неожиданных местах и избегать скрытых ошибок.
Object.prototype: корень цепочки
Почти каждый создаваемый вами объект в конечном счёте наследует от Object.prototype, который располагается в вершине цепочки. Именно оттуда берутся такие методы, как toString, hasOwnProperty и valueOf. Когда поиск не находит свойство ни на самом объекте, ни на каком-либо прототипе по пути, цепочка завершается на Object.prototype, а следующая ссылка равна null.
Прототипы Array, Function и Number
Встроенные типы добавляют собственный прототип поверх Object.prototype. Например, массив наследует от Array.prototype (который предоставляет map, filter, push и т.д.), а Array.prototype, в свою очередь, наследует от Object.prototype. Та же схема применима к функциям и числам.
Заимствование методов через call и apply
Поскольку встроенные методы живут в прототипах, их можно заимствовать и вызывать для любого совместимого значения с помощью call или apply. Классический пример — использование методов Array.prototype на массивоподобных объектах (таких как arguments или строка), у которых нет этих методов.
Расширение встроенных прототипов
Хотя JavaScript позволяет расширять встроенные прототипы, эта практика в целом не рекомендуется в глобальной области видимости из-за возможных конфликтов в крупных кодовых базах или со сторонними скриптами. Есть несколько конкретных причин избегать этого:
- Коллизии имён. Если два скрипта добавляют метод с одинаковым именем (или будущая версия ECMAScript стандартизирует это имя с иным поведением), один из них молча перезаписывает другой.
- Перечисляемость. Метод, добавленный обычным присваиванием, является перечисляемым, поэтому он будет появляться в циклах
for...inдля каждого объекта данного типа — если только не использовать защиту черезhasOwnProperty. Это частый источник ошибок. - Глобальные побочные эффекты. Изменение затрагивает каждое значение данного типа во всей программе, включая код, который вы не писали.
Фрагмент ниже демонстрирует ловушку с for...in. Обычное присваивание «утекает» в цикл; использование Object.defineProperty для создания неперечисляемого метода этого не делает.
Понимание этой возможности всё равно полезно — для распознавания потенциальных проблем, чтения полифиллов и изучения продвинутых паттернов в контролируемых средах.
Практические примеры работы со встроенными прототипами
Манипуляции с массивами через Array.prototype
Оцените мощь Array.prototype, который предлагает такие методы, как map, filter и reduce. Эти методы обеспечивают элегантные решения для преобразования данных, хранящихся в массивах, и работы с ними. Мы можем добавлять новые методы, манипулируя Array.prototype.
В этом примере мы определяем новый метод mapToSquare на прототипе, который использует встроенный метод map, чтобы вернуть квадрат каждого числа.
Расширение строк через String.prototype
String.prototype — ещё одно богатое хранилище методов: toLowerCase, toUpperCase, includes и другие, облегчающие операции со строками и их анализ.
В этом примере мы определяем removeSpace на прототипе, используя split, filter и join для удаления пробелов.
Пользовательские расширения встроенных прототипов
Хотя осторожность необходима, добавление пользовательских методов в встроенные прототипы может продемонстрировать гибкость JavaScript. Вот как можно расширить Array.prototype, добавив метод для вычисления суммы элементов массива:
Этот пользовательский метод sum добавляет новое измерение к прототипу Array, демонстрируя как потенциал, так и риски расширения встроенных прототипов.
Лучшие практики работы со встроенными прототипами
Хотя мощь встроенных прототипов неоспорима, вот несколько лучших практик, которые помогут сохранить ваш код надёжным и свободным от конфликтов:
- Избегайте расширения встроенных прототипов: если это не является абсолютной необходимостью, воздерживайтесь от изменения встроенных прототипов во избежание непредвиденного поведения вашего кода или сторонних библиотек.
- Используйте
Object.definePropertyдля безопасных расширений: когда нужно добавить методы, используйтеObject.defineProperty, чтобы сделать их неперечисляемыми. Это предотвращает попадание ваших пользовательских свойств в циклыfor...inи снижает вероятность конфликтов имён. - Используйте полифиллы осознанно: применяя полифиллы для добавления отсутствующих возможностей в старых браузерах, убедитесь, что они проверяют наличие метода перед тем, как добавить его в прототип.
- Используйте современные возможности JavaScript: с развитием языка многие задачи, для которых раньше требовалось расширение встроенных прототипов, теперь решаются с помощью новых конструкций — классов и модулей.
Заключение
Встроенные прототипы — это механизм, лежащий в основе каждого встроенного метода, которым вы пользуетесь ежедневно: они располагаются в цепочке прототипов, с Object.prototype в корне, и позволяют таким значениям, как массивы, функции и числа, разделять поведение. Умение инспектировать их с помощью Object.getPrototypeOf, заимствовать их методы через call и apply, а также не поддаваться искушению расширять их глобально сделает ваш код более выразительным и предсказуемым.
Для более глубокого изучения смотрите прототипное наследование — там объясняется, как строится цепочка, — и методы прототипов и объекты без __proto__ — там описывается современный API Object.create / Object.getPrototypeOf.