Конспект JS-course

"Классы" в JavaScript

Источник: http://learn.javascript.ru/classes

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

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

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

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

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

Object.prototype

Встроенный прототип 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 берётся из прототипа, а новое — пишется уже в сам объект. И в дальнейшем используется. Это вполне нормально, но здесь есть важная тонкость.

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