Просунуте використання об'єктів 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

Щоб дозволити це, необхідно використовувати властивість
перечіслімого


var i, inventory;

inventory = {
"apples": 10,
"oranges": 13,
};

Object.defineProperty(inventory, "strawberries", {
value: 3,
перечіслімого: 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"

jsfiddle

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"

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 function constructor
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

jsfiddle

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

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}

jsfiddle

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"]

jsfiddle

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"

jsfiddle

Symbol
унікальний і є незмінним

//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));

jsfiddle

Symbol
можна використовувати разом з
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

jsfiddle

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));

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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.