Для полноты статьи и единого стиля, перевод начинается с вопросов наследования, несмотря на то, что они уже были упомянуты в конце первой части. Далее рассматриваются разнообразные задачи наследования так, как их рассмотрел автор. Надо сказать, что автор широко использует новые конструкции ES5 (объяснив это в конце), которые работают не во всех браузерах и заслоняют от понимания реализацию их на низком уровне языка, на котором они изначально применялись. Для настоящего понимания наследования следует обратиться к более глубокому разбору реализаций или к реализациям методов-обёрток из ES5: Object.create, Object.defineProperty, Function.bind, get и set literals, Object.getOwnPropertyNames, Object.defineProperty, Object.getOwnPropertyDescriptor, Object.getPrototypeOf. Часть их разбирается в статье (Object.create, get и set, Object.defineProperty, bind), но не всегда в порядке появления. Таким образом, статья стремится преподнести не реализацию наследования вообще, а ту реализацию, которую успели формализовать в рабочем черновике стандарта EcmaScript 5. Это лучше, чем ничего, но несколько меньше, чем полное понимание реализаций наследования.
Зато, данная часть статьи в нескольких (4) крупных примерах кода демонстрирует чистейшее прототипное наследование, которому не требуется привлекать понятие конструктора (хотя он там, в .create(), незримо присутствует), о котором много говорят и которое исключительно редко в чистом виде встречается.
1.1 Что есть объекты? (список свойств)
1.2 Создание свойств (Object.defineProperty)
1.3 Описатели свойств (Object.defineProperty)
1.4 Разбор синтаксиса (bracket notation: object[‘property’])
1.5 Доступ к свойствам (через скобочную нотацию)
1.6 Удаление свойств (оператор delete)
1.7 Геттеры и сеттеры (методы доступа и записи)
1.8 Списки свойств (getOwnPropertyNames, keys)
1.9 Литералы (базовые операторы) объекта
2. Методы
2.1 Динамический this
2.2 Как реализован this
2.2.1 Если вызывается как метод объекта
2.2.2 При обычном вызове функции (this === global)
2.2.3 При явном указании контекста (.apply, .call)
2.3 Привязывание методов к контексту (.bind)
4.1 Магия оператора new
4.2 Наследование с конструкторами
5. Соглашения и совместимость
5.1 Создание объектов
5.2 Определение свойств
5.3 Списки свойств
5.4 Методы связывания
5.5 Получение [[Prototype]]
5.6 Библиотеки обратной совместимости
6. Синтаксические обёртки
7. Что читать дальше
8. Благодарности
Примечания
3. Прототипное наследование
До сих пор мы рассматривали, как определяются методы в объектах и как их повторно используют в других объектах при явном указании контекста, но это — всё же не лучший путь использования и расширения объектов.
Далее в игру вступает наследование. Оно лучше разделяет понятия, когда объекты наделяются своими методами на основе методов других объектов.
Прототипное наследование идёт дальше и может избирательно расширять методы, описывать общее поведение и использовать другие занятные приёмы, которых мы коснёмся. Печалит лишь то, что модель наследования в JS немного ограничена, и для обхода трудностей эти приёмы будут временами избыточны выносить мозг.
3.1. Прототипы
Идея наследования в джаваскрипте крутится вокруг клонирования методов объекта и дополения его собственным поведением. Объект, который клонируется, называется прототипом (не путать со свойством prototype у функций).
Прототип — обычный объект, которому довелось расширять методы другого объекта — он выступает как родитель объекта. (Это — несколько смещённое понятие относительно общепринятого, когда родителем называют функцию-конструктор, содержащую этот прототип. По возможности, перевод старается не использовать понятие родителя применительно к прототипу — прим.перев.)
Однако, клонирование не означает, что вы будете иметь различные копии функций или данных. На самом деле, в JS реализовано наследование через делегирование: все свойства хранятся у родителя, а наследникам дают попользоваться.
Наш пример пока что хорошо укладывается в эту модель. Например, методы имени и приветствия могут быть описаны в отдельном объекте и показаны там, где надо. Что приводит нас к следующей модели:
Она описывается в JS таким кодом: (хардкорно новый синтаксис, для ES5. Напомним, что аргументы в defineProperty — это объект, его имя и присваиваемый специальный объект —прим.перев)
// () → String function get_full_name(){ //возвращает полное имя объекта return this.first_name + ' ' + this.last_name; } // (new_name:String) → undefined function set_full_name(new_name){ //Вычисляем части имени из полного var names = new_name.trim().split(/\s+/); this.first_name = names['0'] ||''; this.last_name = names['1'] ||''; } //=============================================== var person = Object.create(null); //пустой объект ради правильного аргумента //Впрочем, достаточно и {} --прим.перев. Object.defineProperty(person, 'name' ,{get: get_full_name // используем предыдущие геттеры/сеттеры ,set: set_full_name ,configurable: true ,enumerable: true}); person.greet = function(person){ return this.name + ': Ну что, привет ' + person + '.'; }; // Присоединяем метод к новому объекту mikhail, добавляя person в [[Prototype]] var mikhail = Object.create(person); mikhail.first_name = 'Михаил'; mikhail.last_name = 'Белый'; mikhail.age = 19; mikhail.gender = 'Male'; //===Тестируем сделанное===: console.log(mikhail.name); // => 'Михаил Белый' - .name видно за счёт прототипа person mikhail.name = 'Michael White'; // Присваивание в name должно запустить сеттер //Теперь first_name и last_name показывают новые значения console.log(mikhail.first_name); // => 'Michael' - действительно console.log(mikhail.last_name); // => 'White' // .greet тоже унаследовано из person. console.log(mikhail.greet('тебе') ); // => 'Michael White: Ну что, привет тебе.' // Убедимся, что видим собственные свойства у mikhail console.log(Object.keys(mikhail) ); // => [ 'first_name', 'last_name', 'age', 'gender' ]
jsfiddle (1) для неверующих (IE9+)
3.2. Как работает [[Prototype]]
Как видно из примера, ни одно свойство из person не было упомянуто в mikhail, но все они прекрасно работают, потому что в JS передаются (делегируются) доступы к свойствам, т.е. свойства ищутся во всех родителях объекта.
Цепочка родителей определена в скрытых объектах каждого родителя, именуемых [[Prototype]]. Их нельзя изменить напрямую (кроме реализаций, где поддерживается .__proto__), поэтому единственный (специфицированный) способ — сеттеры при создании.
Когда свойство запрашивается из объекта, интерпретатор проверяет собственные свойства объекта. Если такое свойство отсутствует, проверяется родитель, и так — до конца цепочки родителей или до первого существующего свойства.
Если изменяем свойство прототипа, оно немедленно изменится для всех прототипов других объектов-наследников.
// (person:String) → String person.greet = function(person){ // Приветствие человеку return this.name + ': привет ' + person + '.' }; mikhail.greet('тебе'); // => 'Michael White: привет тебе.'
3.3. Переопределение свойств
Таким образом, прототипы и наследование используется для разделения доступа к данным для разных объектов и выполняется очень быстро и эффективно по затратам памяти, поскольку используется один источник данных для всех наследников.
Что, если нужно добавить специализированные методы на основе данных, которые имелись в момент наследования? Мы видели раньше, что методы определяются на основе свойств, поэтому будем определять особое поведение тем же способом — просто присваивать новые свойства.
Для демонстрации предположим, что Person реализует общее приветствие, а наследники Person — определяют собственные. Кроме того, добавим ещё одного человека, чтобы увидеть разницу.
Заметьте, что mikhail и kristin имеют индивидуальные приветствия, выражаемые версиями метода greet.
// (person:String) → String person.greet = function(person){ //общее формальное приветствие от персонажа return this.name + ': Здравствуйте' + (person ?', '+ person :'') +'!'; }; var mikhail = Object.create(person); mikhail.first_name = 'Михаил'; mikhail.last_name = 'Белый'; mikhail.age = 19; mikhail.gender = 'Male'; //переопределим greet -- вспомним про индивидуальность Михаила: //(person:String) → String mikhail.greet = function(person){ //индивидуальное панибратское приветствие return this.name + ': Здорово'+ (person ?', '+ person :', братан') +'!'; }; var kristin = Object.create(person); //новый персонаж kristin.first_name = 'Кристина'; kristin.last_name = 'Белая'; kristin.age = 19; kristin.gender = 'Female'; //(У неё другая манера приветствия) // (person:String) → String kristin.greet = function(person){ //индивидуальное эмоциональное приветствие return this.name + ': Чмоки, ' + (person ||'парниша'); }; //===Проверим, как это всё работает=== console.log(mikhail.greet(kristin.first_name) ); //=> 'Михаил Белый: Здорово, Кристина!' console.log(mikhail.greet() ); //=> 'Михаил Белый: Здорово, братан!' console.log(kristin.greet(mikhail.first_name) ); //=> 'Кристина Белая: Чмоки, Михаил' //пользуясь прототипом kristin, вернём Кристине стандартное поведение: console.log('Удаление свойства: ', delete kristin.greet); //=> true console.log(kristin.greet(mikhail.first_name) ); //=> 'Кристина Белая: Здравствуйте, Михаил'
3.4 Миксины (примеси)
Прототипы в Javascript позволяют использование общих методов, и хотя они — несомненно, сильный инструмент, они могли бы быть ещё мощнее. Они только обеспечивают наследование одного объекта другим в момент наследования.
Но такой подход не реализует другие интересные случаи, когда надо делать композицию методов, смешивание и комбинирование нескольких объектов в одном со всеми примуществами прототипного наследования.
К примеру, множественное наследование позволило бы использовать объекты — источники данных, дающие настройки методов или свойства по умолчанию.
К счастью, поскольку мы напрямую определяем методы объектов, мы можем решать эти проблемы примесями — некоторым дополнительным определением объектов во время их создания.
Что есть примеси? Можно сказать, они — «безродные», неунаследованные ниоткуда объекты. Они полностью определены в своих свойствах-методах и, чаще всего, сделаны для включения в другие объекты (хотя, их методы могли бы использоваться напрямую).
Развивая нашу небольшую модель персонажей, давайте добавим им некоторые способности. Пусть человек может быть пианистом или певцом — иметь методы pianist или singer в произвольных сочетаниях. Этот случай не укладывается в прототипную модель, поэтому пойдём на небольшой трюк. (На самом деле, можно заменить миксины переменной цепочкой наследований с прототипами, поэтому выбор миксина — это вопрос удобства и оптимальной реализации, а не следствие невыполнимости в модели прототипов. — прим.перев.)
Для работы миксинов, прежде всего, скомпонуем разные объекты в один. JS нативно не поддерживает этот необычный формат объекта, но он легко создаётся копированием всех собственных (не унаследованных) свойств.
var descriptor = Object.getOwnPropertyDescriptor //сокращения ,properties = Object.getOwnPropertyNames ,define_prop = Object.defineProperty; // (target:Object, source:Object) → Object function extend(target, source){ //копируем свойства source в target properties(source).forEach(function(key){ define_prop(target, key, descriptor(source, key)) }); return target; }
extend() здесь перебирает собственные свойства source и копирует их в target. Отметим, что target будет изменяться, для него эта функция — разрушительная, что обычно — не проблема. Важнее то, что она наименее затратна.
Теперь можем добавлять «способности» к нашим объектам.
var pianist = Object.create(null); //pianist - тот, кто может .play() на пианино pianist.play = function(){ return this.name + ' начинает играть на пианино.'; }; var singer = Object.create(null); //singer - тот, кто может .sing() singer.sing = function(){ return this.name + ' начинает петь.'; }; extend(mikhail, pianist); //добавляем возможности конечным объектам - примесь пианиста console.log(mikhail.play() ); // => 'Михаил Белый начинает играть на пианино.' // смотрим собственные, неунаследованные свойства у mikhail console.log(Object.keys(mikhail) ); //=> ['first_name', 'last_name', 'age', 'gender', 'play'] extend(kristin, singer); //определим kristin как певца (певицу) console.log(kristin.sing() ); //=> 'Кристина Белая начинает петь.' // mikhail ещё не умеет петь: try{ mikhail.sing(); //=> TypeError: Object #<Object> has no method 'sing' }catch(er){console.error('Предусмотренная ошибка: ', er)} // Но mikhail получит .sing, если расширить прототип у объекта-предка person: extend(person, singer); console.log(mikhail.sing() ); //=> 'Михаил Белый начинает петь.'
jsfiddle (3) для удобства контроля (IE9+)
3.5. Доступ к экранированным свойствам
Мы научились наследовать свойства и расширять их миксинами. Теперь есть небольшая проблема: что делать, если хотим получить доступ к перезаписанному (экранированному) свойству родительского объекта?
JS предоставляет функцию Object.getPrototypeOf которая возвращает [[Prototype]]. Поэтому доступ к свойствам прототипа достаточно прост:
Object.getPrototypeOf(mikhail).name; //не получаем результата, как и для person.name // => 'undefined undefined' person.first_name = 'Random'; //...но можем определить человека по .first_name и .last_name person.last_name = 'Person'; //...пользуясь тем, что они вызываются в геттере Object.getPrototypeOf(mikhail).name; //=> 'Random Person'
Можно было бы навно предположить, что достаточно обращения к прототипу контекста (this):
var proto = Object.getPrototypeOf; // (name:String) → String mikhail.greet = function(name){ //личное обращение к одной определённой персоне return name == 'Кристина Белая'? this.name +': Приветик, Кристи' : /*обращение ко всем остальным*/ proto(this).greet.call(this, name); }; console.log(mikhail.greet(kristin.name) ); //=> 'Михаил Белый: Приветик, Кристи' console.log(mikhail.greet('Маргарет') ); //=> 'Михаил Белый: Здравствуйте, Маргарет'
Выглядит хорошо, но есть загвоздка: если попытаться применить подход не к непосредственному предку, возникнет бесконечная рекурсия из-за того, что this видит всегда ближайший контекст функции и будет попадать на один и тот же родительский объект, как проиллюстрировано:
Простое решение — брать прототип из родительского объекта, а не из текущего. Последний пример становится таким:
var proto = Object.getPrototypeOf; //(name:String) → String //Явно указали прототип объекта mikhail - ошибки с искажением ссылки this не будет mikhail.greet = function(name){ //Избирательное приветствие return name =='Кристина Белая' ? this.name + ': Приветик, Кристи' : proto(mikhail).greet.call(this, name); //обращение к остальным }; mikhail.greet(kristin.name); //=> 'Михаил Белый: Приветик, Кристи' mikhail.greet('Маргарет'); //=> 'Михаил Белый: Здравствуйте, Маргарет!'
Способ не лишён недостатков: объект жёстко задан в функции, и мы не можем так просто взять, и применить функцию к любому объекту, как было до сих пор. Функция будет зависима от предка объекта, а не от него самого.
Если делать динамический, универсальный достуступ к прототипу родителя, это потребовало бы передачи дополнительного параметра для каждого вызова функции, что не может быть решено, как сейчас, по-быстрому в уродливых хаках. (А именно, надо сопровождать каждое наследование свойством типа .ancestor или .superclass для доступа к конструктору-предку, а функциям — использовать эти данные — прим.перев.)
Подход, предложенный в новой версии JS, решает только первую часть задачи, что самое простое. Здесь мы будем делать то же самое, но введением другого способа определения методов. Да, методов, а не общих функций.
Функции для доступа к свойствам в [[Prototype]] требуют дополнительной информации: объекта, где они записаны. Это требует поискового алгоритма, работающего со статическими данными, но решает наши рекурсивные проблемы.
Введём функцию make_method, которая возвращает функцию которая передаёт эту информацию целевой функции. (Т.е. нужно получить ссылку на прототип, в котором объявлен наш экранированный метод, и это достигается модификацией метода на каждом шаге наследования — прим.перев.)
//(object:Object, fun:Function) → Function function make_method(object, fun){ //сохранение места объявления в методе при наследовании return function(){ var args; args = [].slice.call(arguments); args.unshift(object); //вставить 'object' первым аргументом fun.apply(this, args); }; } //Все методы будут содержать в первом аргументе объект // (прототип) их объявления (конечно, теперь нигде //нельзя использовать позиционный доступ к аргументам - только по имени) function message(self, message){ var proto; proto = Object.getPrototypeOf(self); if(proto && proto.log) proto.log.call(this, message); console.log('-- собственное имя прототипа: ' + self.name +'; видимое name: '+ this.name + '; контекст вызова: '+ message); } var A = Object.create(null); //описываем цепочку прототипов C -> B -> A A.name = 'A'; A.log = make_method(A, message); var B = Object.create(A); B.name = 'B'; B.log = make_method(B, message); var C = Object.create(B); C.name = 'C'; C.log = make_method(C, message); //===тестируем вызовами методов=== A.log('~A~'); //=>-- собственное имя прототипа: A; видимое name: A; контекст вызова: ~A~ B.log('~B~'); //=>-- собственное имя прототипа: A; видимое name: B; контекст вызова: ~B~ //=>-- собственное имя прототипа: B; видимое name: B; контекст вызова: ~B~ C.log('~C~'); //=>-- собственное имя прототипа: A; видимое name: C; контекст вызова: ~C~ //=>-- собственное имя прототипа: B; видимое name: C; контекст вызова: ~C~ //=>-- собственное имя прототипа: C; видимое name: C; контекст вызова: ~C~
(Для лучшей наглядности этот пример сильно изменён в формате вывода трассировки по сравнению с оригиналом статьи; исправлены опечатки оригинала — прим.перев.)
jsfiddle (4), для любителей поковырять
Продолжение следует.
ссылка на оригинал статьи http://habrahabr.ru/post/189432/
Добавить комментарий