В JavaScript существует действительно много различных возможностей для создания объектов и реализации наследования. Поначалу в этом многообразии довольно сложно определить для самого себя, в каком стиле следует писать код, что вытекает в гремучую смесь разных подходов в одном приложении.
Данный пост является переводом "заметки" Демиана Клиннерта (Damien Klinnert) о том, каким образом принято использовать объекты в сообществе JS.
Данный код демонстрирует, каким образом в javascript следует создавать объекты. Он показывает лучшие приемы по использованию конструкторов, созданию атрибутов и применению наследования.
Замечу, что в JS существует много способов по работе с объектами и наследованием. Мы решили использовать описанный ниже подход, так как он довольно общеупотребительный, использует базовый для JS подход к наследованию через прототипы, полностью совместим с чистым ECMAScript3.1 и ECMAScript6.
Сообщество использует именно этот подход. Последняя версия ECMAScript так же демонстрирует, что язык развивается в этом направлении и к тому же это наиболее производительный подход.
Хотя, данный подход сильно завязан на доверии. Он очень динамичный и гибкий, но к тому и же и не безопасный. В нем не существует действительно приватных атрибутов и мы доверяем другим разработчикам в том, что они тоже следуют данному подходу, так как в противном случае они могут нанести серьезный вред всей системе в целом.
Начнем с определения нового типа, в данном примере - с машины. Название объекта начинается с большой буквы, чтобы дать понять, что данная функция должна быть вызвана с ключевым словом "new".
var Car = function (arg1, arg2) {
this._velocity = 0;
this._speed = 0;
this._color = null;
this._arg1 = arg1;
this._arg2 = arg2;
};
С точки зрения большей производительности в конструкторе всегда стоит устанавливать некоторое значение для атрибутов даже если это null. Благодаря этому компилятор или интерпретатор JavaScript сможет оптимизировать использование памяти.
Так же имейте ввиду, что в JavaScript нет приватных атрибутов. Общим подходом является использование префикса "_" в имени атрибута, чтобы показать, что данный атрибут не должен изменяться снаружи. Все это, безусловно, построено только на доверии.
Чтобы добавить методы для экземпляра объекта Car необходимо установить его через прототип этого объекта. Таким образом мы будем уверены в оптимальной производительности, так как все экземпляры объекта Car будут использовать одинаковую реализацию функции, отличную лишь окружением. Это позволяет сильно сэкономить память. Никогда не определяйте функции в конструкторе, так как если вы это сделаете, то каждый объект будет иметь свою собственную реализацию данной функции.
Car.prototype.drive = function () {
// …
};
Чтобы добавить статический метод в объект Car - просто определим его в конструкторе. Все статические методы используют одну реализацию и имеют одно пространство имен.
Car.staticMethod = function () {
// …
};
Хотя все атрибуты имеют прямой доступ снаружи, это само по себе плохая практика и должна каким-то образом контролироваться. Getters имеют такое же название как и атрибут, только без "_". Они не имеют параметров и всегда возвращают значение из "this".
Car.prototype.velocity = function () {
return this._velocity;
};
У getters, которые возвращают значение типа Boolean, должен быть префикс "is" или "has". Например:
Car.prototype.isMotorActive = function () {
return this._isMotorActive;
};
В случае Setters
к имени атрибута добавляется префикс "set". У них есть один параметр и они всегда возвращают "this" (для возможности выстраивания их цепочку - будет показано ниже).
Car.prototype.setVelocity = function (velocity) {
this._velocity = velocity;
return this;
};
Благодаря тому, что setters всегда возвращают "this", они могут быть выстроены в цепочку, например:
var myCar = new Car('arg1', 'arg2');
myCar.setVelocity(0)
.setSpeed(0)
.setSomethingElse(0);
Чтобы унаследовать один объект от другого, мы используем наследование прототипа. Для этого в первую очередь в новом объекте нам следует создать новый конструктор:
var FireCar = function (arg1, arg2, arg3) {
Car.call(this, arg1, arg2);
this._color = '#ff0000';
this._hasLedge = true;
this._arg3 = arg3;
};
Наиболее важной строчкой является вызов родительского конструктора через "Car.call". Благодаря этому объект FireCar будет иметь все атрибуты своего родителя "Car". После этого мы можем добавлять новые атрибуты, необходимые для нового объекта.
Затем нам необходимо убедится в том, что когда будет вызвана функция отсутствующая в новом объекте, то вызов будет перенаправлен к родительской реализации этой функции.
FireCar.prototype = new Car();
Благодаря этому прототип FireCar будет перезаписан новым объектом, созданным на основе Car.prototype.
Для того чтобы просто переопределить один из родительских методов - необходимо просто определить его в FireCar.prototype.
FireCar.prototype.drive = function () {
// …
};
Если же вы хотите переопределить родительский метод, но при этом использовать родительскую реализацию, то необходимо воспользоваться методом "call" (как и в случае с конструктором):
FireCar.prototype.drive = function (arg1, arg2) {
var result = Car.prototype.drive.call(this, arg1, arg2);
// do some more stuff (and own implementation)
return result + 1;
};
Подготовка к наследованию завершена, теперь можно создать желанный объект:
var yourCar = new FireCar('arg1', 'arg2', 'arg3');