Этот пост выходит за рамки повседневного использования объектов в 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
Используя геттер можно упростить этот код.
/**
* @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"
Код остается немного избыточным, а синтаксис — немного непривычным, однако преимущества применения 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;
}
});
Поведение этого кода такое же как и в предыдущем примере. Вместо добавления геттеров и сеттеров, предпочтение отдается 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
Результат:
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
Ключ 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
Если 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"
Также необходимо обратить внимание на то, что значения, определенные при помощи 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]);
}
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]);
}
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
Вызов 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"
Object.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"
Свойства. описанные при помощи дескриптора, перезаписывают соответствующие свойства прототипа:
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"
Использование не примитивного типа, например 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"]
Этого можно избежать, инициализировав 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 []
Это изящный способ объединить инициализацию переменных с их определением. Я думаю, что предпочел бы выполнять определение переменных вместе с их инициализацией и это было бы гораздо лучше, чем делать то же в конструкторе. В прошлом я писал гигантский конструктор, в котором было очень много кода, выполняющего инициализацию.
Предыдущий пример демонстрирует необходимость помнить о том, что выражения, переданные любому значению в дескрипторе 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
isPrototypeOf
спускается по цепочке прототипов и возвращает 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
«Пломбирование» объектов, «заморозка» и предотвращение возможности расширения
Добавление произвольных свойств случайным объектам и экземплярам класса только потому, что есть такая возможность, код, как минимум, лучше не делает. На 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 нужно будет перезапустить с открытой консолью разработчика, т.к. в консоль могут вывестись только окончательные значения объекта)
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"
});
Вы также не можете удалять свойства даже если они были изначально настраиваемыми. Остается возможность только изменять значения свойств.
"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
В конце концов, 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"
});
Методы позволяющие проверить является ли объект «замороженным», «опломбированным» или защищенным от расширения следующие:
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"
И 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);
Соединив использование этих двух методов можно получить неожиданный результат:
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);
Правильный способ использовать 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}
getOwnPropertyNames и 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"]
Symbol
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"
Symbol
уникален и является неизменным
//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));
Symbol
можно использовать вместе с Object.defineProperty
:
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
console.log(obj[foo]); //logs "foobar"
Свойства, определенные при помощи Symbol
не будут итерироваться в цикле for in
, однако вызов hasOwnProperty
сработает нормально:
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
console.log(obj.hasOwnProperty(foo)); //logs true
Symbol
не попадет в массив, возвращаемый функцией 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));
Использование 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"
В этом примере мы проксируем объект obj
. Мы создаем объект handler
, который будет обрабатывать взаимодействие с создаваемым объектом. Метод обработчика get
довольно прост. Он принимает объект и имя свойства, к которому осуществляется доступ. Эту информацию можно возвращать когда угодно, но в нашем примере возвращается фактическое значение, если ключ есть и «foobar», если его нет. Я вижу огромное поле возможностей и интересных способов использования прокси, один из которых немного похож на switch
, такой, как в Scala
.
Ещё одна область применения для прокси это тестирование. Кроме get
есть ещё и другие обработчики: set
, has
, прочие. Когда прокси получат поддержку получше, я не задумываясь уделю им целый пост в своем блоге. Советую посмотреть документацию MDN по прокси и обратить внимание на приведенные примеры.
Кроме прочего есть ещё и отличный с доклад с jsconf о прокси, который я очень рекомендую: видео | слайды
Существует много способов использовать объекты в JavaScript более глубоко, чем просто хранилище случайных данных. Уже сейчас доступны мощные способы определения свойств, а в будущем нас ждет, как вы можете убедиться, подумав о том, как прокси может изменить способ написания кода на JavaScript, ещё много интересного. Если у вас есть какие-либо уточнения или замечания, дайте пожалуйста мне знать об этом, вот мой твиттер: @bjorntipling.
Автор: benjie