Когда вы работаете с языком программирования на основе классов, использование объектов в 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). И я буду использовать этот опыт для обсуждения некоторых «продвинутых» техник, чтобы поделиться поведением, таким как миксин и черты характера .