Эта статья нуждается в редакционном обзоре. Как вы можете помочь.
Модель наследования в JavaScript может озадачить опытных разработчиков на высокоуровневых объектно-ориентированных языках (таких, например, как Java или C++), так как она динамическая и не включает в себя реализацию понятия class
(хотя ключевое слово class
и является зарезервированным, т.е., не может быть использовано в качестве имени переменной).
В плане наследования JavaScript работает лишь с одной сущностью: объектами. Каждый объект имеет внутреннюю ссылку на другой объект, называемый его прототипом. У объекта-прототипа также есть свой собственный прототип и так далее до тех пор, пока цепочка не завершится объектом, у которого свойство prototype равно null
. null
, по определению, не имеет прототипа и служит в качестве завершающего звена в цепочке прототипов.
Хотя прототипную модель наследования некоторые относят к недостаткам JavaScript, на самом деле она мощнее классической. К примеру, поверх неё можно предельно просто реализовать классическое наследование, а вот попытки совершить обратное непременно вынудят Вас попотеть.
Наследование с цепочкой прототипов
Наследование свойств
Объекты в JavaScript - это как бы динамические "контейнеры", наполненные свойствами (называемыми собственными свойствами) и у каждого объекта есть при этом ссылка на свой объект-прототип. При попытке получить доступ к какому-либо свойству объекта происходит следующее:
// Допустим, у нас есть объект 'o' с цепочкой прототипов выглядящей как: // {a:1, b:2} ---> {b:3, c:4} ---> null // где 'a' и 'b' - собственные свойства объекта 'o'. // В этом примере someObject.[[Prototype]] означает прототип someObject. // Это упрощённая нотация (описанная в стандарте ECMAScript). Она не может быть использована в скриптах. console.log(o.a); // 1 // Есть ли у объекта 'o' собственное свойство 'a'? Да, и его значение равно 1 console.log(o.b); // 2 // Есть ли у объекта 'o' собственное свойство 'b'? Да, и его значение равно 2 // У прототипа тоже есть свойство 'b', но обращения к нему в данном случае не происходит. Это и называется "property shadowing" console.log(o.c); // 4 // Есть ли у объекта 'o' собственное свойство 'с'? Нет, тогда поищем его в прототипе. // Есть ли у объекта o.[[Prototype]] собственное свойство 'с'? Да, оно равно 4 console.log(o.d); // undefined // Есть ли у объекта 'o' собственное свойство 'd'? Нет, тогда поищем его в прототипе. // Есть ли у объекта o.[[Prototype]] собственное свойство 'd'? Нет, продолжаем поиск по цепочке прототипов. // o.[[Prototype]].[[Prototype]] равно null, прекращаем поиск, свойство не найдено, возвращаем undefined
При добавлении к объекту нового свойства создаётся новое собственное свойство (own property). Единственным исключением из этого правила являются наследуемые свойства, имеющие getter или setter.
Наследование "методов"
JavaScript не имеет "методов" в смысле, принятом в классической модели ООП. В JavaScript любая функция может быть добавлена к объекту в виде его свойства. Унаследованная функция ведёт себя точно так же, как любое другое свойство объекта, в том числе и в плане "затенения свойств" (property shadowing), как показано в примере выше (в данном конкретном случае это форма переопределения метода - method overriding).
В области видимости унаследованной функции ссылка this
указывает на наследуемый объект, а не на прототип, в котором данная функция является собственным свойством.
var o = { a: 2, m: function(){ return this.a + 1; } }; console.log(o.m()); // 3 // в этом случае при вызове 'o.m' this указывает на 'o' var p = Object.create(o); // 'p' - наследник 'o' p.a = 12; // создаст собственное свойство 'a' объекта 'p' console.log(p.m()); // 13 // при вызове 'p.m' this указывает на 'p'. // т.е. когда 'p' наследует функцию 'm' объекта 'o', this.a означает 'p.a', собственное свойство 'a' объекта 'p'
Различные способы создания объектов и получаемые в итоге цепочки прототипов
Создание объектов с помощью литералов
var o = {a: 1}; // Созданный объект 'o' имеет Object.prototype в качестве своего [[Prototype]] // у 'o' нет собственного свойства 'hasOwnProperty' // hasOwnProperty - это собственное свойство Object.prototype. Таким образом 'o' наследует hasOwnProperty от Object.prototype // Object.prototype в качестве прототипа имеет null. // o ---> Object.prototype ---> null var a = ["yo", "whadup", "?"]; // Массивы наследуются от Array.prototype (у которого есть такие методы, как indexOf, forEach и т.п.). // Цепочка прототипов при этом выглядит так: // a ---> Array.prototype ---> Object.prototype ---> null function f(){ return 2; } // Функции наследуются от Function.prototype (у которого есть такие методы, как call, bind и т.п.): // f ---> Function.prototype ---> Object.prototype ---> null
Создание объектов с помощью конструктора
В JavaScript "конструктор" - это "просто" функция, вызываемая с оператором new.
function Graph() { this.vertexes = []; this.edges = []; } Graph.prototype = { addVertex: function(v){ this.vertexes.push(v); } } var g = new Graph(); // объект 'g' имеет собственные свойства 'vertexes' и 'edges'. // g.[[Prototype]] принимает значение Graph.prototype при выполнении new Graph().
Object.create
В ECMAScript 5 представлен новый метод создания объектов: Object.create. Прототип создаваемого объекта указывается в первом аргументе этого метода:
var a = {a: 1}; // a ---> Object.prototype ---> null var b = Object.create(a); // b ---> a ---> Object.prototype ---> null console.log(b.a); // 1 (унаследовано) var c = Object.create(b); // c ---> b ---> a ---> Object.prototype ---> null var d = Object.create(null); // d ---> null console.log(d.hasOwnProperty); // undefined, т.к. 'd' не наследуется от Object.prototype
Используя ключевое слово class
С выходом ECMAScript 6 появился целый набор ключевых слов, реализующих классы. Они могут показаться знакомыми людям, изучавшим языки, основанные на классах, но есть существенные отличия. JavaScript был и остаётся прототипно-ориентированным языком. Новые ключевые слова: "class
", "constructor
", "static
", "extends
" и "super
".
"use strict"; class Polygon { constructor(height, width) { this.height = height; this.width = width; } } class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(newLength) { this.height = newLength; this.width = newLength; } } var square = new Square(2);
Эффективность
Длительное время поиска свойств, располагающихся относительно высоко в цепочке прототипов, может негативно сказаться на эффективности, особенно в критических в этом смысле местах кода. Кроме того, попытка найти несуществующие свойства неизбежно приведет к проверке на их наличие у всех объектов цепочки прототипов.
Кроме того, при циклическом переборе свойств объекта будет обработано каждое свойство, присутствующее в цепочке прототипов.
Если вам необходимо проверить, определено ли свойство у самого объекта, а не где-то в его цепочке прототипов, вы можете использовать метод hasOwnProperty
, который все объекты наследуют от Object.prototype
.
hasOwnProperty
— единственная существующая в JavaScript возможность работать со свойствами, не затрагивая цепочку прототипов.
undefined
. Свойство может вполне себе существовать, но при этом ему может быть присвоено значение undefined
.Нехорошая практика: Расширение базовых прототипов
Одной из частых ошибок является расширение Object.prototype
или других базовых прототипов.
Эта технология, называемая monkey patching, нарушает принцип инкапсуляции. Несмотря на то, что она используется в широко распространенных фреймворках, таких как Prototype.js, на настоящий момент не существует разумных причин для ее использования, так как в данном случае встроенные типы "захламляются" дополнительной нестандартной функциональностью.
Единственным оправданием расширения базовых прототипов является лишь эмуляция новых возможностей, таких как Array.forEach
, для неподдерживающих их старых версий языка.
Примеры
B
наследует от A
:
function A(a){ this.varA = a; } // What is the purpose of including varA in the prototype when A.prototype.varA will always be shadowed by // this.varA, given the definition of function A above? A.prototype = { varA : null, // Shouldn't we strike varA from the prototype as doing nothing? // perhaps intended as an optimization to allocate space in hidden classes? // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables // would be valid if varA wasn't being initialized uniquely for each instance doSomething : function(){ // ... } } function B(a, b){ A.call(this, a); this.varB = b; } B.prototype = Object.create(A.prototype, { varB : { value: null, enumerable: true, configurable: true, writable: true }, doSomething : { value: function(){ // override A.prototype.doSomething.apply(this, arguments); // call super // ... }, enumerable: true, configurable: true, writable: true } }); B.prototype.constructor = B; var b = new B(); b.doSomething();
Важно:
- Типы определяются в
.prototype
- Для наследования используется
Object.create()
prototype и Object.getPrototypeOf
Как уже упоминали, JavaScript может запутать разработчиков на Java или C++, ведь в нём совершенно нет классов. Всё что мы имеем лишь объекты. Даже те "classes", которые мы имитировали в статье, тоже являются функциональными объектами.
Вы наверняка заметили, что у function A
есть особое свойство prototype
. Это свойство работает с оператором new
. Ссылка на объект-прототип копируется во внутреннее свойство [[Prototype]]
нового объекта. Например, в этом случае var a1 = new A()
, JavaScript (после создания объекта в памяти и до выполнения функции function A()
) устанавливает a1.[[Prototype]] = A.prototype
. Потом, при попытке доступа к свойству нового экземпляра объекта, JavaScript проверяет, принадлежит ли свойство непосредственно объекту. Если нет, то интерпретатор ищет в свойстве [[Prototype]]
. Всё, что было определено в prototype
в равной степени доступно и всем экземплярам данного объекта. При внесении изменений в prototype
все эти изменения сразу же становятся доступными и всем экземплярам объекта.
[[Prototype]]
работает рекурсивно, то есть при вызове:
var o = new Foo();
JavaScript на самом деле выполняет что-то подобное:
var o = new Object(); o.[[Prototype]] = Foo.prototype; Foo.call(o);
а когда вы делаете так:
o.someProp;
JavaScript проверяет, есть ли у o
свойство someProp
. Если нет, проверяется Object.getPrototypeOf(o).someProp
и если и там нет, то ищет в Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp
и так далее.
Заключение
Перед тем как приступать к написанию сложного кода с использованием прототипной модели наследования, важно иметь четкое представление о том, как она работает. Также, во избежание возможных проблем с эффективностью вашего кода, не следует забывать о длине цепочек прототипов, укорачивая их при каждом удобном случае. Кроме того, не следует расширять базовые прототипы иначе как в целях обеспечения совместимости с более поздними версиями JavaScript.