[Перевод] Продвинутое использование объектов в JavaScript

Этот пост выходит за рамки повседневного использования объектов в JavaScript. Основы работы с объектами по большей части так же просты, как использование JSON-нотации. Тем не менее, JavaScript дает возможность использовать тонкий инструментарий, с помощью которого можно создавать объекты некоторыми интересными и полезными способами и который теперь доступен в последних версиях современных браузеров.Последние два вопроса, которые будут затронуты — Proxy и Symbol относятся к спецификации ECMAScript 6, реализованы частично и внедрены только в некоторых из современных браузеров.Геттеры и сеттерыГеттеры и сеттеры уже некоторое время доступны в JavaScript, однако я не замечал за собой, чтобы мне приходилось их часто использовать. Зачастую я пишу обычные функции для получения свойств, нечто вроде этого: /** * @param {string} prefix * @constructor */ function Product (prefix) { /** * @private * @type {string} */ this.prefix_ = prefix; /** * @private * @type {string} */ this.type_ = »; }

/** * @param {string} newType */ Product.prototype.setType = function (newType) { this.type_ = newType; };

/** * @return {string} */ Product.prototype.type = function () { return this.prefix_ + »:» + this.type_; }

var product = new Product («fruit»); product.setType («apple»); console.log (product.type ()); //logs fruit: apple jsfiddleИспользуя геттер можно упростить этот код.

/** * @param {string} prefix * @constructor */ function Product (prefix) { /** * @private * @type {number} */ this.prefix_ = prefix; /** * @private * @type {string} */ this.type_ = »; }

/** * @param {string} newType */ Product.prototype = { /** * @return {string} */ get type () { return this.prefix_ + »:» + this.type_; }, /** * @param {string} */ set type (newType) { this.type_ = newType; } };

var product = new Product («fruit»);

product.type = «apple»; console.log (product.type); //logs «fruit: apple»

console.log (product.type = «orange»); //logs «orange» console.log (product.type); //logs «fruit: orange» jsfiddleКод остается немного избыточным, а синтаксис — немного непривычным, однако преимущества применения get и set становятся более явными во время их прямого использования. Я для себя нашел, что:

product.type = «apple»; console.log (product.type); гораздо более читаемо, чем: product.setType («apple»); console.log (product.type ()); хотя моя встроенная сигнализация плохого JavaScript до сих пор срабатывает, когда я вижу прямое обращение и задание свойств экземплярам объекта. За долгое время я был научен багами и техническими требованиями избегать произвольного задания свойств экземплярам класса, так как это непременно приводит к тому, что информация распространяется между ими всеми. Также есть некоторый нюанс в том, в каком порядке возвращаются устанавливаемые значения, обратите внимание на пример ниже. console.log (product.type = «orange»); //logs «orange» console.log (product.type); //logs «fruit: orange» Обратите внимание, что сначала в консоль выводится «orange» и только потом «fruit: orange». Геттер не выполняется в то время как возвращается устанавливаемое значение, так что при такой форме сокращенной записи можно наткнуться на неприятности. Возвращаемые при помощи set значения игнорируются. Добавление return this.type; к set не решает этой проблемы. Обычно это решается повторным использованием заданного значения, но могут возникнуть проблемы со свойством, имеющим геттер.defineProperty Синтаксис get propertyname () работает с литералами объектов и в предыдущем примере я назначил литерал объекта Product.prototype. В этом нет ничего плохого, но использование литералов вроде этого усложняет цепочку вызова прототипов для реализации наследования. Существует возможность определения геттеров и сеттеров в прототипе без использования литералов — при помощи defineProperty /** * @param {string} prefix * @constructor */ function Product (prefix) { /** * @private * @type {number} */ this.prefix_ = prefix; /** * @private * @type {string} */ this.type_ = »; }

/** * @param {string} newType */ Object.defineProperty (Product.prototype, «type», { /** * @return {string} */ get: function () { return this.prefix_ + »:» + this.type_; }, /** * @param {string} */ set: function (newType) { this.type_ = newType; } }); jsfiddleПоведение этого кода такое же как и в предыдущем примере. Вместо добавления геттеров и сеттеров, предпочтение отдается defineProperty. Третьим аргументом в defineProperty передается дескриптор и в дополнение к set и get он дает возможность настроить доступность и установить значение. При помощи defineProperty можно создать нечто вроде константы — свойства, которое никогда не будет удалено или переопределено.

var obj = { foo: «bar», };

//A normal object property console.log (obj.foo); //logs «bar»

obj.foo = «foobar»; console.log (obj.foo); //logs «foobar»

delete obj.foo; console.log (obj.foo); //logs undefined

Object.defineProperty (obj, «foo», { value: «bar», });

console.log (obj.foo); //logs «bar», we were able to modify foo

obj.foo = «foobar»; console.log (obj.foo); //logs «bar», write failed silently

delete obj.foo; console.log (obj.foo); //logs bar, delete failed silently jsfiddleРезультат:

bar foobar undefined bar bar bar Две последние попытки переопределить foo.bar в примере завершились неудачей (пусть и не были прерваны сообщением об ошибке), так как это поведение defineProperty по умолчанию — запрещать изменения. Чтобы изменить такое поведение, можно использовать ключи configurable и writable. Если вы используете строгий режим, ошибки будут брошены, так как являются обычными ошибками JavaScript. var obj = {};

Object.defineProperty (obj, «foo», { value: «bar», configurable: true, writable: true, });

console.log (obj.foo); //logs «bar» obj.foo = «foobar»; console.log (obj.foo); //logs «foobar» delete obj.foo; console.log (obj.foo); //logs undefined jsfiddleКлюч configurable позволяет предотвратить удаление свойства из объекта. Кроме того, он дает возможность предотвратить последующее изменение свойства при помощи другого вызова defineProperty. Ключ writable дает возможность записать в свойство или изменять его значение.

Если configurable установлен в false (как и есть по умолчанию), попытки вызова defineProperty во второй раз приведут к тому, что будет брошена ошибка.

var obj = {};

Object.defineProperty (obj, «foo», { value: «bar», });

Object.defineProperty (obj, «foo», { value: «foobar», });

// Uncaught TypeError: Cannot redefine property: foo jsfiddleЕсли configurable установлен в true, то можно изменять свойство в будущем. Это можно использовать для того, чтобы изменять значение незаписываемого свойства.

var obj = {};

Object.defineProperty (obj, «foo», { value: «bar», configurable: true, });

obj.foo = «foobar»;

console.log (obj.foo); // logs «bar», write failed

Object.defineProperty (obj, «foo», { value: «foobar», configurable: true, });

console.log (obj.foo); // logs «foobar» jsfiddleТакже необходимо обратить внимание на то, что значения, определенные при помощи defineProperty не итерируются в цикле for in

var i, inventory;

inventory = { «apples»: 10, «oranges»: 13, };

Object.defineProperty (inventory, «strawberries», { value: 3, });

for (i in inventory) { console.log (i, inventory[i]); } jsfiddle apples 10 oranges 13 Чтобы позволить это, необходимо использовать свойство enumerable var i, inventory;

inventory = { «apples»: 10, «oranges»: 13, };

Object.defineProperty (inventory, «strawberries», { value: 3, enumerable: true, });

for (i in inventory) { console.log (i, inventory[i]); } jsfiddle apples 10 oranges 13 strawberries 3 Для проверки того, появится ли свойство в цикле for in можно использовать isPropertyEnumerable var i, inventory;

inventory = { «apples»: 10, «oranges»: 13, };

Object.defineProperty (inventory, «strawberries», { value: 3, });

console.log (inventory.propertyIsEnumerable («apples»)); //console logs true console.log (inventory.propertyIsEnumerable («strawberries»)); //console logs false jsfiddleВызов propertyIsEnumerable также вернет false для свойств, определенных выше по цепочке прототипов, или для свойств, не определенных любым другим способом для этого объекта, что, впрочем, очевидно.И ещё несколько слов напоследок об использовании defineProperty: будет ошибкой совмещать методы доступа set и get с writable: true или комбинировать их с value. Определение свойства при помощи числа приведет это число к строке, как было бы при любых других обстоятельствах. Вы также можете использовать defineProperty чтобы определить value как функцию.

defineProperties Существует также и defineProperties. Этот метод позволяет определить несколько свойств за один раз. Мне попадался на глаза jsperf, сравнивающий использование defineProperties с defineProperty и, по крайней мере в Хроме, особой разницы в том, какой из методов использовать, не было.

var foo = {}

Object.defineProperties (foo, { bar: { value: «foo», writable: true, }, foo: { value: function () { console.log (this.bar); } }, });

foo.bar = «foobar»; foo.foo (); //logs «foobar» jsfiddleObject.create Object.create это альтернатива new, дающему возможность создать объект с определенным прототипом. Эта функция принимает два аргумента: первый это прототип, из которого вы хотите создать объект, а второй — тот же дескриптор, который используется при вызове Object.defineProperties

var prototypeDef = { protoBar: «protoBar», protoLog: function () { console.log (this.protoBar); } }; var propertiesDef = { instanceBar: { value: «instanceBar» }, instanceLog: { value: function () { console.log (this.instanceBar); } } }

var foo = Object.create (prototypeDef, propertiesDef); foo.protoLog (); //logs «protoBar» foo.instanceLog (); //logs «instanceBar» jsfiddleСвойства. описанные при помощи дескриптора, перезаписывают соответствующие свойства прототипа:

var prototypeDef = { bar: «protoBar», }; var propertiesDef = { bar: { value: «instanceBar», }, log: { value: function () { console.log (this.bar); } } }

var foo = Object.create (prototypeDef, propertiesDef); foo.log (); //logs «instanceBar» jsfiddleИспользование не примитивного типа, например Array или Object в качестве значений определяемых свойств может быть ошибкой, так как эти свойства расшарятся со всеми созданными экземплярами.

var prototypeDef = { protoArray: [], }; var propertiesDef = { propertyArray: { value: [], } }

var foo = Object.create (prototypeDef, propertiesDef); var bar = Object.create (prototypeDef, propertiesDef);

foo.protoArray.push («foobar»); console.log (bar.protoArray); //logs [«foobar»] foo.propertyArray.push («foobar»); console.log (bar.propertyArray); //also logs [«foobar»] jsfiddleЭтого можно избежать, инициализировав propertyArray со значением null, после чего добавить необходимый массив, или сделать что-нибудь хипстерское, например использовать геттер:

var prototypeDef = { protoArray: [], }; var propertiesDef = { propertyArrayValue_: { value: null, writable: true }, propertyArray: { get: function () { if (! this.propertyArrayValue_) { this.propertyArrayValue_ = []; } return this.propertyArrayValue_; } } }

var foo = Object.create (prototypeDef, propertiesDef); var bar = Object.create (prototypeDef, propertiesDef);

foo.protoArray.push («foobar»); console.log (bar.protoArray); //logs [«foobar»] foo.propertyArray.push («foobar»); console.log (bar.propertyArray); //logs [] jsfiddleЭто изящный способ объединить инициализацию переменных с их определением. Я думаю, что предпочел бы выполнять определение переменных вместе с их инициализацией и это было бы гораздо лучше, чем делать то же в конструкторе. В прошлом я писал гигантский конструктор, в котором было очень много кода, выполняющего инициализацию.

Предыдущий пример демонстрирует необходимость помнить о том, что выражения, переданные любому значению в дескрипторе Object.create выполняются в момент определения дескриптора. Это — причина, по которой массивы становились общими для всех экземпляров класса. Я также рекомендую никогда не рассчитывать на фиксированный порядок, когда несколько свойств определяются вместе. Если это действительно необходимо — определить одно свойство раньше других — лучше использовать для него Object.defineProperty в этом случае.

Так как Object.create не вызывает функцию-конструктор, отпадает возможность использовать instanceof для проверки идентичности объектов. Вместо этого можно использовать isPrototypeOf, который сверяется со свойством prototype объекта. Это будет MyFunction.prototype в случае конструктора, или объект, переданный первым аргументом в Object.create

function Foo () { }

var prototypeDef = { protoArray: [], }; var propertiesDef = { propertyArrayValue_: { value: null, writable: true }, propertyArray: { get: function () { if (! this.propertyArrayValue_) { this.propertyArrayValue_ = []; } return this.propertyArrayValue_; } } }

var foo1 = new Foo ();

//old way using instanceof works with constructors console.log (foo1 instanceof Foo); //logs true

//You check against the prototype object, not the constructor function console.log (Foo.prototype.isPrototypeOf (foo1)); //true

var foo2 = Object.create (prototypeDef, propertiesDef);

//can’t use instanceof with Object.create, test against prototype object… //…given as first agument to Object.create console.log (prototypeDef.isPrototypeOf (foo2)); //true jsfiddleisPrototypeOf спускается по цепочке прототипов и возвращает true, если любой из них соответствует тому объекту, с которым происходит сравнение.

var foo1Proto = { foo: «foo», };

var foo2Proto = Object.create (foo1Proto); foo2Proto.bar = «bar»;

var foo = Object.create (foo2Proto);

console.log (foo.foo, foo.bar); //logs «foo bar» console.log (foo1Proto.isPrototypeOf (foo)); // logs true console.log (foo2Proto.isPrototypeOf (foo)); // logs true jsfiddle«Пломбирование» объектов, «заморозка» и предотвращение возможности расширения Добавление произвольных свойств случайным объектам и экземплярам класса только потому, что есть такая возможность, код, как минимум, лучше не делает. На node.js и в современных браузерах, в добавок к возможности ограничения изменений отдельных свойств при помощи defineProperty, существует возможность ограничить изменения и объекту в целом. Object.preventExtensions, Object.seal и Object.freeze — каждый из этих методов налагает более строгие ограничения на изменения в объекте. В строгом режиме нарушение ограничений, налагаемых этими методами, приведет к тому, что будет брошена ошибка, иначе же ошибки произойдут, но «тихо».

Метод Object.preventExtensions предотвращает добавление новых свойств в объект. Он не помешает ни изменить открытые для записи свойства, ни удалить те, которые являются настраиваемыми. Кроме того, Object.preventExtensions также не лишает возможности использовать вызов Object.defineProperty для того, чтобы изменять существующие свойства.

var obj = { foo: «foo», };

obj.bar = «bar»; console.log (obj); // logs Object {foo: «foo», bar: «bar»}

Object.preventExtensions (obj);

delete obj.bar; console.log (obj); // logs Object {foo: «foo»}

obj.bar = «bar»; console.log (obj); // still logs Object {foo: «foo»}

obj.foo = «foobar» console.log (obj); // logs {foo: «foobar»} can still change values jsfiddle (обратите внимание, что предыдущий jsfiddle нужно будет перезапустить с открытой консолью разработчика, т.к. в консоль могут вывестись только окончательные значения объекта)

Object.seal идет дальше. чем Object.preventExtensions. В дополнение к запрету на добавление новых свойств к объекту, этот метод также ограничивает возможности дальнейшей настройки и удаления существующих свойств. Как только объект был «опломбирован», вы больше не можете изменять существующие свойства при помощи defineProperty. Как было упомянуто выше, нарушение этих запретов в строгом режиме приведет к тому, что будет брошена ошибка.

«use strict»;

var obj = {};

Object.defineProperty (obj, «foo», { value: «foo» });

Object.seal (obj);

//Uncaught TypeError: Cannot redefine property: foo Object.defineProperty (obj, «foo», { value: «bar» }); jsfiddleВы также не можете удалять свойства даже если они были изначально настраиваемыми. Остается возможность только изменять значения свойств.

«use strict»;

var obj = {};

Object.defineProperty (obj, «foo», { value: «foo», writable: true, configurable: true, });

Object.seal (obj);

console.log (obj.foo); //logs «foo» obj.foo = «bar»; console.log (obj.foo); //logs «bar» delete obj.foo; //TypeError, cannot delete jsfiddleВ конце концов, Object.freeze делает объект абсолютно защищенным от изменений. Нельзя добавить, удалить или изменить значения свойств замороженного «объекта». Также нет никакой возможности воспользоваться Object.defineProperty с целью изменить значения существующих свойств объекта.

«use strict»;

var obj = { foo: «foo1» };

Object.freeze (obj);

//All of the following will fail, and result in errors in strict mode obj.foo = «foo2»; //cannot change values obj.bar = «bar»; //cannot add a property delete obj.bar; //cannot delete a property //cannot call defineProperty on a frozen object Object.defineProperty (obj, «foo», { value: «foo2» }); jsfiddleМетоды позволяющие проверить является ли объект «замороженным», «опломбированным» или защищенным от расширения следующие: Object.isFrozen, Object.isSealed и Object.isExtensible

valueOf и toString Можно использовать valueOf и toString для настройки поведения объекта в контексте, когда JavaScript ожидает получить примитивное значение.

Вот пример использования toString:

function Foo (stuff) { this.stuff = stuff; }

Foo.prototype.toString = function () { return this.stuff; }

var f = new Foo («foo»); console.log (f + «bar»); //logs «foobar» jsfiddleИ valueOf:

function Foo (stuff) { this.stuff = stuff; }

Foo.prototype.valueOf = function () { return this.stuff.length; }

var f = new Foo («foo»); console.log (1 + f); //logs 4 (length of «foo» + 1); jsfiddleСоединив использование этих двух методов можно получить неожиданный результат:

function Foo (stuff) { this.stuff = stuff; }

Foo.prototype.valueOf = function () { return this.stuff.length; }

Foo.prototype.toString = function () { return this.stuff; }

var f = new Foo («foo»); console.log (f + «bar»); //logs »3bar» instead of «foobar» console.log (1 + f); //logs 4 (length of «foo» + 1); jsfiddleПравильный способ использовать toString это сделать объект хэшируемым:

function Foo (stuff) { this.stuff = stuff; }

Foo.prototype.toString = function () { return this.stuff; }

var f = new Foo («foo»);

var obj = {}; obj[f] = true; console.log (obj); //logs {foo: true} jsfiddlegetOwnPropertyNames и keys Для того, чтобы получить все свойства объекта, можно использовать Object.getOwnPropertyNames. Если вы знакомы с python, то он, в общем, аналогичен методу keys словаря, хотя метод Object.keys также существует. Основная разница между Object.keys и Object.getOwnPropertyNames в том, что последний также возвращает «неперечисляемые» свойства, те, которые не будут учитываться при работе цикла for in.

var obj = { foo: «foo», };

Object.defineProperty (obj, «bar», { value: «bar» });

console.log (Object.getOwnPropertyNames (obj)); //logs [«foo», «bar»] console.log (Object.keys (obj)); //logs [«foo»] jsfiddleSymbol Symbol это специальный новый примитив, определенный в ECMAScrpt 6 harmony, и он будет доступен в следующей итерации JavaScript. Его уже сейчас можно попробовать в Chrome Canary и Firefox Nightly и следующие примеры на jsfiddle будут работать только в этих браузерах, по крайней мере на время написания этого поста, в августе 2014.

Symbol могут быть использованы как способ создать и ссылаться на свойства объекта

var obj = {};

var foo = Symbol («foo»);

obj[foo] = «foobar»;

console.log (obj[foo]); //logs «foobar» jsfiddleSymbol уникален и является неизменным

//console logs false, symbols are unique: console.log (Symbol («foo») === Symbol («foo»)); jsfiddleSymbol можно использовать вместе с Object.defineProperty:

var obj = {};

var foo = Symbol («foo»);

Object.defineProperty (obj, foo, { value: «foobar», });

console.log (obj[foo]); //logs «foobar» jsfiddleСвойства, определенные при помощи Symbol не будут итерироваться в цикле for in, однако вызов hasOwnProperty сработает нормально:

var obj = {};

var foo = Symbol («foo»);

Object.defineProperty (obj, foo, { value: «foobar», });

console.log (obj.hasOwnProperty (foo)); //logs true jsfiddleSymbol не попадет в массив, возвращаемый функцией Object.getOwnPropertyNames, но зато есть метод Object. getOwnPropertySymbols

var obj = {};

var foo = Symbol («foo»);

Object.defineProperty (obj, foo, { value: «foobar», });

//console logs [] console.log (Object.getOwnPropertyNames (obj));

//console logs [Symbol (foo)] console.log (Object.getOwnPropertySymbols (obj)); jsfiddleИспользование Symbol может быть удобным в случае, если вы хотите не только защитить свойство от случайного изменения, но вы даже не хотите его показывать в ходе обычной работы. Я пока не задумывался всерьёз над всеми потенциальными возможностями, но считаю, что их ещё может быть гораздо больше.

Proxy Ещё одно нововведение в ECMAScript 6 это Proxy. Состоянием на август 2014 года прокси работают только в Firefox. Следующий пример с jsfiddle будет работать только в Firefox и, фактически, я тестировал его в Firefox beta, который был у меня установлен.

Я нахожу прокси восхитительными, потому что они дают возможность подхватить все свойства, обратите внимание на пример:

var obj = { foo: «foo», }; var handler = { get: function (target, name) { if (target.hasOwnProperty (name)) { return target[name]; } return «foobar»; }, }; var p = new Proxy (obj, handler); console.log (p.foo); //logs «foo» console.log (p.bar); //logs «foobar» console.log (p.asdf); //logs «foobar» jsfiddleВ этом примере мы проксируем объект obj. Мы создаем объект handler, который будет обрабатывать взаимодействие с создаваемым объектом. Метод обработчика get довольно прост. Он принимает объект и имя свойства, к которому осуществляется доступ. Эту информацию можно возвращать когда угодно, но в нашем примере возвращается фактическое значение, если ключ есть и «foobar», если его нет. Я вижу огромное поле возможностей и интересных способов использования прокси, один из которых немного похож на switch, такой, как в Scala.

Ещё одна область применения для прокси это тестирование. Кроме get есть ещё и другие обработчики: set, has, прочие. Когда прокси получат поддержку получше, я не задумываясь уделю им целый пост в своем блоге. Советую посмотреть документацию MDN по прокси и обратить внимание на приведенные примеры.Кроме прочего есть ещё и отличный с доклад с jsconf о прокси, который я очень рекомендую: видео | слайды

Существует много способов использовать объекты в JavaScript более глубоко, чем просто хранилище случайных данных. Уже сейчас доступны мощные способы определения свойств, а в будущем нас ждет, как вы можете убедиться, подумав о том, как прокси может изменить способ написания кода на JavaScript, ещё много интересного. Если у вас есть какие-либо уточнения или замечания, дайте пожалуйста мне знать об этом, вот мой твиттер: @bjorntipling.

© Habrahabr.ru