Когда вы работаете с языком программирования на основе классов, использование объектов в JavaScript кажется странным: где ключевое слово class ? Как я могу сделать наследство?
Как мы увидим, JavaScript на самом деле довольно прост. Он поддерживает классоподобное определение объектов и единственное наследование из коробки.
Но сначала небольшой совет, который изменит ваш опыт чтения: используя консоль JavaScript вашего браузера, вы можете поиграть с примерами, не покидая этой страницы:
- Chrome : MacOSX Cmd-Alt-J / Windows Ctrl-Shift-J
- Firefox : MacOSX Cmd-Alt-K / Windows Ctrl-Alt-K
- Safari : Cmd-Alt-C (только если вы включите меню «Разработка» в разделе «Дополнительные настройки»)
- IE8 + : нажмите F12 и перейдите к консоли
Основы
ОК, все готово. Теперь первый шаг, определите объект:
var point = {x: 1, y: 2};
Как видите, синтаксис довольно прост, и доступ к членам объекта осуществляется обычными средствами:
point.x // gives 1
Также мы можем добавить свойства в любое время:
var point = {};
point.x = 1;
point.y = 2;
Неуловимое это ключевое слово
Теперь давайте перейдем к более интересным вещам. У нас есть функция, которая вычисляет расстояние от точки до начала координат (0,0):
function distanceFromOrigin(x, y) {
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
}
Та же самая функция, написанная как метод
pointвыглядит следующим образом:
var point = {
x:1, y:2,
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
Если мы оцениваем:
point.distanceFromOrigin(), тоthisключевое слово становитсяpoint.Когда вы пришли из Java, это может показаться очевидным, но поскольку мы углубляемся в детали JavaScript, это не так.
Функции в JavaScript обрабатываются как любое другое значение; это означает , что distanceFromOriginне имеет ничего особенного по сравнению с xи yполями. Например, мы можем переписать код следующим образом:
var fn = function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
};
var point = {x:1, y:2, distanceFromOrigin: fn };
Как это определяется?
JavaScript знает, как назначать this, из-за того, как distanceFromOriginоценивается:
point.distanceFromOrigin();
Но выполнение просто
fn()не будет работать, как ожидалось: оно вернетсяNaN, причинаthis.xиthis.yестьundefined. Смущенный? Давайте вернемся к нашему первоначальномуpointопределению:var point = {
x:1, y:2,
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
Поскольку
distanceFromOrigin, как и любое другое значение, мы можем получить его и присвоить переменной:
var fn = point.distanceFromOrigin;
Опять
fn()возвращается
NaN. Как видно из двух предыдущих примеров, когда функция определена, специальной привязки к объекту нет. Привязка выполняется при вызове функции: если используется
obj.method()синтаксис
this, автоматически устанавливается получатель.
Можно явно установить это ?
Функции JavaScript являются объектами, и, как и любой объект, они имеют методы.
В частности, функция имеет два метода applyи call, который выполняет функцию , но позволяет установить значение this:
point.distanceFromOrigin() // is equivalent to… point.distanceFromOrigin.call(point);
Например:
function twoTimes() {
return this * 2;
}
twoTimes.call(2); // returns 4
Определение общего поведения
Теперь предположим, что у нас больше очков:
var point1 = {
x:1, y:2,
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
var point2 = {
x:3, y:4,
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
Нет смысла копировать и вставлять этот фрагмент каждый раз, когда вы хотите получить точку, поэтому небольшой рефакторинг помогает:
function createPoint(x, y) {
var fn = function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
return {x: x, y:y, distanceFromOrigin:fn};
}
var point1 = createPoint(1, 2);
var point2 = createPoint(3, 4);
Мы можем создать много точек таким образом, но:
- Это делает неэффективное использование памяти:
fnсоздается для каждой точки. - Поскольку между каждым точечным объектом нет взаимосвязи, виртуальная машина не может выполнять какую-либо динамическую оптимизацию. (ОК, это не очевидно и зависит от виртуальной машины, но может повлиять на скорость выполнения).
Чтобы исправить эти проблемы, JavaScript имеет возможность сделать интеллектуальную копию существующего объекта:
var point1 = {
x:1, y:2,
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
var point2 = Object.create(point1);
point2.x = 2;
point2.y = 3;
Object.create(point1)использует
point1в качестве
прототипа для создания нового объекта. Если вы проверите,
point2это будет выглядеть так:
x: 2
y: 3
__proto__
distanceFromOrigin: function () { /* … */ }
x: 1
y: 2
ПРИМЕЧАНИЕ:
__proto__нестандартное внутреннее поле, отображаемое отладчиком. Правильный способ получить прототип объектаObject.getPrototypeOf, например:Object.getPrototypeOf(point2) === point1
Этот способ обработки объектов как копий других объектов называется программированием на основе прототипов и концептуально проще, чем программирование на основе классов.
Некрасивая синтаксическая часть
До сих пор я рассказывал вам приятную часть истории.
Object.createбыл добавлен в JavaScript 1.8.5 (он же ECMAScript 5th Edition или просто ES5 ). Итак, как были клонированы объекты в предыдущих версиях языка?
Здесь идет некрасивая синтаксическая часть. Каждая функция является объектом, поэтому мы можем динамически добавлять свойства к функциям:
fn.somevalue = 'hello';
Предположим на минуту, что у нас есть
Object.create. Таким образом, мы можем использовать функциональные объекты и
Object.createполучать всю информацию, необходимую для копирования и инициализации объектов за один шаг:
// we store the "prototype" in fn.prototype
function newObject(fn, args) {
var obj = Object.create(fn.prototype);
obj.constructor = fn; // we keep this reference... just because we can 😉
fn.apply(obj, args); // remember this will evaluate fn with obj as "this"
return obj;
}
Хорошо, но я сказал вам, что у нас
Object.createеще нет, так что мы будем делать?
У JavaScript есть ключевое слово, которое делает то же самое, что и
newObjectфункция:
newObject(fn); // is equivalent to.. new fn()
ПРИМЕЧАНИЕ. В целях пояснения я показал, как реализовать
newиспользованиеObject.create. Примите во внимание, чтоnewэто ключевое слово языка, и даже если оно семантически эквивалентноnewObject, реализация отличается. На самом деле для некоторых движков JavaScript создание объектов сnewнемного быстрее, чемObject.create.
ТакжеObject.createэто относительно недавнее добавление, в старых движках, таких как IE8, обычная хитрость — реализовать это с помощьюnew. Я показалObject.createсначала, потому что это делает вещи простыми для понимания.
Почему в JavaScript такое странное использование функций? Я не знаю. Я предполагаю, что, вероятно, разработчики языка хотели каким-то образом походить на Java, поэтому они добавили newключевое слово для имитации классов и конструкторов.
Используя new, вы можете написать предыдущий пример:
// the point constructor
function Point(x, y) {
// "this" will be a copy of Point.prototype
this.x = x;
this.y = y;
}
// the prototype instance to copy
Point.prototype = {
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
Теперь каждый раз, когда мы делаем
new Point(x, y), мы получаем новую точку:
var point1 = new Point(1, 2); var point2 = new Point(2, 3);
Что нужно знать о прототипе и конструкторе
Когда вы оцениваете obj.x, двигатель следует этой логике:
- Есть ли OBJ определяет й ? Если ответ «да», то используется x из obj .
- В противном случае ищите x в прототипе.
- Если еще не найдено, продолжайте с прототипом прототипа.
Как видите, это похоже на поиск методов, используемый в языках программирования на основе классов; просто замените прототип на суперкласс .
Но так как prototypeполе почти как любое другое поле, мы можем делать классные динамические вещи, такие как добавление новых методов в существующие экземпляры:
var hello = "Hello";
String.prototype.display = function () { console.log(this.toString()); }
hello.display()
А как насчет
constructor?
У каждого объекта в JavaScript есть
constructorсвойство, даже если вы его не определяете. Когда объект , созданный с использованием
newв
constructorпункты свойств функции , используемой для создания объекта.
Единственное Наследование
Мы можем применить то, что мы научились делать с одиночным наследованием:
// the "super class"
function Point(x, y) { this.x = x; this.y = y; }
Point.prototype = {
distanceFromOrigin: function () {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
};
// the "sub class": ColoredPoint extends Point
function ColoredPoint(x, y, color) {
// call the "super constructor"
Point.call(this, x, y);
this.color = color;
}
// We use a clone of Point.prototype as the extension prototype
ColoredPoint.prototype = Object.create(Point.prototype);
// we extend Point with the show method
ColoredPoint.prototype.show = function () {
console.log('Point with color ' + this.color + ' at (' + this.x + ',' + this.y + ')');
}
// finally we can use colored points:
var p = new ColoredPoint(10, 20, 'red');
console.log(p.distanceFromOrigin());
p.show();
Как видите, можно выполнить одиночное наследование, но необходимо выполнить много шагов. Вот почему существует так много библиотек JavaScript, чтобы упростить определение объектов.
В следующем посте я поделюсь своим опытом создания бармена (одной из множества библиотек определений объектов JavaScript). И я буду использовать этот опыт для обсуждения некоторых «продвинутых» техник, чтобы поделиться поведением, таким как миксин и черты характера .