Свойство "prototype"
широко используется внутри самого языка JavaScript. Все встроенные функции-конструкторы используют его.
Сначала мы рассмотрим детали, а затем используем "prototype"
для добавления встроенным объектам новой функциональности.
Object.prototype
Давайте выведем пустой объект:
let obj = {};
alert( obj ); // "[object Object]" ?
Где код, который генерирует строку "[object Object]"
? Это встроенный метод toString
, но где он? obj
ведь пуст!
…Но краткая нотация obj = {}
– это то же самое, что и obj = new Object()
, где Object
– встроенная функция-конструктор для объектов с собственным свойством prototype
, которое ссылается на огромный объект с методом toString
и другими.
Вот что происходит:
Когда вызывается new Object()
(или создаётся объект с помощью литерала {...}
), свойство [[Prototype]]
этого объекта устанавливается на Object.prototype
по правилам, которые мы обсуждали в предыдущей главе:
Таким образом, когда вызывается obj.toString()
, метод берётся из Object.prototype
.
Мы можем проверить это так:
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
// obj.toString === obj.__proto__.toString === Object.prototype.toString
Обратите внимание, что по цепочке прототипов выше Object.prototype
больше нет свойства [[Prototype]]
:
alert(Object.prototype.__proto__); // null
Другие встроенные прототипы
Другие встроенные объекты, такие как Array
, Date
, Function
и другие, также хранят свои методы в прототипах.
Например, при создании массива [1, 2, 3]
внутренне используется конструктор массива Array
. Поэтому прототипом массива становится Array.prototype
, предоставляя ему свои методы. Это позволяет эффективно использовать память.
Согласно спецификации, наверху иерархии встроенных прототипов находится Object.prototype
. Поэтому иногда говорят, что «всё наследует от объектов».
Вот более полная картина (для трёх встроенных объектов):
Давайте проверим прототипы:
let arr = [1, 2, 3];
// наследует ли от Array.prototype?
alert( arr.__proto__ === Array.prototype ); // true
// затем наследует ли от Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// и null на вершине иерархии
alert( arr.__proto__.__proto__.__proto__ ); // null
Некоторые методы в прототипах могут пересекаться, например, у Array.prototype
есть свой метод toString
, который выводит элементы массива через запятую:
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- результат Array.prototype.toString
Как мы видели ранее, у Object.prototype
есть свой метод toString
, но так как Array.prototype
ближе в цепочке прототипов, то берётся именно вариант для массивов:
В браузерных инструментах, таких как консоль разработчика, можно посмотреть цепочку наследования (возможно, потребуется использовать console.dir
для встроенных объектов):
Другие встроенные объекты устроены аналогично. Даже функции – они объекты встроенного конструктора Function
, и все их методы (call
/apply
и другие) берутся из Function.prototype
. Также у функций есть свой метод toString
.
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, наследует от Object
Примитивы
Самое сложное происходит со строками, числами и булевыми значениями.
Как мы помним, они не объекты. Но если мы попытаемся получить доступ к их свойствам, то тогда будет создан временный объект-обёртка с использованием встроенных конструкторов String
, Number
и Boolean
, который предоставит методы и после этого исчезнет.
Эти объекты создаются невидимо для нас, и большая часть движков оптимизирует этот процесс, но спецификация описывает это именно таким образом. Методы этих объектов также находятся в прототипах, доступных как String.prototype
, Number.prototype
и Boolean.prototype
.
null
и undefined
не имеют объектов-обёртокСпециальные значения null
и undefined
стоят особняком. У них нет объектов-обёрток, так что методы и свойства им недоступны. Также у них нет соответствующих прототипов.
Изменение встроенных прототипов
Встроенные прототипы можно изменять. Например, если добавить метод к String.prototype
, метод становится доступен для всех строк:
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
В течение процесса разработки у нас могут возникнуть идеи о новых встроенных методах, которые нам хотелось бы иметь, и искушение добавить их во встроенные прототипы. Это плохая идея.
Прототипы глобальны, поэтому очень легко могут возникнуть конфликты. Если две библиотеки добавляют метод String.prototype.show
, то одна из них перепишет метод другой.
Так что, в общем, изменение встроенных прототипов считается плохой идеей.
В современном программировании есть только один случай, в котором одобряется изменение встроенных прототипов. Это создание полифилов.
Полифил – это термин, который означает эмуляцию метода, который существует в спецификации JavaScript, но ещё не поддерживается текущим движком JavaScript.
Тогда мы можем реализовать его сами и добавить во встроенный прототип.
Например:
if (!String.prototype.repeat) { // Если такого метода нет
// добавляем его в прототип
String.prototype.repeat = function(n) {
// повторить строку n раз
// на самом деле код должен быть немного более сложным
// (полный алгоритм можно найти в спецификации)
// но даже неполный полифил зачастую достаточно хорош для использования
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
Заимствование у прототипов
В главе Декораторы и переадресация вызова, call/apply мы говорили о заимствовании методов.
Это когда мы берём метод из одного объекта и копируем его в другой.
Некоторые методы встроенных прототипов часто одалживают.
Например, если мы создаём объект, похожий на массив (псевдомассив), мы можем скопировать некоторые методы из Array
в этот объект.
Пример:
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
Это работает, потому что для внутреннего алгоритма встроенного метода join
важны только корректность индексов и свойство length
, он не проверяет, является ли объект на самом деле массивом. И многие встроенные методы работают так же.
Альтернативная возможность – мы можем унаследовать от массива, установив obj.__proto__
как Array.prototype
, таким образом все методы Array
станут автоматически доступны в obj
.
Но это будет невозможно, если obj
уже наследует от другого объекта. Помните, мы можем наследовать только от одного объекта одновременно.
Заимствование методов – гибкий способ, позволяющий смешивать функциональность разных объектов по необходимости.
Итого
- Все встроенные объекты следуют одному шаблону:
- Методы хранятся в прототипах (
Array.prototype
,Object.prototype
,Date.prototype
и т.д.). - Сами объекты хранят только данные (элементы массивов, свойства объектов, даты).
- Методы хранятся в прототипах (
- Примитивы также хранят свои методы в прототипах объектов-обёрток:
Number.prototype
,String.prototype
,Boolean.prototype
. Только у значенийundefined
иnull
нет объектов-обёрток. - Встроенные прототипы могут быть изменены или дополнены новыми методами. Но не рекомендуется менять их. Единственная допустимая причина – это добавление нового метода из стандарта, который ещё не поддерживается движком JavaScript.