Перейти к содержанию

Великий Прототип

В JavaScript отсутствует классическая модель наследования — вместо неё используется прототипная модель.

Хотя её часто расценивают как один из недостатков JavaScript, на самом деле прототипная модель наследования намного мощнее классической. К примеру, поверх неё можно предельно легко реализовать классическое наследование, а попытки совершить обратное вынудят вас попотеть.

Из-за того, что JavaScript — практически единственный широко используемый язык с прототипным наследованием, придётся потратить некоторое время на осознание различий между этими двумя моделями.

Первое важное отличие заключается в том, что наследование в JavaScript выполняется с использованием так называемых цепочек прототипов.

Замечание: В результате выполнения конструкции Bar.prototype = Foo.prototype оба объекта будут делить друг с другом один и тот же прототип. Так что изменение прототипа одного из объектов повлечёт за собой изменение прототипа другого и наоборот — вряд ли это окажется тем, чего вы ожидали.

Замечание: Для объявления наследования вместо Bar.prototype = Object.create(Foo.prototype) можно воспользоваться конструкций Bar.prototype = new Foo(), но у нее есть пару недостатков: 1) как правило требуется унаследовать только методы и свойства прототипа, а не создавать для этого новый объект; 2) создание объекта может требовать обязательные аргументы.

Примечание: Метод Object.create отсутствует в IE8 и ниже, но его легко реализовать созданием своей такой функции или же можно подключить библиотеку для поддержки старых IE es5-shim

function Foo() {
  this.value = 42
}
Foo.prototype.method = function () {}

function Bar() {}

// Зададим наследование от Foo
Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.foo = 'Hello World'

// Убедимся, что Bar является действующим конструктором
Bar.prototype.constructor = Bar

var test = new Bar() // создадим новый экземпляр bar
// Цепочка прототипов, которая получится в результате
test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: 'Hello World', value: 42 }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* и т.д. */ }

В приведённом коде объект test наследует оба прототипа: Bar.prototype и Foo.prototype; следовательно, он имеет доступ к функции method которую мы определили в прототипе Foo. Также у него есть доступ к свойству value одного уникального экземпляра Foo, который является его прототипом. Важно заметить, что код new Bar() не создаёт новый экземпляр Foo, а повторно вызывает функцию, которая была назначена его прототипом: таким образом все новые экземпляры Bar будут иметь одинаковое свойство value.

Замечание: Никогда не используйте конструкцию Bar.prototype = Foo, поскольку ссылка будет указывать не на прототип Foo, а на объект функции Foo. Из-за этого цепочка прототипов будет проходить через Function.prototype, а не через Foo.prototype и в результате функция method не будет содержаться в цепочке прототипов.

Поиск свойств

При обращении к какому-либо свойству объекта, JavaScript проходит вверх по цепочке прототипов этого объекта, пока не найдет свойство c запрашиваемым именем.

Если он достигнет верхушки этой цепочки (Object.prototype) и при этом так и не найдёт указанное свойство, вместо него вернётся значение undefined.

Свойство prototype

То, что свойство prototype используется языком для построения цепочек прототипов, даёт нам возможность присвоить любое значение этому свойству. Однако обычные примитивы, если назначать их в качестве прототипа, будут просто-напросто игнорироваться.

function Foo() {}
Foo.prototype = 1 // ничего не произойдёт
Foo.prototype = {
  foo: 'bar',
}

При этом присвоение объектов, как в примере выше, позволит вам динамически создавать цепочки прототипов.

Производительность

Поиск свойств, располагающихся относительно высоко по цепочке прототипов, может негативно сказаться на производительности, особенно в критических местах кода. Если же мы попытаемся найти несуществующее свойство, то поиск будет осуществлён вообще по всей цепочке, со всеми вытекающими последствиями.

Вдобавок, при циклическом переборе свойств объекта, будет обработано каждое свойство, существующее в цепочке прототипов.

Расширение встроенных прототипов

Часто встречается неверное применение прототипов — расширение прототипа Object.prototype или прототипов одного из встроенных объектов JavaScript.

Подобная практика нарушает принцип инкапсуляции и имеет соответствующее название — monkey patching. К сожалению, в основу многих широко распространенных фреймворков, например Prototype, положен принцип изменения базовых прототипов. Вам же стоит запомнить — от хорошей жизни прототипы встроенных объектов не меняют.

Единственным оправданием для расширения встроенных прототипов может быть только воссоздание возможностей более новых движков JavaScript, например функции Array.forEach, которая появилась в версии 1.6.

Заключение

Перед тем, как вы приступите к разработке сложных приложений на JavaScript, вы должны полностью осознать как работают прототипы, и как организовывать наследование на их основе. Также, помните о зависимости между длиной цепочек прототипов и производительностью — разрывайте их при необходимости. Кроме того — никогда не расширяйте прототипы встроенных объектов (ну, если только для совместимости с новыми возможностями Javascript).