- •Введение
- •Введение в JavaScript
- •Справочники и спецификации
- •Редакторы для кода
- •Консоль разработчика
- •Основы JavaScript
- •Привет, мир!
- •Внешние скрипты, порядок исполнения
- •Структура кода
- •Современный стандарт, «use strict»
- •Переменные
- •Правильный выбор имени переменной
- •Шесть типов данных, typeof
- •Основные операторы
- •Операторы сравнения и логические значения
- •Побитовые операторы
- •Взаимодействие с пользователем: alert, prompt, confirm
- •Условные операторы: if, '?'
- •Логические операторы
- •Преобразование типов для примитивов
- •Циклы while, for
- •Конструкция switch
- •Функции
- •Функциональные выражения
- •Именованные функциональные выражения
- •Всё вместе: особенности JavaScript
- •Качество кода
- •Отладка в браузере Chrome
- •Советы по стилю кода
- •Как писать неподдерживаемый код?
- •Автоматические тесты при помощи chai и mocha
- •Структуры данных
- •Введение в методы и свойства
- •Числа
- •Строки
- •Объекты как ассоциативные массивы
- •Объекты: перебор свойств
- •Объекты: передача по ссылке
- •Массивы c числовыми индексами
- •Массивы: методы
- •Массив: перебирающие методы
- •Псевдомассив аргументов «arguments»
- •Дата и Время
- •Замыкания, область видимости
- •Глобальный объект
- •Замыкания, функции изнутри
- •[[Scope]] для new Function
- •Локальные переменные для объекта
- •Модули через замыкания
- •Управление памятью в JavaScript
- •Устаревшая конструкция «with»
- •Методы объектов и контекст вызова
- •Методы объектов, this
- •Преобразование объектов: toString и valueOf
- •Создание объектов через «new»
- •Дескрипторы, геттеры и сеттеры свойств
- •Статические и фабричные методы
- •Явное указание this: «call», «apply»
- •Привязка контекста и карринг: «bind»
- •Функции-обёртки, декораторы
- •Некоторые другие возможности
- •Типы данных: [[Class]], instanceof и утки
- •Формат JSON, метод toJSON
- •setTimeout и setInterval
- •Запуск кода из строки: eval
- •Перехват ошибок, «try..catch»
- •ООП в функциональном стиле
- •Введение
- •Внутренний и внешний интерфейс
- •Геттеры и сеттеры
- •Функциональное наследование
- •ООП в прототипном стиле
- •Прототип объекта
- •Свойство F.prototype и создание объектов через new
- •Встроенные «классы» в JavaScript
- •Свои классы на прототипах
- •Наследование классов в JavaScript
- •Проверка класса: «instanceof»
- •Свои ошибки, наследование от Error
function Hamster() {}
Hamster.prototype.food = []; // пустой "живот"
Hamster.prototype.found = function(something) { this.food.push(something);
};
// Создаём двух хомяков и кормим первого speedy = new Hamster();
lazy = new Hamster();
speedy.found("яблоко"); speedy.found("орех");
alert( speedy.food.length ); // 2 alert( lazy.food.length ); // 2 (!??)
К решению
Наследование классов в JavaScript
Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку __proto__.
Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через new Admin, должны иметь все методы, которые есть у объектов, создаваемых через
new User, и ещё какие-то свои.
Наследование Array от Object
Для реализации наследования в наших классах мы будем использовать тот же подход, который принят внутри JavaScript.
Взглянем на него ещё раз на примере Array, который наследует от Object:
● Методы массивов Arrayхранятся в Array.prototype.
● Array.prototypeимеет прототипом Object.prototype.
Поэтому когда экземпляры класса Arrayхотят получить метод массива — они берут его из своего прототипа, например Array.prototype.slice.
Если же нужен метод объекта, например, hasOwnProperty, то его в Array.prototypeнет, и он берётся из Object.prototype.
Отличный способ «потрогать это руками» — запустить в консоли команду console.dir([1,2,3]).
Вывод в Chrome будет примерно таким:
Здесь отчётливо видно, что сами данные и lengthнаходятся в массиве, дальше в __proto__идут методы для массивов concat, то есть Array.prototype, а далее — Object.prototype.
console.dirдля доступа к свойствам
Обратите внимание, я использовал именно console.dir, а не console.log, поскольку log зачастую выводит объект в виде строки, без доступа к свойствам.
Наследование в наших классах
Применим тот же подход для наших классов: объявим класс Rabbit, который будет наследовать от
Animal.
Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы.
Animal:
function Animal(name) { this.name = name; this.speed = 0;
}
Animal.prototype.run = function(speed) { this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed ); };
Animal.prototype.stop = function() { this.speed = 0;
alert( this.name + ' стоит' ); };
Rabbit:
function Rabbit(name) { this.name = name; this.speed = 0;
}
Rabbit.prototype.jump = function() { this.speed++;
alert( this.name + ' прыгает' ); };
var rabbit = new Rabbit('Кроль');
Для того, чтобы наследование работало, объект rabbit = new Rabbitдолжен использовать свойства и методы из своего прототипа Rabbit.prototype, а если их там нет, то — свойства и метода родителя, которые хранятся в Animal.prototype.
Если ещё короче — порядок поиска свойств и методов должен быть таким: rabbit > Rabbit.prototype > Animal.prototype, по аналогии с тем, как это сделано для объектов и массивов.
Для этого можно поставить ссылку __proto__с Rabbit.prototypeна Animal.prototype.
Можно сделать это так:
Rabbit.prototype.__proto__ = Animal.prototype;
Однако, прямой доступ к __proto__не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию Object.create. Она либо встроена либо легко эмулируется во всех браузерах.
Класс Animalостаётся без изменений, а Rabbit.prototypeмы будем создавать с нужным прототипом,
используя Object.create:
function Rabbit(name) { this.name = name; this.speed = 0;
}
// задаём наследование
Rabbit.prototype = Object.create(Animal.prototype);
// и добавим свой метод (или методы...) Rabbit.prototype.jump = function() { ... };
Теперь выглядеть иерархия будет так:
В prototypeпо умолчанию всегда находится свойство constructor, указывающее на функциюконструктор. В частности, Rabbit.prototype.constructor == Rabbit. Если мы рассчитываем использовать это свойство, то при замене prototypeчерез Object.createнужно его явно сохранить:
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
Полный код наследования
Для наглядности — вот итоговый код с двумя классами Animalи Rabbit:
//1. Конструктор Animal function Animal(name) {
this.name = name; this.speed = 0;
}
//1.1. Методы в прототип
Animal.prototype.stop = function() { this.speed = 0;
alert( this.name + ' стоит' );
}
Animal.prototype.run = function(speed) { this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed ); };
//2. Конструктор Rabbit function Rabbit(name) {
this.name = name; this.speed = 0;
}
//2.1. Наследование
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
// 2.2. Методы Rabbit Rabbit.prototype.jump = function() {
this.speed++;
alert( this.name + ' прыгает, скорость ' + this.speed );
}
Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте.
Обратим внимание: Rabbit.prototype = Object.create(proto)присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы.
Неправильный вариант: Rabbit.prototype = new Animal
В некоторых устаревших руководствах предлагают вместо Object.create(Animal.prototype) записывать в прототип new Animal, вот так:
// вместо Rabbit.prototype = Object.create(Animal.prototype) Rabbit.prototype = new Animal();
Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь new Animal— это
объект с прототипом Animal.prototype, как и Object.create(Animal.prototype). Они в этом плане идентичны.
Но у этого подхода важный недостаток. Как правило мы не хотим создавать Animal, а хотим только унаследовать его методы!
Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с Object.create.
Вызов конструктора родителя
Посмотрим внимательно на конструкторы Animalи Rabbitиз примеров выше:
function Animal(name) { this.name = name; this.speed = 0;
}
function Rabbit(name) { this.name = name; this.speed = 0;
}
Как видно, объект Rabbitне добавляет никакой особенной логики при создании, которой не было в
Animal.
Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора Animal, а напрямую вызвать его:
function Rabbit(name) { Animal.apply(this, arguments);
}
Такой вызов запустит функцию Animalв контексте текущего объекта, со всеми аргументами, она выполнится и запишет в thisвсё, что нужно.
Здесь можно было бы использовать и Animal.call(this, name), но applyнадёжнее, так как работает с любым количеством аргументов.
Переопределение метода
Итак, Rabbitнаследует Animal. Теперь если какого-то метода нет в Rabbit.prototype— он будет взят из Animal.prototype.
В Rabbitможет понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод run():
Rabbit.prototype.run = function(speed) { this.speed++;
this.jump(); };
Вызов rabbit.run()теперь будет брать runиз своего прототипа:
Вызов метода родителя внутри своего
Более частая ситуация — когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает.
Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа:
Rabbit.prototype.run = function() {
// вызвать метод родителя, передав ему текущие аргументы
Animal.prototype.run.apply(this, arguments); this.jump();
}
Обратите внимание на вызов через applyи явное указание контекста.
Если вызвать просто Animal.prototype.run(), то в качестве thisфункция runполучит Animal.prototype, а это неверно, нужен текущий объект.
Итого
●Для наследования нужно, чтобы «склад методов потомка» (Child.prototype) наследовал от «склада метода родителей» (Parent.prototype).
Это можно сделать при помощи Object.create:
Код:
Rabbit.prototype = Object.create(Animal.prototype);
●Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя apply(this, arguments), вот так:
function Rabbit(...) { Animal.apply(this, arguments);
}
●При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа:
Rabbit.prototype.run = function() {
var result = Animal.prototype.run.apply(this, ...); // result результат вызова метода родителя
}
Структура наследования полностью:
// Класс Родитель
//Конструктор родителя пишет свойства конкретного объекта function Animal(name) {
this.name = name; this.speed = 0;
}
//Методы хранятся в прототипе
Animal.prototype.run = function() { alert(this.name + " бежит!")
}
// Класс потомок
//Конструктор потомка function Rabbit(name) {
Animal.apply(this, arguments);
}
//Унаследовать
Rabbit.prototype = Object.create(Animal.prototype);
//Желательно и constructor сохранить
Rabbit.prototype.constructor = Rabbit;
//Методы потомка
Rabbit.prototype.run = function() {
// Вызов метода родителя внутри своего
Animal.prototype.run.apply(this); alert( this.name + " подпрыгивает!" );
};
// Готово, можно создавать объекты var rabbit = new Rabbit('Кроль'); rabbit.run();
Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.
Кроме того, есть ещё неявное, но очень важное архитектурное отличие.
Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно.
Иначе говоря, в функциональном стиле в процессе создания Rabbitнужно обязательно вызывать
Animal.apply(this, arguments), чтобы получить методы родителя — и если этот Animal.apply
кроме добавления методов говорит: «Му-у-у!», то это проблема:
function Animal() { this.walk = function() {
alert('walk') };
alert( 'Му у у!' );
}
function Rabbit() {
Animal.apply(this, arguments); // как избавиться от мычания, но получить walk?
}
…Которой нет в прототипном подходе, потому что в процессе создания new Rabbitмы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.
Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и
универсальный. А что касается красоты синтаксиса — она сильно лучше в новом стандарте ES6,
которым можно пользоваться уже сейчас, если взять транслятор babeljs .
Задачи
Найдите ошибку в наследовании
важность: 5
Найдите ошибку в прототипном наследовании. К чему она приведёт?
function Animal(name) { this.name = name;
}
Animal.prototype.walk = function() { alert( "ходит " + this.name );
};
function Rabbit(name) { this.name = name;
}
Rabbit.prototype = Animal.prototype;
Rabbit.prototype.walk = function() {
alert( "прыгает! и ходит: " + this.name ); };
К решению
В чём ошибка в наследовании
важность: 5
Найдите ошибку в прототипном наследовании. К чему она приведёт?
function Animal(name) { this.name = name;
this.walk = function() {
alert( "ходит " + this.name ); };
}
function Rabbit(name) { Animal.apply(this, arguments);
}
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.walk = function() { alert( "прыгает " + this.name );
};
var rabbit = new Rabbit("Кроль"); rabbit.walk();
К решению
Класс «часы»
важность: 5
Есть реализация часиков, оформленная в виде одной функции-конструктора. У неё есть приватные свойства timer, templateи метод render.
Задача: переписать часы на прототипах. Приватные свойства и методы сделать защищёнными.
P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть).
Открыть песочницу для задачи.
К решению
Класс «расширенные часы»
важность: 5
Есть реализация часиков на прототипах. Создайте класс, расширяющий её, добавляющий поддержку параметра precision, который будет задавать частоту тика в setInterval. Значение по умолчанию:
1000.
●Для этого класс Clockнадо унаследовать. Пишите ваш новый код в файле extended clock.js.
●Исходный класс Clockменять нельзя.
●Пусть конструктор потомка вызывает конструктор родителя. Это позволит избежать проблем при расширении Clockновыми опциями.
P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть).
Открыть песочницу для задачи.
К решению
Меню с таймером для анимации
важность: 5
Есть класс Menu. У него может быть два состояния: открыто STATE_OPENи закрыто STATE_CLOSED.
Создайте наследника AnimatingMenu, который добавляет третье состояние STATE_ANIMATING.
●При вызове open()состояние меняется на STATE_ANIMATING, а через 1 секунду, по таймеру, открытие завершается вызовом open()родителя.
●Вызов close()при необходимости отменяет таймер анимации (назначаемый в open) и передаёт вызов родительскому close.
●Метод showStateдля нового состояния выводит "анимация", для остальных — полагается на родителя.
Исходный документ, вместе с тестом
Открыть песочницу для задачи.
К решению
Что содержит constructor?
важность: 5
В коде ниже создаётся простейшая иерархия классов: Animal > Rabbit.
Что содержит свойство rabbit.constructor? Распознает ли проверка в alertобъект как Rabbit?
function Animal() {}
function Rabbit() {}
Rabbit.prototype = Object.create(Animal.prototype);
var rabbit = new Rabbit();
alert( rabbit.constructor == Rabbit ); // что выведет?
К решению