in ECMAScript

Тонкости ECMA-262-5. Часть 1. Свойства и дескрипторы свойств

Read this article in: English.

Введение

Эта глава посвященна одной из новых концепций в спецификации ECMA-262-5 — атрибутам свойств и механизму работы с ними, дескрипторам свойств.

Обычно, говоря, что объект имеет какие-то свойства, мы имеем ввиду связь между именем свойства и его значением. Но, как мы знаем из анализа ES3, структура свойства несколько сложнее, чем просто строковое имя. Оно также имеет набор атрибутов — те, которые мы уже обсудили в статьях об ES3, например {ReadOnly}, {DontEnum} и другие. С этой точки зрения свойство само по себе является объектом.

Для полного понимания этой главы рекомендуется прочитать главу 7.2. ООП: Реализация в ECMAScript из серии статей об ECMA-262-3.

Новые методы API

Для работы со свойствами и их атрибутами ES5 вводит несколько новых API методов, которые мы обсудим детально:

// улучшенное прототипное наследование
Object.create(parentProto, properties);

// получение прототипа
Object.getPrototypeOf(o);

// объявление свойств со специальными атрибутами
Object.defineProperty(o, propertyName, descriptor);
Object.defineProperties(o, properties);

// анализ свойств
Object.getOwnPropertyDescriptor(o, propertyName);

// статические (или “замороженные”) объекты
Object.freeze(o);
Object.isFrozen(o);

// нерасширяемые объекты
Object.preventExtensions(o);
Object.isExtensible(o);

// “запечатанные”: нерасширяемые
// и неконфигурируемые объекты
Object.seal(o);
Object.isSealed(o);

// список свойств
Object.keys(o);
Object.getOwnPropertyNames(o);

Но давайте все по порядку.

Виды Свойств

В ES3 у нас была только прямая связь между именем свойства и его значением. Хотя, некоторые реализации имея свои собственные расширения, предоставляли концепцию геттеров и сеттеров в ES3. ECMA-262-5 стандартизирует эту концепцию, и сейчас мы имеем в своём распоряжении три вида свойств.

Также мы знаем, что свойство может быть либо собственным, т.е. находящимся непосредственно в объекте, так и унаследованным, т.е. находящимся в одном из объектов в цепи прототипов.

Существуют именованные свойства, которые доступны для ECMAScript программы и внутренние свойства, которые доступны только на уровне реализации (однако есть возможность управлять некоторыми из них в программе ECMAScript через специальные методы). Мы их вскоре обсудим.

Атрибуты свойств

Именованные свойства различаются набором атрибутов. Рассмотренные ранее в серии статей об ES3, такие атрибуты свойств как {ReadOnly}, {DontEnum} и другие, в ES5 были переименованы с обратным булевым состоянием. При этом, существуют два общих атрибута для обоих видов (данные и аксессоры) именованных свойств в ECMA-262-5:

  • [[Enumerable]]

    атрибут (является обратным состоянием {DontEnum} в ES3) определяет в истинном состоянии, что свойство доступно для перечисления при помощи for-in цикла.

  • [[Configurable]]

    атрибут (в ES3 — обратное состояние {DontDelete}) в ложном состоянии делает невозможным удаление свойства, измение его типа или изменение его атрибутов (отличные от [[Value]]).

Обратите внимание, что если атрибут [[Configurable]] был определен как false, он не может быть изменен обратно в true. Как было уже сказано, мы не сможем даже изменить другие атрибуты, например [[Enumerable]]. Однако, мы сможем изменить атрибут [[Value]] и атрибут [[Writable]], но только с true в false (обратное не верно: если [[Writable]] был уже определен как false, он не может быть возвращен в true в неконфигурируемом состоянии).

Мы рассмотрим остальные атрибуты свойств, специфичные для соответствующих видов свойств несколько позже, а пока, давайте разберем виды свойств.

Именованные свойства-данные

Это те своства, которые мы уже успешно использовали в ES3. Такие свойства имеют имя (которое всегда является строкой) и напрямую ассоциированное с этим именем значение.

Например:

// определяем в декларативной форме
var foo = {
 bar: 10 // явное значение свойства типа Number
};

// определяем в императивной форме,
// также явное значение, однако тип уже Function и свойство является методом
foo.baz = function () {
 return this.bar;
};

Также как и в ES3, в случае, когда значением свойства является функция, такое свойство называется методом. Однако, эти явные функции не путать с аксессорами, которые будут рассмотрены ниже.

В дополнение к базовым атрибутам именованных свойств, свойство данных имеет следующие атрибуты:

  • [[Value]]

    атрибут определяющий значение возвращаемое при чтении свойства.

  • [[Writable]]

    атрибут (обратное состояние {ReadOnly} в ES3) в ложном состоянии делает невозможным изменение атрибута свойства [[Value]] используя внутренний метод [[Put]].

Полная карта атрибутов для именованных свойств данных со значениями по умолчанию является следующей:

var defaultDataPropertyAttributes = {
 [[Value]]: undefined,
 [[Writable]]: false,
 [[Enumerable]]: false,
 [[Configurable]]: false
};

Таким образом в изначальном состоянии свойства — это константы:

// определяем глобальную константу
Object.defineProperty(this, "MAX_SIZE", {
 value: 100
});

console.log(MAX_SIZE); // 100

MAX_SIZE = 200; // ошибка в строгом режиме, [[Writable]] = false,
delete MAX_SIZE; // ошибка в строгом режиме, [[Configurable]] = false

console.log(MAX_SIZE); // все еще 100

К сожалению, в ES3 у нас не было какого-либо контроля над атрибутами свойств, что приводило к известной проблеме с изменением встроенных прототипов. Из-за динамической изменяющейся природы объектов в ECMAScript можно легко добавить новую функциональность и использовать делегирующие свойства так, как будто они являются “собственными” для объекта. Но без контроля, например над атрибутом {DontEnum} в ES3 мы получали проблему, при которой это свойство появляется в циклах for-in:

// ES3
Array.prototype.sum = function () {
 // реализация sum
};

var a = [10, 20, 30];

// работает хорошо
console.log(a.sum()); // 60

// но из-за того что for-in также проверяет
// цепочку прототипов новое совойство “sum”
// также будет перечислено, потому что
// имеет {DontEnum} == false

// проходим по свойствам
for (var k in a) {
 console.log(k); // 0, 1, 2, sum
}

ES5 предлагает такой контроль при помощи мета-методов для манипуляции свойствами объекта:

Object.defineProperty(Array.prototype, "sum", {
 value: function arraySum() {
   //  реализация sum
 },
 enumerable: false
});

// сейчас свойство “sum” неперечислимо
for (var k in a) {
 console.log(k); // 0, 1, 2
}

В примере выше мы определили атрибут enumerable вручную и явно. Однако, как уже было упомянуто, значением по умолчание всех атрибутов является false и мы можем опустить явное определение атрибутов как false.

Простой же оператор присваивания теперь соотносится с инвертированным состоянием по-умолчанию всех атрибутов (то что мы имели в ES3):

// простое присваивание (если мы сосздали новое свойство)
foo.bar = 10;

// так же как и...
Object.defineProperty(foo, "bar", {
 value: 10,
 writable: true,
 enumerable: true,
 configurable: true
});

Заметьте также, что мета-метод Object.defineProperty используется не только для создания свойств объекта, но также и для изменения его атрибутов. Более того, он возвращает измененный объект, и мы можем использовать этот метод для связки вновь созданного объекта с нужной переменной одним действием:

// создаем объект “foo” и определяем свойсво “bar”
var foo = Object.defineProperty({}, "bar", {
 value: 10,
 enumerable: true
});

// изменяем атрибуты значения и перечислимости
Object.defineProperty(foo, "bar", {
 value: 20,
 enumerable: false
});

console.log(foo.bar); // 20

Для получения собственных свойств объекта существует два мета-метода: Object.keys, который возвращает только перечислимые свойства, и Object.getOwnPropertyNames, который в свою очередь возвращает как перечислимые так и неперечислимые свойства:

var foo = {bar: 10, baz: 20};

Object.defineProperty(foo, "x", {
 value: 30,
 enumerable: false
});

console.log(Object.keys(foo)); // ["bar", "baz"]
console.log(Object.getOwnPropertyNames(foo)); // ["bar", "baz", "x"]

Именованные аксессор-свойства

Именованное аксессор-свойство ассоциирует строковое имя с одним или двумя аксессорными функициями: геттером и сеттером.

Аксессорные функции используются для косвенной записи и чтения значений, ассоциируемых со свойством.

Как мы отметили, некоторые реализации ES3 уже имели эту концепцию. Однако ES5 определяет её официально и предостваляет немного измененный синтаксис (отличный, например, от подобного расширения в SpiderMonkey).

В дополнение к базовым атрибутам, аксессорное свойство имеет следующие атрибуты:

  • [[Get]]

    атрибут является функцией, которая вызывается каждый раз, как производится попытка чтения свойства. Не путайте этот атрибут свойства с внутренним методом объекта с таким же именем, базовым методом чтения значения свойства. Так в случае аксессорного свойства, внутренний метод объекта [[Get]] вызывает атрибут свойства объекта [[Get]].

  • [[Set]]

    атрибут также является функцией, которая испольуется для записи значения свойства. Этот атрибут вызывается внутренним методом объекта [[Put]].

Обратите внимание, что эффект от вызова [[Set]] может, но не обязательно произведет изменение значения возвращаемого последующим вызовом [[Get]]. Иначе говоря, если мы сделаем set свойству, например 10, геттер может вернуть совершенно другое значение, например 20, потому что связь тут неявная.

Полная карта атрибутов для именованного свойства аксессора со значениями по умолчанию является следующей:

var defaultAccessorPropertyAttributes = {
 [[Get]]: undefined,
 [[Set]]: undefined,
 [[Enumerable]]: false,
 [[Configurable]]: false
};

Соответсвенно, если атрибут [[Set]] отсутствует, свойство может быть только прочитано — как в случае с false у атрибута [[Writable]] свойства данных.

Аксессор-свойство может быть объявленно уже упомянутым выше мета-методом Object.defineProperty:

var foo = {};

Object.defineProperty(foo, "bar", {
 get: function getBar() {
   return 20;
 },
 set: function setBar(value) {
   // реализация сеттера
 }
});

foo.bar = 10; // вызываем foo.bar.[[Set]](10)

// независимо всегда 20
console.log(foo.bar); // вызываем foo.bar.[[Get]]()

Или же декларативно, используя инициализатор объекта:

var foo = {
 get bar () {
   return 20;
 },
 set bar (value) {
   console.log(value);
 }
};

foo.bar = 100;
console.log(foo.bar); // 20

Заметьте также одно важную особенность касающуюся конфигурации аксессор-свойства. Как мы уже упоминали в описании атрибута [[Configurable]], если он был выставлен в false, свойство не может быть более переконфигурировано (кроме атрибута [[Value]] свойства данных). Это может сбить с толку, например в следующем случае:

// configurable false по умолчанию
var foo = Object.defineProperty({}, "bar", {
 get: function () {
   return "bar";
 }
});

// пытаемся переконфигурировать свойство "bar"
// получаем исключение
try {
 Object.defineProperty(foo, "bar", {
   get: function () {
     return "baz"
   }
 });
} catch (e) {
 if (e instanceof TypeError) {
   console.log(foo.bar); // все еще "bar"
 }
}

Но исключение не будет выброшено если преконфигурируемое значение атрибута останется прежним. На приктике, однако, это не особо является полезным, т.к. мы фактически не переконфигурируем геттер и он остается прежним:

function getBar() {
 return "bar";
}

var foo = Object.defineProperty({}, "bar", {
 get: getBar
});

// никакого исключения если configurable равен false
// но на практике такое “переконфигурирование” бесполезно
Object.defineProperty(foo, "bar", {
 get: getBar
});

И как уже упоминалось, аттрибут [[Value]] свойства данных может быть переконфигурирован даже, если [[Configurable]] равен false. Конечно атрибут [[Writable]] должен быть при этом равен true. Также атрибут [[Writable]] в состоянии true может быть выставлен в false, но не наоборот для неконфигурируемых свойств:

var foo = Object.defineProperty({}, "bar", {
 value: "bar",
 writable: true,
 configurable: false
});

Object.defineProperty(foo, "bar", {
 value: "baz"
});

console.log(foo.bar); // "baz"

// изменяем writable
Object.defineProperty(foo, "bar", {
 value: "qux",
 writable: false // изменено с true в false
});

console.log(foo.bar); // "qux"

// пытаемся изменить writable еще раз обратно в true
Object.defineProperty(foo, "bar", {
 value: "qux",
 writable: true // ОШИБКА
});

Также мы не сможем изменить тип свойства из свойства данных в свойство аксессора и наоборот, если его атрибут [[Configurable]] находится в false. В истинном состоянии атрибута [[Configurable]] такое изменение возможно. Таким образом, состояние атрибута [[Writable]] не важно и может быть false:

// writable ложно по умолчанию
var foo = Object.defineProperty({}, "bar", {
 value: "bar",
 configurable: true
});

Object.defineProperty(foo, "bar", {
 get: function () {
   return "baz";
 }
});

console.log(foo.bar); // OK, "baz"

Другой очевидный факт заключается в том, что свойство не может быть одновременно двух типов. Это означает, что присутствие каких либо специализированных атрибутов, не принадлежащих текущему типу свойства, вызывает исключение:

// ошибка, “get” и “writable” одновременно
var foo = Object.defineProperty({}, "bar", {
 get: function () {
   return "baz";
 },
 writable: true
});

// также ошибка из-за наличия специализированных атрибутов “value” и “set”
var baz = Object.defineProperty({}, "bar", {
 value: "baz",
 set: function (v) {}
})

Стоит также отметить, что использование геттера и сеттера оправданно и имеет смысл больше в случаях, когда нам нужно инкапсулировать некоторые сложные вычисления со вспомогательными данными и сделать использование данного свойства удобным — как если бы оно было обычным свойством данных. Мы уже отмечали этот нюанс в части посвященной инкапсуляции на примере свойства element.innerHTML, где мы абстрактно говорим при присваивании, что “свойство html этого элемента сейчас следующее“, в то время как сеттер функция для свойства innerHTML содержит в себе сложные вычисления и проверки, которые приводят к перестройке дерева объектов DOM и обновления пользовательского интерфейса.

Для сущностей, не требующих дополнительной абстракции, геттеры и сеттеры могут быть не столь полезны. Например:

var foo = {};
Object.defineProperty(foo, "bar", {
 get: function getBar() {
   return this.baz;
 },

 set: function setBar(value) {
   this.baz = value;
 }
});

foo.bar = 10;

console.log(foo.bar); // 10
console.log(foo.baz); // 10

Мы не только используем аксессоры для неабстрактной сущности, но также создали собственное свойство объекта baz. В данном случае можно использовать простое свойство данных, что улучшит производительность.

Случаими, оправдывающими использование аксессоров являются абстракция и инкапсуляция вспомогательных данных. Простой пример:

var foo = {};

// энкапсулированный контекст
(function () {
 // какое-то внутреннее состояние
 var data = [];
 Object.defineProperty(foo, "bar", {
   get: function getBar() {
     return "We have " + data.length + " bars: " + data;
   },
   set: function setBar(value) {
     // вызываем геттер вначале
     console.log('Alert from "bar" setter: ' + this.bar);
     data = Array(value).join("bar-").concat("bar").split("-");
     // конечно, если требуется, мы можем обновить
     // also some public property          некоторые внешние свойства
     this.baz = 'updated from "bar" setter: ' + value;

   },
   configurable: true,
   enumerable: true
 });
})();

foo.baz = 100;
console.log(foo.baz); // 100

// первый геттер будет вызван внутри сеттера
// bar равен 3:
foo.bar = 3;

// проверяем
console.log(foo.bar); // We have 3 bars: bar, bar, bar
console.log(foo.baz); // updated from "bar" setter: 3

Данный пример не особо практичен, но он показывает главное назначение аксессоров — увеличение уровня абстракции и инкапсуляции внутренних данных.

Еще одной особенностью, связанной с аксессорами является запись в унаследованные аксессорные свойства. Как мы знаем из серии ES3, унаследованные свойства доступны для чтения, но не для записи: при присвоении обычным свойствам, всегда создается собственное свойство:

Object.prototype.x = 10;
var foo = {};

// читаем унаследованное свойство
console.log(foo.x); // 10

// но при присвоении
// создается всегда собственное свойство объекта
foo.x = 20;

// читаем собственное свойство
console.log(foo.x); // 20
console.log(foo.hasOwnProperty("x")); // true

В противоположность свойствам данных, унаследованные аксессоры доступны для модификации через присвоение свойству наследующего объекта:

var _x = 10;

var proto = {
 get x() {
   return _x;
 },
 set x(x) {
   _x = x;
 }
};

console.log(proto.hasOwnProperty("x")); // true

console.log(proto.x); // 10

proto.x = 20; // задаем собственное свойство

console.log(proto.x); // 20

var a = Object.create(proto); // "a" наследуется от "proto"

console.log(a.x); // 20, читаем унаследованное "x"

a.x = 30; // задаем *унаследованное* свойство

console.log(a.x); // 30
console.log(proto.x); // 30
console.log(a.hasOwnProperty("x")); //false

Однако, если мы создадим объект а наследуемый от proto со своим свойством x, присвоение, конечно, повлияет на собственное свойство объекта:

var a = Object.create(proto, {
 x: {
   value: 100,
   writable: true
 }
});

console.log(a.x); // 100, читаем собственное свойство

a.x = 30; // задаем также собственное

console.log(a.x); // 30
console.log(proto.x); // 20
console.log(a.hasOwnProperty("x")); // true

Тот же самый результат мы получим, если зададим собственное свойство используя мета-метод, а не оператор присваивания:

var a = Object.create(proto);

a.x = 30; // меняем унаследованное свойство

Object.defineProperty(a, "x", {
 value: 100,
 writable: true
});

a.x = 30; // меняем собственное свойство

Также стоит упомянуть то, что, если мы попытаемся скрыть посредством присвоения незаписываемое унаследованное свойство в строгом режиме (strict mode), мы получим исключение TypeError. Это произойдет независимо от того, является ли свойство аксессором или данным. Однако, если мы скроем свойство не через присвоение, а через Object.defineProperty, исключения не будет:

"use strict";

var foo = Object.defineProperty({}, "x", {
 value: 10,
 writable: false

});

// "bar" наследуется от "foo"
var bar = Object.create(foo);

console.log(bar.x); // 10, inherited   10, унаследовали

// пытаемся скрыть свойство “х”
// и получаем ошибку в строгом
// режиме, или просто тихий сбой
// исполнения в нестрогом ES5 или ES3

bar.x = 20; // TypeError

console.log(bar.x); // все еще 10, если нестрогий режим

// однако скрытие сработает если
// использовать Object.defineProperty
Object.defineProperty(bar, "x", { // OK
 value: 20
});

console.log(bar.x); // а сейчас 20

Про строгий режим читайте в следующей главе серии ES5.

Внутренние свойства

Внутренние свойства не являются частью языка ECMAScript. Они определены спецификаций больше для объяснения поведения внутренних операторов. Мы их уже рассматривали в отношении спецификации ES3.

ES5 предоставляет дополнительные внутренние свойства. Вы можете найти детальное описание всех этих свойств в секции 8.6.2 спецификации ECMA-262-5. Поскольку мы уже обсудили эту концепцию в статье посвященной ES3, здесь мы затроним только некоторые новые свойства.

Например, объект в ES5 может быть запечатан, заморожен или просто стать нерасширяемым, т.е. статическим. Со всеми этими состояниями связано внутреннее свойство [[Extensible]]. Оно управляется с помощью специальных мета-метадов:

var foo = {bar: 10};

console.log(Object.isExtensible(foo)); // true

Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false

foo.baz = 20; // ошибка в строгом режиме
console.log(foo.baz); // undefined

Заметьте, что внутреннее свойство [[Extensible]] однажды установленное в false, не можеть быть возвращено в true.

Но даже из такого нерасширяемого объекта некоторые свойства возможно удалить. Чтобы это предотвратить используется мета-метод Object.seal, который кроме внутреннего свойства [[Extensible]] также выставляет в false атрибут [[Configurable]] всех свойств объкта:

var foo = {bar: 10};

console.log(Object.isSealed(foo)); // false

Object.seal(foo);
console.log(Object.isSealed(foo)); // true

delete foo.bar; // ошибка в строгом режиме
console.log(foo.bar); // 10

Если мы хотим сделать объект полностью статичным, т.е. заморозить его, предотвратив тем самым изменение существующих свойств, мы можем использовать соответсвующий мета-метод Object.freeze. Этот метод кроме уже упомянутых атрибута [[Configurable]] и внутреннего свойства [[Extensible]] выставляет атрибут [[Writable]] в false всех свойств объекта:

var foo = {bar: 10};

print(Object.isFrozen(foo)); // false

Object.freeze(foo);
print(Object.isFrozen(foo)); // true

delete foo.bar; // error in strict mode
foo.bar = 20; // error in strict

print(foo.bar); // 10

Как запечатанное так и замороженное состояния необратимы.

Так же как и в ES3 у нас есть возможность исследовать внутреннее свойство [[Class]] через стандартный метод Object.prototype.toString:

var getClass = Object.prototype.toString;

console.log(
 getClass.call(1), // [object Number]
 getClass.call({}), // [object Object]
 getClass.call([]), // [object Array]
 getClass.call(function () {}) // [object Function]
 // etc.
);

ECMA-262-5 также предоставляет возможность прочитать внутреннее свойство [[Prototype]] через мета-метод Object.getPrototypeOf. Также в текущей версии спецификации мы можем создать объект на основе прототипа используя метод Object.create:

// создаем объект “foo” с двумя собственными
// свойствами “sum” и “length” которые в качестве
// значения свойства [[Prototype]] имеют Array.prototype
var foo = Object.create(Array.prototype, {
 sum: {
   value: function arraySum() {
     // реализация sum
   }
 },

 // неперчеслимое но изменяемое!
 // иначе методы массива не будут работать
 length: {
   value: 0,
   enumerable: false,
   writable: true
 }
});

foo.push(1, 2, 3);

console.log(foo.length); // 3
console.log(foo.join("-")); "1-2-3"

// ни “sum” ни “length” не перечислимы
for (var k in foo) {
 console.log(k); // 0, 1, 2
}

// получаем прототип “foo”
var fooPrototype = Object.getPrototypeOf(foo);

console.log(fooPrototype === Array.prototype); // true

Но к сожалению используя только такой подход мы все еще не можем наследовать от Array.prototype со всей функциональностью обычного массива, включая перегруженные методы, обрабатывающие, например, свойство length:

foo[5] = 10;
console.log(foo.length); // still 3

По-прежнему единственным способом унаследовать от Array.prototype, и в тоже время иметь все связанные с ним прегруженные внутренние методы, является использование обычного массива (т.е. объекта, [[Class]] которого равен "Array") и нестандартного свойство __proto__. Это доступно не всем реализациям:

var foo = [];
foo.__proto__= {bar: 10};
foo.__proto__.__proto__= Array.prototype;

console.log(foo instanceof Array); // true

console.log(foo.bar); // 10

console.log(foo.length); // 0

foo.push(20);

foo[3] = 30;
console.log(foo.length); //4

console.log(foo); // 20,,,30

foo.length = 0;
console.log(foo); // пустой массив

И все же, ES5 не предоставляет возможностей для прямого определения прототипа объекта, кроме как через нестандартное свойство __proto__ некоторых реализаций языка.

Дескриптор свойств и типы идентификаторов свойств

Как мы видели, ES5 позволяет контролировать атрибуты свойств. Набор атрибутов свойств и их значений называется в ES5 дескриптором свойств.

Связанный с соответствующим именованным свойством дескриптор может быть как дескриптором свойства данных так и дескриптором аксессор-свойства.

Спецификация также определяет концепт базового дескриптора свойства, т.е. такого дескриптора который не принадлежит ни к одному типу свойств. И также полностью заполненный дескриптор свойства, который либо дескриптор аксессора либо, дескриптор данных и содержит все поля, относящиеся к атрибутам свойства. Но это больше относится у уровню реализации.

Таким образом, из-за указанных значений атрибутов по умолчанию, если дескриптор пустой, будет создано свойство данных. Очевидно, что свойство данных также создается, если дескриптор содержит либо writable либо value поля. В случае если дескриптор имеет либо get или set поля, будет создано аксессор-свойство. Для получения дескриптора свойства объекта используется мета-метод Object.getOwnProperyDescriptor:

// определяем несколько свойств сразу
Object.defineProperties(foo, {
 bar: {}, // "empty" descriptor, пустой дескриптор
 baz: {get: function () {}}
});

var barProperty = Object.getOwnPropertyDescriptor(foo, "bar");
var hasOwn = Object.prototype.hasOwnProperty;

console.log(
 barProperty.value, // undefined
 hasOwn.call(barProperty, "value"), // true

 barProperty.get, // undefined
 hasOwn.call(barProperty, "get"), // false

 barProperty.set, // undefined
 hasOwn.call(barProperty, "set"), // false
);

console.log(foo.bar); // undefined (correct), in Rhino 1.73 - null
console.log(foo.nonExisting); // undefined and in Rhino too

// in contrast "baz" property is an accessor property
var bazProperty = Object.getOwnPropertyDescriptor(foo, "baz");

console.log(
 bazProperty.value, // undefined
 hasOwn.call(bazProperty, "value"), // false

 bazProperty.get, // function
 hasOwn.call(bazProperty, "get"), // true

 bazProperty.set, // undefined
 hasOwn.call(bazProperty, "set"), // false
);

Тип Идентификатора Свойства используется для связи имени свойства и его дескриптора. Поэтому свойства, будучи значениями типа Идентификатор Свойства, представляют собой пару в виде (имя, дескриптор):

Абстрактно:

foo.bar = 10;

// свойство это объект типа Идентификатор Свойства
var barProperty = {
 name: "bar",
 descriptor: {
   value: 10,
   writable: true,
   enumerable: true,
   configurable: true
 }
};

Заключение

В этой первой статье мы ближе познакомились с одной из концепций спецификации ECMA-262-5. Следующая глава будет посвящена новым деталям, касающимся контекстов исполнения, таким как Лексическое Окружение, Записей Окружений и другим. Как всегда, если у вас есть вопросы или дополнения мы будем рады обсудить их в блоке комментариев.

Дополнительная литература

Авторы перевода: Артём Гончаров и Дмитрий Сошников
Дата перевода: 29.05.2013

Автор оригинала: Дмитрий Сошников [англ., читать »]
Дата публикации: 28.04.2010

Write a Comment

Comment

  1. Здравствуйте Дмитрий – появился вопрос.

    Object.defineProperty(Array.prototype,'sum',{
        value:"str",
        enumerable: true
    });

    Выполнив код что выше, я начал дальше пробовать атрибуты свойств в действии и следующими были такие строки кода:

    var obj = {
      param1: "1",
      param2: "2"
    };
    console.log(Object.keys(obj)); //["param1", "param2", sum: "str"]
    

    Почему подобное произошло – ведь мы записывали в Array.prototype свойcтво sum?

  2. @Viktor Soroka

    Это произошло из-за особенности работы функции console.log. Поскольку Вы объявили свойство "sum" как enumerable, оно будет выводиться в цикле for-in. И именно for-in используется при выводе объекта и, получается, массива тоже, в отладчике Chrome. А for-in учитывает цепь прототипов, и находит там это свойство.

    Чтобы подтвердить, можно вывести любой другой массив через console.log, и там тоже это свойство будет (обратите внимание на странный формат вывода – вроде массив, а вроде и объект):

    console.log([1]); // [1, sum: "str"]
    

    Вероятно, это сделано, чтобы предупредить разработчиков о потенциальной ошибке, но, возможно, это просто баг console.log (я рекомендую сообщить разработчикам).

    Object.keys же возращает правильное значение:

    console.log(Object.keys(obj).toString()); // "param1,param2"
    
  3. Спасибо Дмитрий за оперативный ответ.