Статьи

Краткое введение в объекты JavaScript

Когда вы работаете с языком программирования на основе классов, использование объектов в 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, двигатель следует этой логике:

  1. Есть ли OBJ определяет й ? Если ответ «да», то используется x из obj .
  2. В противном случае ищите x в прототипе.
  3. Если еще не найдено, продолжайте с прототипом прототипа.

Как видите, это похоже на поиск методов, используемый в языках программирования на основе классов; просто замените прототип на суперкласс .

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