Что такое прототипное наследование в javascript
Перейти к содержимому

Что такое прототипное наследование в javascript

  • автор:

Прототипное наследование в JS

Поскольку вопрос достаточно тривиальный, решил сюда набросать небольшую статью.

Так вот: для того, чтобы понять, почему такой вопрос вообще возникает и чем наследование в JS отличается от классического наследования в ООП, давайте это самое классическое наследование рассмотрим.

Классическое наследование работает примерно так: есть родительский класс, когда мы наследуем от него другой класс, в него копируются атрибуты и методы родительского класса. В общем-то, это все. Вот так вот просто.

Почему так нельзя в JavaScript? И тут тоже все просто — в JS классов нет. Есть объекты. Конечно, есть ключевое слово class — правда, оно, во-первых, появилось сравнительно недавно, в 2015 году, вместе с ES6, а, во-вторых, не создает на самом деле никаких классов — оно все еще создает те же объекты.

Так как реализовать что-то похожее на классическое ООП-наследование, не имея классов? В JavaScript у объектов есть скрытое свойство [[Prototype]] . Свойство это может быть либо равным null , либо содержать в себе ссылку на другой объект.

Собственно, так мы и реализуем наследование: если мы пытаемся прочитать свойство или использовать метод объекта, а его в этом объекте нет, JS смотрит в его прототип, если находит там ссылку на объект, то ищет это свойство или метод уже в этом объекте. Если и там его нет, JS смотрит в прототип уже этого объекта — если и там нужного метода / свойства, JS снова идет в прототип — и так либо пока не найдет нужный метод / свойство, либо не встретит в прототипе null.

В таком случае JS либо вернет undefined, если мы искали свойство, либо, если мы искали функцию, упадет с TypeError: obj.nonExistingMethod is not a function .

Это и называется прототипным наследованием в JavaScript.

Кстати, из этого всего можно сделать два интересных вывода: во-первых, нужно стараться не делать цепочки связанных через прототипы объекты слишком длинными, чтобы не влиять на производительность негативно. Во-вторых, наследование в JavaScript как минимум не слабее классического — к примеру, мы можем отнаследовать класс от другого уже после его создания.

Теперь немного о практике. К прототипу объекта мы можем обращаться либо через геттер / сеттер __proto__ ( obj.__proto__ = anotherObj ), либо (более современный способ) использовать методы Object.getPrototypeOf(obj) и Object.setPrototypeOf(obj, prototype) . Еще мы можем делать class ClassSecond extends ClassFirst — по сути, раньше мы бы сделали то же самое так: ClassSecond.__proto__ = ClassFirst или Object.setPrototypeOf(ClassSecond, ClassFirst) .

Здесь стоит упомянуть цикл for..in — он перебирает не только свойства самого объекта, но и унаследованные. То есть, следующий код выведет свойства как объекта obj2, так и объекта obj1, от которого он унаследован:

const obj1 =  test1: 'test1', test2: 'test2' >; const obj2 =  test3: 'test3', test4: 'test4' >; Object.setPrototypeOf(obj2, obj1); // наследуем obj2 от obj1 for (const prop in obj2)  console.log(`$prop>: $obj2[prop]>`); > // выведет: test3: test3 test4: test4 test1: test1 test2: test2 

Как небольшой итог: прототипное наследование ничем не хуже, а иногда и лучше классического. Да и понять его не так и сложно.

Интересный пост?

Вот еще похожие:

  • Событийно-ориентированная архитектура и Node.js Events
  • Реактивное программирование: теория и практика
  • Как и зачем писать тесты?
  • Функциональное программирование. Что это и зачем?
  • Профилирование Node.js-приложений

Форма для записи на менторинг — тут. Ну, и по любым вопросам можете написать в телегу, там я отвечаю быстрее всего.

Что такое прототипное наследование в javascript

В прошлой теме было рассмотрено наследование объектов или точнее их прототипов. Использование функций-конструкторов делает шаг вперед в этом плане, позволяя наследовать прототипы в псевдоклассовом стиле, как наследование типов.

Например, у нас может быть объект Person, который представляет отдельного пользователя. И также может быть объект Employee, который представляет работника. Но работник также может являться пользователем и поэтому должен иметь все его свойства и методы. Например:

// конструктор пользователя function Person (name, age) < this.name = name; this.age = age; this.sayHello = function()< console.log(`Person $says "Hello"`); >; > // добавляем прототип в функцию Person.prototype.print = function() < console.log(`Name: $Age: $`); >; // конструктор работника function Employee(name, age, comp) < Person.call(this, name, age); // применяем конструктор Person this.company = comp; this.work = function()< console.log(`$works in $`); >; > // наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); // устанавливаем конструктор Employee.prototype.constructor = Employee;

Здесь в начале определяет функция-конструктор Person, который представляет пользователя. В Person определены два свойства и два метода. Для примера один мето — sayHello определен внутри конструктора, а второй метод — print определен непосредственно в прототипе.

Затем определяется функция-конструктор Employee, который представляет работника.

В конструкторе Employee происходит обращение к конструктору Person с помощью вызова:

Person.call(this, name, age);

Передача первого параметра позволяет вызвать функцию конструктора Person для объекта, создаваемого конструктором Employee. Благодаря этому все свойства и методы, определенные в конструкторе Person, также переходят на объект Employee. Дополнительно определяется свойство company, которое представляет компанию работника, и метод work.

Кроме того, необходимо унаследовать также и прототип Person и соответственно все определенные через прототип функции (например, в примере выше это функция Person.prototype.print ). Для этого служит вызов:

Employee.prototype = Object.create(Person.prototype);

Метод Object.create() позволяет создать объект прототипа Person, который затем присваивается прототипу Employee.

Нередко вместо вызова метода Object.create() для установки прототипа используется вызов наследуемого конструктора, например:

Employee.prototype = new Person();

В результате будет создан объект, у которого прототип ( Employee.prototype.__proto__ ) будет указывать на прототип Person

Однако стоит учитывать, что созданный объект прототипа будет указывать на конструктор Person. Поэтому также устанавливаем нужный конструктор:

Employee.prototype.constructor = Employee;

Конструктор редко используется сам по себе, и, возможно, осутствие установки конструктора никак не скажется на работе программы. Но тем не менее рассмотрим следующую ситуацию

const obj = new Employee.prototype.constructor("Bob", 23, "Google"); console.log(obj); // Employee или Person в зависимости от типа конструктора obj.work(); // Если obj - Person, то будет ошибка

Здесь напрямую вызываем конструктор для создания объекта obj. И тип объекта obj здесь будет зависеть от того, какой конструктор установлен для Employee.prototype.constructor

Протестируем выше определенные функции-конструкторы:

// конструктор пользователя function Person (name, age) < this.name = name; this.age = age; this.sayHello = function()< console.log(`Person $says "Hello"`); >; > Person.prototype.print = function() < console.log(`Name: $Age: $`); >; // конструктор работника function Employee(name, age, comp) < Person.call(this, name, age); // применяем конструктор Person this.company = comp; this.work = function()< console.log(`$works in $`); >; > // наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); // устанавливаем конструктор Employee.prototype.constructor = Employee; // создаем объект Employee const tom = new Employee("Tom", 39, "Google"); // обращение к унаследованному свойству console.log("Age:", tom.age); // обращение к унаследованному методу tom.sayHello(); // Person Tom says "Hello" // обращение к унаследованному методу прототипа tom.print(); // Name: Tom Age: 39 // обращение к собственному методу tom.work(); // Tom works in Google

Переопределение функций

При наследовании мы можем переопределять наследуемый функционал. Например, в примере выше для Person определено два метода: sayHello (в конструкторе) и print() (в прототипе). Но, допустим, для Employee мы хотим изменить их логику, например, в методе print также выводить компанию работника. В этом случае мы можем определить для Employee методы с теми же именами:

function Person (name, age) < this.name = name; this.age = age; this.sayHello = function()< console.log(`Person $says "Hello"`); >; > Person.prototype.print = function() < console.log(`Name: $Age: $`); >; function Employee(name, age, comp) < Person.call(this, name, age); this.company = comp; // переопределяем метод sayHello this.sayHello = function()< console.log(`Employee $says "Hello"`); >; > Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // переопределяем метод print Employee.prototype.print = function() < console.log(`Name: $Age: $ Company: $`); >; const tom = new Employee("Tom", 39, "Google"); tom.sayHello(); // Employee Tom says "Hello" tom.print(); // Name: Tom Age: 39 Company: Google

Метод sayHello() определен внутри конструктора Person, поэтому данный метод переопределяется внутри конструктора Employee. Метод print() определен как метод прототипа Person, поэтому его можно переопределить в прототипе Employee.

Вызов метода родительского прототипа

В прототипе-наследнике может потребоваться вызвать метод из родительского прототипа. Например, это может быть необходимо для сокращении логики кода/, если логика метода наследника повторяет логику метода родителя. В этом случае для обращения к методам родительского прототипа применяется функция call()() :

function Person (name, age) < this.name = name; this.age = age; >Person.prototype.print = function() < console.log(`Name: $Age: $`); >; function Employee(name, age, comp) < Person.call(this, name, age); this.company = comp; >Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // переопределяем метод print Employee.prototype.print = function()< Person.prototype.print.call(this); // вызываем метод print из Person console.log(`Company: $`); >; const tom = new Employee("Tom", 39, "Google"); tom.print(); // Name: Tom Age: 39 // Company: Google

В данном случае при переопределении метода print в прототипе Employee вызывается метод print из прототипа Person:

Employee.prototype.print = function()< Person.prototype.print.call(this); // вызываем метод print из Person console.log(`Company: $`); >;

Проблемы прототипного наследования

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

const tom = new Employee("Tom", 39, "Google"); Person.prototype.sleep = function() sleeps`);> tom.sleep();

Здесь в прототип Person добавляется метод sleep. Причем она добавляется уже после создания объекта tom, который представляет тип Employee. Тем не менее даже у этого объекта мы можем вызвать метод sleep.

Другой момент, который стоит учитывать, через прототип конструктора-наследника можно изменить прототип конструктора-родителя. Например:

function Person (name, age) < this.name = name; this.age = age; this.sayHello = function()< console.log(`Person $says "Hello"`); >; > Person.prototype.print = function() < console.log(`Name: $Age: $`); >; function Employee(name, age, comp) < Person.call(this, name, age); this.company = comp; >// наследуем прототип от Person Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; // меняем метод print в базовом прототипе Person Employee.prototype.__proto__.print = function()< console.log("Person prototype hacked");>; // создаем объект Person const bob = new Person("Bob", 43); bob.print(); // Person prototype hacked

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

Модель наследования в 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) для не поддерживающих её реализаций языка в старых веб-браузерах.

Примеры

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 ()  // переопределение 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 на самом деле выполняет что-то подобное:

Прототипное наследование

В программировании мы часто хотим взять что-то и расширить.

Например, у нас есть объект user со своими свойствами и методами, и мы хотим создать объекты admin и guest как его слегка изменённые варианты. Мы хотели бы повторно использовать то, что есть у объекта user , не копировать/переопределять его методы, а просто создать новый объект на его основе.

Прототипное наследование — это возможность языка, которая помогает в этом.

[[Prototype]]

В JavaScript объекты имеют специальное скрытое свойство [[Prototype]] (так оно названо в спецификации), которое либо равно null , либо ссылается на другой объект. Этот объект называется «прототип»:

Прототип даёт нам немного «магии». Когда мы хотим прочитать свойство из object , а оно отсутствует, JavaScript автоматически берёт его из прототипа. В программировании такой механизм называется «прототипным наследованием». Многие интересные возможности языка и техники программирования основываются на нём.

Свойство [[Prototype]] является внутренним и скрытым, но есть много способов задать его.

Одним из них является использование __proto__ , например так:

let animal = < eats: true >; let rabbit = < jumps: true >; rabbit.__proto__ = animal;

Если мы ищем свойство в rabbit , а оно отсутствует, JavaScript автоматически берёт его из animal .

let animal = < eats: true >; let rabbit = < jumps: true >; rabbit.__proto__ = animal; // (*) // теперь мы можем найти оба свойства в rabbit: alert( rabbit.eats ); // true (**) alert( rabbit.jumps ); // true

Здесь строка (*) устанавливает animal как прототип для rabbit .

Затем, когда alert пытается прочитать свойство rabbit.eats (**) , его нет в rabbit , поэтому JavaScript следует по ссылке [[Prototype]] и находит его в animal (смотрите снизу вверх):

Здесь мы можем сказать, что » animal является прототипом rabbit » или » rabbit прототипно наследует от animal «.

Так что если у animal много полезных свойств и методов, то они автоматически становятся доступными у rabbit . Такие свойства называются «унаследованными».

Если у нас есть метод в animal , он может быть вызван на rabbit :

let animal = < eats: true, walk() < alert("Animal walk"); >>; let rabbit = < jumps: true, __proto__: animal >; // walk взят из прототипа rabbit.walk(); // Animal walk

Метод автоматически берётся из прототипа:

Цепочка прототипов может быть длиннее:

let animal = < eats: true, walk() < alert("Animal walk"); >>; let rabbit = < jumps: true, __proto__: animal >; let longEar = < earLength: 10, __proto__: rabbit >; // walk взят из цепочки прототипов longEar.walk(); // Animal walk alert(longEar.jumps); // true (из rabbit)

Теперь, если мы прочтём что-нибудь из longEar , и оно будет отсутствовать, JavaScript будет искать его в rabbit , а затем в animal .

Есть только два ограничения:

  1. Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__ по кругу.
  2. Значение __proto__ может быть объектом или null . Другие типы игнорируются.

Это вполне очевидно, но всё же: может быть только один [[Prototype]] . Объект не может наследоваться от двух других объектов.

Свойство __proto__ — исторически обусловленный геттер/сеттер для [[Prototype]]

Это распространённая ошибка начинающих разработчиков – не знать разницы между этими двумя понятиями.

Обратите внимание, что __proto__ — не то же самое, что внутреннее свойство [[Prototype]] . Это геттер/сеттер для [[Prototype]] . Позже мы увидим ситуации, когда это имеет значение, а пока давайте просто будем иметь это в виду, поскольку мы строим наше понимание языка JavaScript.

Свойство __proto__ немного устарело, оно существует по историческим причинам. Современный JavaScript предполагает, что мы должны использовать функции Object.getPrototypeOf/Object.setPrototypeOf вместо того, чтобы получать/устанавливать прототип. Мы также рассмотрим эти функции позже.

По спецификации __proto__ должен поддерживаться только браузерами, но по факту все среды, включая серверную, поддерживают его. Так что мы вполне безопасно его используем.

Далее мы будем в примерах использовать __proto__ , так как это самый короткий и интуитивно понятный способ установки и чтения прототипа.

Операция записи не использует прототип

Прототип используется только для чтения свойств.

Операции записи/удаления работают напрямую с объектом.

В приведённом ниже примере мы присваиваем rabbit собственный метод walk :

let animal = < eats: true, walk() < /* этот метод не будет использоваться в rabbit */ >>; let rabbit = < __proto__: animal >; rabbit.walk = function() < alert("Rabbit! Bounce-bounce!"); >; rabbit.walk(); // Rabbit! Bounce-bounce!

Теперь вызов rabbit.walk() находит метод непосредственно в объекте и выполняет его, не используя прототип:

Свойства-аксессоры – исключение, так как запись в него обрабатывается функцией-сеттером. То есть это фактически вызов функции.

По этой причине admin.fullName работает корректно в приведённом ниже коде:

let user = < name: "John", surname: "Smith", set fullName(value) < [this.name, this.surname] = value.split(" "); >, get fullName() < return `$$`; > >; let admin = < __proto__: user, isAdmin: true >; alert(admin.fullName); // John Smith (*) // срабатывает сеттер! admin.fullName = "Alice Cooper"; // (**) alert(admin.name); // Alice alert(admin.surname); // Cooper

Здесь в строке (*) свойство admin.fullName имеет геттер в прототипе user , поэтому вызывается он. В строке (**) свойство также имеет сеттер в прототипе, который и будет вызван.

Значение «this»

В приведённом выше примере может возникнуть интересный вопрос: каково значение this внутри set fullName(value) ? Куда записаны свойства this.name и this.surname : в user или в admin ?

Ответ прост: прототипы никак не влияют на this .

Неважно, где находится метод: в объекте или его прототипе. При вызове метода this — всегда объект перед точкой.

Таким образом, вызов сеттера admin.fullName= в качестве this использует admin , а не user .

Это на самом деле очень важная деталь, потому что у нас может быть большой объект со множеством методов, от которого можно наследовать. Затем наследующие объекты могут вызывать его методы, но они будут изменять своё состояние, а не состояние объекта-родителя.

Например, здесь animal представляет собой «хранилище методов», и rabbit использует его.

Вызов rabbit.sleep() устанавливает this.isSleeping для объекта rabbit :

// методы animal let animal = < walk() < if (!this.isSleeping) < alert(`I walk`); >>, sleep() < this.isSleeping = true; >>; let rabbit = < name: "White Rabbit", __proto__: animal >; // модифицирует rabbit.isSleeping rabbit.sleep(); alert(rabbit.isSleeping); // true alert(animal.isSleeping); // undefined (нет такого свойства в прототипе)

Картинка с результатом:

Если бы у нас были другие объекты, такие как bird , snake и т.д., унаследованные от animal , они также получили бы доступ к методам animal . Но this при вызове каждого метода будет соответствовать объекту (перед точкой), на котором происходит вызов, а не animal . Поэтому, когда мы записываем данные в this , они сохраняются в этих объектах.

В результате методы являются общими, а состояние объекта — нет.

Цикл for…in

Цикл for..in проходит не только по собственным, но и по унаследованным свойствам объекта.

let animal = < eats: true >; let rabbit = < jumps: true, __proto__: animal >; // Object.keys возвращает только собственные ключи alert(Object.keys(rabbit)); // jumps // for..in проходит и по своим, и по унаследованным ключам for(let prop in rabbit) alert(prop); // jumps, затем eats

Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода obj.hasOwnProperty(key): он возвращает true , если у obj есть собственное, не унаследованное, свойство с именем key .

Пример такой фильтрации:

let animal = < eats: true >; let rabbit = < jumps: true, __proto__: animal >; for(let prop in rabbit) < let isOwn = rabbit.hasOwnProperty(prop); if (isOwn) < alert(`Our: $`); // Our: jumps > else < alert(`Inherited: $`); // Inherited: eats > >

В этом примере цепочка наследования выглядит так: rabbit наследует от animal , который наследует от Object.prototype (так как animal – литеральный объект <. >, то это по умолчанию), а затем null на самом верху:

Заметим ещё одну деталь. Откуда взялся метод rabbit.hasOwnProperty ? Мы его явно не определяли. Если посмотреть на цепочку прототипов, то видно, что он берётся из Object.prototype.hasOwnProperty . То есть он унаследован.

…Но почему hasOwnProperty не появляется в цикле for..in в отличие от eats и jumps ? Он ведь перечисляет все унаследованные свойства.

Ответ простой: оно не перечислимо. То есть у него внутренний флаг enumerable стоит false , как и у других свойств Object.prototype . Поэтому оно и не появляется в цикле.

Почти все остальные методы получения ключей/значений игнорируют унаследованные свойства

Почти все остальные методы, получающие ключи/значения, такие как Object.keys , Object.values и другие – игнорируют унаследованные свойства.

Они учитывают только свойства самого объекта, не его прототипа.

Итого

  • В JavaScript все объекты имеют скрытое свойство [[Prototype]] , которое является либо другим объектом, либо null .
  • Мы можем использовать obj.__proto__ для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены).
  • Объект, на который ссылается [[Prototype]] , называется «прототипом».
  • Если мы хотим прочитать свойство obj или вызвать метод, которого не существует у obj , тогда JavaScript попытается найти его в прототипе.
  • Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное свойство, а не сеттер).
  • Если мы вызываем obj.method() , а метод при этом взят из прототипа, то this всё равно ссылается на obj . Таким образом, методы всегда работают с текущим объектом, даже если они наследуются.
  • Цикл for..in перебирает как свои, так и унаследованные свойства. Остальные методы получения ключей/значений работают только с собственными свойствами объекта.

Задачи

Работа с прототипами

важность: 5

В приведённом ниже коде создаются и изменяются два объекта.

Какие значения показываются в процессе выполнения кода?

let animal = < jumps: null >; let rabbit = < __proto__: animal, jumps: true >; alert( rabbit.jumps ); // ? (1) delete rabbit.jumps; alert( rabbit.jumps ); // ? (2) delete animal.jumps; alert( rabbit.jumps ); // ? (3)

Должно быть 3 ответа.

  1. true , берётся из rabbit .
  2. null , берётся из animal .
  3. undefined , такого свойства больше нет.

Алгоритм поиска

важность: 5

Задача состоит из двух частей.

У нас есть объекты:

let head = < glasses: 1 >; let table = < pen: 3 >; let bed = < sheet: 1, pillow: 2 >; let pockets = < money: 2000 >;
  1. С помощью свойства __proto__ задайте прототипы так, чтобы поиск любого свойства выполнялся по следующему пути: pockets → bed → table → head . Например, pockets.pen должно возвращать значение 3 (найденное в table ), а bed.glasses – значение 1 (найденное в head ).
  2. Ответьте на вопрос: как быстрее получить значение glasses – через pockets.glasses или через head.glasses ? При необходимости составьте цепочки поиска и сравните их.
  1. Добавим свойство __proto__ :
let head = < glasses: 1 >; let table = < pen: 3, __proto__: head >; let bed = < sheet: 1, pillow: 2, __proto__: table >; let pockets = < money: 2000, __proto__: bed >; alert( pockets.pen ); // 3 alert( bed.glasses ); // 1 alert( table.money ); // undefined

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *