Javascript объекты и наследование

Наследование и цепочка прототипов

Модель наследования в JavaScript может озадачить опытных разработчиков на высокоуровневых объектно-ориентированных языках (таких, например, как Java или C++), поскольку она динамическая и не включает в себя реализацию понятия class (хотя ключевое слово class , бывшее долгие годы зарезервированным, и приобрело практическое значение в стандарте ES2015, однако, классы в JavaScript представляют собой лишь «синтаксический сахар» поверх прототипно-ориентированной модели наследования).

В плане наследования JavaScript работает лишь с одной сущностью: объектами. Каждый объект имеет внутреннюю ссылку на другой объект, называемый его прототипом. У объекта-прототипа также есть свой собственный прототип и так далее до тех пор, пока цепочка не завершится объектом, у которого свойство prototype равно null . По определению, null не имеет прототипа и является завершающим звеном в цепочке прототипов.

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

Наследование с цепочкой прототипов

Наследование свойств

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

// В этом примере someObject.[[Prototype]] означает прототип someObject. // Это упрощённая нотация (описанная в стандарте ECMAScript). // Она не может быть использована в реальных скриптах. // Допустим, у нас есть объект 'o' с собственными свойствами a и b // // o.[[Prototype]] имеет свойства b и с // // Далее, o.[[Prototype]].[[Prototype]] является null // null - это окончание в цепочке прототипов // по определению, null не имеет свойства [[Prototype]] // В итоге полная цепочка прототипов выглядит так: // ---> ---> null console.log(o.a); // 1 // Есть ли у объекта 'o' собственное свойство 'a'? // Да, и его значение равно 1 console.log(o.b); // 2 // Есть ли у объекта 'o' собственное свойство 'b'? // Да, и его значение равно 2. // У прототипа o.[[Prototype]] также есть свойство '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); 

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

Длительное время поиска свойств, располагающихся относительно высоко в цепочке прототипов, может негативно сказаться на производительности (performance), особенно в критических в этом смысле местах кода. Кроме того, попытка найти несуществующие свойства неизбежно приведёт к проверке на их наличие у всех объектов цепочки прототипов.

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

Если вам необходимо проверить, определено ли свойство у самого объекта, а не где-то в его цепочке прототипов, вы можете использовать метод hasOwnProperty , который все объекты наследуют от Object.prototype .

hasOwnProperty — единственная существующая в JavaScript возможность работать со свойствами, не затрагивая цепочку прототипов.

Примечание: Примечание: Для проверки существования свойства недостаточно проверять, эквивалентно ли оно undefined . Свойство может вполне себе существовать, но при этом ему может быть присвоено значение undefined .

Плохая практика: расширение базовых прототипов

Одной из частых ошибок является расширение Object.prototype или других базовых прототипов.

Такой подход называется monkey patching и нарушает принцип инкапсуляции. Несмотря на то, что ранее он использовался в таких широко распространённых фреймворках, как например, Prototype.js, в настоящее время не существует разумных причин для его использования, поскольку в данном случае встроенные типы «захламляются» дополнительной нестандартной функциональностью.

Единственным оправданием расширения базовых прототипов могут являться лишь полифилы — эмуляторы новой функциональности (например, Array.forEach) для не поддерживающих её реализаций языка в старых веб-браузерах.

Примеры

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() // переопределение 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.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]] работает рекурсивно, то есть при вызове:

JavaScript на самом деле выполняет что-то подобное:

Источник

Читайте также:  What is float number in java
Оцените статью