Встроенные прототипы
Свойство «prototype» широко используется внутри самого языка JavaScript. Все встроенные функции-конструкторы используют его.
Сначала мы рассмотрим детали, а затем используем «prototype» для добавления встроенным объектам новой функциональности.
Object.prototype
Давайте выведем пустой объект:
let obj = <>; alert( obj ); // "[object Object]" ?
Где код, который генерирует строку «[object Object]» ? Это встроенный метод toString , но где он? obj ведь пуст!
…Но краткая нотация obj = <> – это то же самое, что и obj = new Object() , где Object – встроенная функция-конструктор для объектов с собственным свойством prototype , которое ссылается на огромный объект с методом toString и другими.
Другие встроенные объекты устроены аналогично. Даже функции – они объекты встроенного конструктора 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 стоят особняком. У них нет объектов-обёрток, так что методы и свойства им недоступны. Также у них нет соответствующих прототипов.
Изменение встроенных прототипов
Встроенные прототипы можно изменять. Например, если добавить метод к 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 и т.д.).
- Сами объекты хранят только данные (элементы массивов, свойства объектов, даты).
Задачи
Добавить функциям метод «f.defer(ms)»
Добавьте всем функциям в прототип метод defer(ms) , который вызывает функции через ms миллисекунд.
После этого должен работать такой код:
function f() < alert("Hello!"); >f.defer(1000); // выведет "Hello!" через 1 секунду
Прототип объекта
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/prototype-inheritance.
Объекты в JavaScript можно организовать в цепочки так, чтобы свойство, не найденное в одном объекте, автоматически искалось бы в другом.
Связующим звеном выступает специальное свойство __proto__ .
Прототип proto
Если один объект имеет специальную ссылку __proto__ на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте __proto__ .
Свойство __proto__ доступно во всех браузерах, кроме IE10-, а в более старых IE оно, конечно же, тоже есть, но напрямую к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее.
var animal = < eats: true >; var rabbit = < jumps: true >; rabbit.__proto__ = animal; // в rabbit можно найти оба свойства alert( rabbit.jumps ); // true alert( rabbit.eats ); // true
- Первый alert здесь работает очевидным образом – он выводит свойство jumps объекта rabbit .
- Второй alert хочет вывести rabbit.eats , ищет его в самом объекте rabbit , не находит – и продолжает поиск в объекте rabbit.__proto__ , то есть, в данном случае, в animal .
Иллюстрация происходящего при чтении rabbit.eats (поиск идёт снизу вверх):
Объект, на который указывает ссылка __proto__ , называется «прототипом». В данном случае получилось, что animal является прототипом для rabbit .
Также говорят, что объект rabbit «прототипно наследует» от animal .
Обратим внимание – прототип используется исключительно при чтении. Запись значения, например, rabbit.eats = value или удаление delete rabbit.eats – работает напрямую с объектом.
В примере ниже мы записываем свойство в сам rabbit , после чего alert перестаёт брать его у прототипа, а берёт уже из самого объекта:
var animal = < eats: true >; var rabbit = < jumps: true, eats: false >; rabbit.__proto__ = animal; alert( rabbit.eats ); // false, свойство взято из rabbit
Другими словами, прототип – это «резервное хранилище свойств и методов» объекта, автоматически используемое при поиске.
У объекта, который является __proto__ , может быть свой __proto__ , у того – свой, и так далее. При этом свойства будут искаться по цепочке.
Если вы будете читать спецификацию ECMAScript – свойство __proto__ обозначено в ней как [[Prototype]] .
Двойные квадратные скобки здесь важны, чтобы не перепутать его с совсем другим свойством, которое называется prototype , и которое мы рассмотрим позже.
Метод hasOwnProperty
Обычный цикл for..in не делает различия между свойствами объекта и его прототипа.
Он перебирает всё, например:
var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < alert( key + " = " + rabbitЧто такое prototype javascript ); // выводит и "eats" и "jumps" >
Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе.
Вызов obj.hasOwnProperty(prop) возвращает true , если свойство prop принадлежит самому объекту obj , иначе false .
var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; alert( rabbit.hasOwnProperty('jumps') ); // true: jumps принадлежит rabbit alert( rabbit.hasOwnProperty('eats') ); // false: eats не принадлежит
Для того, чтобы перебрать свойства самого объекта, достаточно профильтровать key через hasOwnProperty :
var animal = < eats: true >; var rabbit = < jumps: true, __proto__: animal >; for (var key in rabbit) < if (!rabbit.hasOwnProperty(key)) continue; // пропустить "не свои" свойства alert( key + " = " + rabbitЧто такое prototype javascript ); // выводит только "jumps" >
Object.create(null)
Зачастую объекты используют для хранения произвольных значений по ключу, как коллекцию:
var data = <>; data.text = "Привет"; data.age = 35; // .
При дальнейшем поиске в этой коллекции мы найдём не только text и age , но и встроенные функции:
var data = <>; alert(data.toString); // функция, хотя мы её туда не записывали
Это может быть неприятным сюрпризом и приводить к ошибкам, если названия свойств приходят от посетителя и могут быть произвольными.
Чтобы этого избежать, мы можем исключать свойства, не принадлежащие самому объекту:
var data = <>; // выведет toString только если оно записано в сам объект alert(data.hasOwnProperty('toString') ? data.toString : undefined);
var data = Object.create(null); data.text = "Привет"; alert(data.text); // Привет alert(data.toString); // undefined
Объект, создаваемый при помощи Object.create(null) не имеет прототипа, а значит в нём нет лишних свойств. Для коллекции – как раз то, что надо.
Методы для работы с proto
В современных браузерах есть два дополнительных метода для работы с __proto__ . Зачем они нужны, если есть __proto__ ? В общем-то, не очень нужны, но по историческим причинам тоже существуют.
Чтение: Object.getPrototypeOf(obj) Возвращает obj.__proto__ (кроме IE8-) Запись: Object.setPrototypeOf(obj, proto) Устанавливает obj.__proto__ = proto (кроме IE10-).
Кроме того, есть ещё один вспомогательный метод:
Создание объекта с прототипом: Object.create(proto, descriptors) Создаёт пустой объект с __proto__ , равным первому аргументу (кроме IE8-), второй необязательный аргумент может содержать дескрипторы свойств.
Итого
- В JavaScript есть встроенное «наследование» между объектами при помощи специального свойства __proto__ .
- При установке свойства rabbit.__proto__ = animal говорят, что объект animal будет «прототипом» rabbit .
- При чтении свойства из объекта, если его в нём нет, оно ищется в __proto__ . Прототип задействуется только при чтении свойства. Операции присвоения obj.prop = или удаления delete obj.prop совершаются всегда над самим объектом obj .
Несколько прототипов одному объекту присвоить нельзя, но можно организовать объекты в цепочку, когда один объект ссылается на другой при помощи __proto__ , тот ссылается на третий, и так далее.
В современных браузерах есть методы для работы с прототипом:
Возможно, вас смущает недостаточная поддержка __proto__ в старых IE. Но это не страшно. В последующих главах мы рассмотрим дополнительные методы работы с __proto__ , включая те, которые работают везде.
Также мы рассмотрим, как свойство __proto__ используется внутри самого языка JavaScript и как организовать классы с его помощью.