Содержание (скрыть) Содержание (показать)

Вы здесь

"Классы" в JavaScript

  1. Откуда методы у {} ?
  2. Object.prototype
  3. Встроенные «классы» в JavaScript
  4. Объявляем свои «классы»

В JavaScript есть встроенные объекты: Date, Array, Object и другие. Они используют прототипы и демонстрируют концепцию «псевдоклассов», которую мы вполне можем применить и для себя.

Откуда методы у {} ?

Начнём мы с того, что создадим пустой объект и выведем его.

var obj = { };
alert( obj ); // "[object Object]" ?

В объекте, очевидно, ничего нет… Но кто же тогда генерирует строковое представление для alert(obj)?

Object.prototype

…Конечно же, это сделал метод toString, который находится во встроенном прототипе Object.prototype. Этот прототип ставится всем объектам Object, и поэтому его методы доступны с момента создания.

В деталях, работает это так:

  1. Запись obj = {} является краткой формой obj = new Object, где Object — встроенная функция-конструктор для объектов.
  2. При выполнении new Object, создаваемому объекту ставится __proto__ по prototype конструктора, в данном случае это будет Object.prototype — встроенный объект, хранящий свойства и методы, общие для объектов, в частности, есть Object.prototype.toString.
  3. В дальнейшем при обращении к obj.toString() — функция будет взята из прототипа.

Это можно легко проверить:

var obj = { };

// метод берётся из прототипа?
alert(obj.toString == Object.prototype.toString); // true, да

// проверим протототип в Firefox, Chrome (где есть __proto__)
alert(obj.__proto__ == Object.prototype); // true

Встроенные «классы» в JavaScript

Точно такой же подход используется в массивах Array, функциях Function и других объектах. Встроенные методы для них находятся в Array.prototype, Function.prototype и т.п.

Как видно, получается иерархия наследования, которая всегда заканчивается на Object.prototype. Объект Object.prototype — единственный, у которого __proto__ равно null.

Поэтому говорят, что «все объекты наследуют от Object». На самом деле ничего подобного. Это все прототипы наследуют от Object.prototype.

Некоторые методы при этом переопределяются. Например, у массива Array есть свой toString, который находится в Array.prototype.toString:

var arr = [1, 2, 3]
alert( arr ); // 1,2,3 <-- результат работы Array.prototype.toString

JavaScript ищет toString, сначала в arr, затем в arr.__proto__ == Array.prototype. Если бы там не нашёл — пошёл бы выше в Array.prototype.__proto__, который по стандарту (см. диаграмму выше) равен Object.prototype.

Методы apply/call у функций тоже берутся из встроенного прототипа Function.prototype.

function f() { }

alert( f.call == Function.prototype.call ); // true

Объявляем свои «классы»

Термины «псевдокласс», «класс» отсутствуют в спецификации ES5. Но их используют, потому что подход, о котором мы будем говорить, похож на «классы», используемые в других языках программирования, таких как C++, Java, PHP и т.п.

Классом называют функцию-конструктор вместе с её prototype.

Например: «класс Object», «класс Date» — это примеры встроенных классов. Мы можем использовать тот же подход для объявления своих.

Чтобы задать класс, нужно:

  1. Объявить функцию-конструктор.
  2. Записать методы и свойства, нужные всем объектам класса, в prototype.

Опишем класс Animal:

// конструктор
function Animal(name) {
  this.name = name;
}

// методы в прототипе
Animal.prototype.run = function(speed) {
  this.speed += speed;
  alert(this.name + ' бежит, скорость ' + this.speed);
};

Animal.prototype.stop = function() {
  this.speed = 0;
  alert(this.name + ' стоит');
};

// свойство speed со значением "по умолчанию"
Animal.prototype.speed = 0;

var animal = new Animal('Зверь'); 

alert(animal.speed);               // 0, свойство взято из прототипа
animal.run(5);                     // Зверь бежит, скорость 5
animal.run(5);                     // Зверь бежит, скорость 10
animal.stop();                     // Зверь стоит

Здесь объекту animal принадлежит лишь свойство name, а остальное находится в прототипе.

Вызовы animal.run(), animal.stop() в примере выше изменяют this.speed.

При этом начальное значение speed берётся из прототипа, а новое — пишется уже в сам объект. И в дальнейшем используется. Это вполне нормально, но здесь есть важная тонкость.

Значения по умолчанию не следует хранить в прототипе в том случае, если это объекты

Посмотрите внимательно задачу ниже на эту тему.

Важность: 5

Вы — руководитель команды, которая разрабатывает игру, хомяковую ферму. Один из программистов получил задание создать класс «хомяк» (англ - "Hamster").

Объекты-хомяки должны иметь массив food для хранения еды и метод found, который добавляет к нему.

Ниже — его решение. При создании двух хомяков, если поел один — почему-то сытым становится и второй тоже.

В чём дело? Как поправить?

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 (!??)

Почему возникает проблема
Решение
Почему возникает проблема

Давайте подробнее разберем происходящее при вызове speedy.found("яблоко"):

  1. Интерпретатор ищет свойство found в speedy. Но speedy — пустой объект, т.к. new Hamster ничего не делает с this.
  2. Интерпретатор идёт по ссылке speedy.__proto__ (==Hamster.prototype) и находят там метод found, запускает его.
  3. Значение this устанавливается в объект перед точкой, т.е. в speedy.
  4. Для выполнения this.food.push() нужно найти свойство this.food. Оно отсутствует в speedy, но есть в speedy.__proto__.
  5. Значение "яблоко" добавляется в speedy.__proto__.food.

У всех хомяков общий живот! Или, в терминах JavaScript, свойство food изменяется в прототипе, который является общим для всех объектов-хомяков.

Заметим, что этой проблемы не было бы при простом присваивании:

this.food = something;

В этом случае значение записалось бы в сам объект, без поиска found в прототипе.

Проблема возникает только со свойствами-объектами в прототипе.

Исправьте её?

Исправление
Исправление

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

function Hamster() { 
*!*
  this.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) // 0(!)

Теперь всё в порядке. У каждого хомяка — свой живот.

Важность: 5

Есть объект меню, задаваемый функцией Menu. У него может быть несколько состояний. Пока — два, в дальнейшем будет больше.

Сейчас меню описано в виде конструктора, который пишет свойства и методы напрямую в объект.

Задача: переписать Menu в виде класса.

Исходный документ: tutorial/prototype/menu-sketch-src.html

Решение
Решение

Решение: tutorial/prototype/menu-sketch.html


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи – откройте её в отдельном окне и напишите там.
  3. Для кода внутри строки используйте тег <code>, для блока кода – тег <pre>, если больше 10 строк – ссылку на песочницу.
  4. Если что-то непонятно – пишите, что именно и с какого места.
Наверх
»
Содержание
»
Нашли опечатку?
Нашли опечатку на сайте? Что-то кажется странным? Выделите соответствующий текст и нажмите Ctrl+Enter!
»
Поделись!
Помоги другим узнать о хорошей статье!