CoffeeScript: Методы для работы с объектами

в 6:30, , рубрики: coffeescript, ECMAScript, javascript, ruby, web-разработка, Веб-разработка, классы, объекты, ооп, руководство, метки: , , , , , , , ,

CoffeeScript Object methods

В 5-й редакции ECMAScript для работы с объектами появилось много новых методов, однако их подробное описание и внутреннюю реализацию на русском языке (зачастую и на английском) найти не так просто. Именно по этой причине в этой статье будут подробно рассмотрены и описаны все методы объекта Object согласно 3-й и 5-й редакции ECMAScript спецификации.

Содержание

1. Object.create(proto [, properties ])
2. Object.defineProperty(object, property, descriptor)
3. Object.defineProperties(object, properties)
4. Object.getOwnPropertyDescriptor(object, properties)
5. Object.keys(object)
6. Object.getOwnPropertyNames(object)
7. Data descriptor
8. Accessor descriptor
9. Object.getPrototypeOf(object)
10. Object.preventExtensions(object)
11. Object.isExtensible(object)
12. Object.seal(object)
13. Object.isSealed(object)
14. Object.freeze(object)
15. Object.deepFreeze(object) (non-standard)
16. Object.prototype.hasOwnProperty(property)
17. Object.prototype.isPrototypeOf(object)
18. Object.prototype.propertyIsEnumerable(object)
19. Заключение

Object.create(proto [, properties ])

Object.create() создает новый объект с заданным объектом-прототипом и свойствами:

object = Object.create property: 1
object.property # 1

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

Рассмотрим пример:

object = Object.create {},
	property:
		value: 1

object.property # 1

В этом примере первым параметром мы задали пустой объект, который будет являться объектом-прототипом для свойств определенных во втором параметре.

Пусть вас не смущает загадочное свойство value, чуть позже мы рассмотрим эту тему более подробно.

Так в чем же отличия между первым и вторым вариантом?
Думаю что ответ на этот вопрос сможет прояснить следующий пример:

object = Object.create property: 1
object.property = 2

object.property # 2

object = Object.create {},
	property:
		value: 1

object.property = 2
object.property # 1

Какого черта спросите вы?
Просто, вторая форма записи по-умолчанию подразумевает, что свойства доступны только для чтения, их нельзя удалить и они не могут быть перечислены!

Давайте рассмотрим пример:

object = Object.create {}
	property:
		value: 1

object.property = 2
object.property # 1

object.method = -> object.property
do object.method # 1

for property of object
property # method

delete object.property # false

Как видите, у нас не получилось ни задать новое значение свойству, ни перечислить, ни удалить.

А сейчас я бы хотел обратить внимание на то, как с помощью Object.create() осуществляется прототипное наследование:

A =
	a: 1
	b: 2

B = Object.create A, c:
	value: 3
	enumerable: on

for own key, value of B
	console.log " # {key}: # {value}" # c:3, a:1, b:2

В этом примере мы реализовали наследование свойств от объекта A, и добавили атрибут enumerable, для того чтобы свойство определенное в объекте B можно было перечислить в цикле for-of.

Хочу заметить, что наличие оператора own в инструкции for-of позволяет избежать использования метода hasOwnProperty в теле цикла и не допустить проверки свойств в цепочке прототипов объекта.
Более подробное описание этого оператора можно найти в моей предыдущей статье.

Если вас интересует как устроен метод create() изнутри, то в качестве задания на закрепление прочитанного привожу его реализацию согласно спецификации:

Object.create = (object, properties) ->
	# 1. If Type(O) is not Object or Null throw a TypeError exception.
	if typeof object is not 'object'
		throw new TypeError "Object.create: # {object.toString()} is not an Object or Null"

	# 2. Let obj be the result of creating a new object as if by the expression new Object()
	# where Object is the standard built-in constructor with that name
	object = new Object

	# 3. Set the [[Prototype]] internal property of obj to O.
	object.constructor:: = properties

	# 4. If the argument Properties is present and not undefined, add own properties
	# to obj as if by calling the standard built-in function Object.defineProperties
	# with arguments obj and Properties.
	if typeof props is not 'undefined'
		Object.defineProperties object, props

	# 5. Return obj.
	object

Object.defineProperty(object, property, descriptor)

Метод Object.defineProperty() позволяет определить новое свойство объекта и/или модифицировать атрибуты существующего свойства. В качестве результата возвращается новый объект.

object — объект для которого нужно определить свойства
property — имя свойства, которое должно быть определено или модифицировано
descriptor — дескриптор

Рассмотрим пример:

object = {}
Object.defineProperty object, 'property'
	value: 1

При этом, наверняка у вас уже возникли вопросы относительно использования свойства value. Сейчас мы постараемся осветить этот вопрос более подробно.

С новыми методами в ECMAScript 5 появились и новые термины, в частности пользовательский дескриптор (descriptor).

Дескриптор — это простой объект (plain object), позволяющий устанавливать значения и уровень доступа для собственных свойств, а также хранить их описание.

Иными словами дескрипторы позволяют контролировать атрибуты свойств.
До появления ECMAScript 5, нам были доступны куда более скромные инструменты для работы с атрибутами объектов: Object.prototype.propertyIsEnumerable() и Object.prototype.hasOwnProperty().

Формально дескрипторы делятся на три вида: дескрипторы данных (Data descriptors), дескрипторы доступа (Accessor descriptors) и общие дескрипторы (Generic descriptors).
Однако последний тип дескрипторов мы рассматривать не будем, т.к. их описание носит более теоретический характер на уровне реализации нежели практический.

Data descriptor

Это свойство, создается если дескриптор пустой или дескриптор имеет один из двух атрибутов: value или writable.
За проверку наличия атрибутов отвечает внутренний метод [[IsDataDescriptor]]:

IsDataDescriptor (Descriptor):
	if Descriptor is undefined
		return off

	if !Descriptor.[[Value]] and !Descriptor.[[Writable]]
		return off

	return on

Помимо обязательных атрибутов, в дескрипторе данных есть опциональные атрибуты: configurable и enumerable.

{
	configurable: false,
	enumerable:  false
}

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

value — устанавливает значение свойства объекта.
writable — определяет возможность изменения свойства.
configurable — определяет возможность удаления свойства.
enumerable — определяет доступность перечисления свойства

Внутри реализации эти атрибуты имеют следующие имена: [[Value]], [[Writable]], [[Enumerable]], [[Configurable]].

По умолчанию, все атрибуты дескриптора данных имеют значение false, за исключением атрибута value, его значение установлено undefined.

Примечание: формально, в ECMAScript 3 пользовательские дескрипторы отсутствуют, но есть внутренние атрибуты DontEnum, ReadOnly и DontDelete.

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

Object.defineProperty {}, 'property'
	value:        1
	writable:     on
	enumerable:   on
	configurable: on

А теперь давайте представим что нам потребовалось расширить прототип Object:

Object::method = -> @

Так мы добавили новый метод в прототип Object и дальше можем его использовать.
В действительности же, такое решение имеет массу недостатков.

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

Например:

Object::my_super_method = -> @

Маловероятно что метод с таким названием когда-нибудь попадет в спецификацию.

Во-вторых, все что мы добавляем в прототип Object попадает и в другие объекты:

Object::method = -> 1
list = []

do list.method # 1

В-третьих, такой метод будет иметь значение true атрибута enumerable:

Object::method = -> 1
object = {};

i for i of object # method

Предотвратить перечисление метода method в object можно можно так:

i for own i of object

Несмотря на то что это решение вполне работает, его можно усовершенствовать еще на этапе добавления метода в прототип Object:

Object.defineProperty Object::, 'method'
	value: -> 1
	enumerable: false

i for i of object #

Как видите, method не был перечислен в инструкции for-of.

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

Accessor descriptors

Дескриптор доступа — это такой дескриптор, в котором присутствует атрибут get или set. При этом допустимо присутствие этих атрибутов вместе. Однако недопустимо присутствие атрибутов value и writable:

IsDataDescriptor (Descriptor):
	if Descriptor is undefined
		return off

	if !Descriptor.[[Get]] and !Descriptor.[[Set]]
		return off

	return on

После чего вызывается внутренний метод [[DefineOwnProperty]]
Внутри реализации атрибуты дескриптора имеют следующие имена: [[Get]] и [[Set]].

Атрибуты configurable и enumerable, также доступны и в дескрипторе данных:

property = 0
object = Object.defineProperty {}, 'property'
	get: ->
		property
	set: (value) ->
		property = value
	configurable: on
	enumerable: on

object.property = 1 # set
object.property   # 1, get

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

Object.defineProperty {}, 'property'
	get: -> 1
	value: 1

 # TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified 'value: 1"

Как и в случае с Object.create(), наследование осуществляется по тому же принципу:

A =
	a: 1
	b: 2

B = Object.defineProperty A, 'c'
	value: 3
	enumerable: on

for own key, value of B
	console.log " # {key}: # {value}" # a:1, b:2, c:3

Object.defineProperties(object, properties)

Позволяет определить новые свойства объекта и/или модифицировать атрибуты существующих свойств. В качестве результата возвращается новый объект. Иными словами Object.defineProperties() делает тоже самое что и Object.defineProperty() только со множеством свойств.

object = Object.defineProperties {},
	a:
		value: 1
		enumerable: on
	b:
		value: 2
		enumerable: on

for own key, value of object
	console.log " # {key}: # {value}" # a:1, b:2

Если имена определяемых свойств совпадают с наследуемыми, то наследуемые переопределяются:

A =
	a: 1
	b: 2

object = Object.defineProperties {}
	a:
		value: 3
		enumerable: on
	b:
		value: 4
		enumerable: on

for own key, value of object
	console.log " # {key}: # {value}" # a:3, b:4

При этом стоит помнить, что переопределить аксессор просто так не получится:

object = Object.defineProperty {}, 'property'
	get: -> 0

Object.defineProperty object, 'property'
	get: -> 1

object.property # 0

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

object = Object.defineProperty {}, 'property'
	get: -> 0
	configurable: on

Object.defineProperty object, 'property'
	get: -> 1

object.property # 1

К небольшому сожалению, IE не поддерживает этот метод ниже 9-й версии. Однако, не все так плохо, т.к. в 8-й версии есть метод Object.defineProperty(), на основе которого можно реализовать и Object.defineProperties():

Object.defineProperties = (object, properties) ->
	type = (object) ->
		Object::toString.call object is '[object Object]'

	if (!type object and !type properties)
		throw new TypeError 'Object.defineProperties(Object object, properties Object)'

	if !Object.
			return object;

	for own key, value of properties
		Object.defineProperty object, key, value
	object;

Object.getOwnPropertyDescriptor(object, properties)

Этот метод позволяет получить доступ к свойствам дескриптора.

object = {}

Object.defineProperty object, 'property'
	value:        1
	writable:     off
	enumerable:   off
	configurable: on


Object.getOwnPropertyDescriptor object, 'property'

###
{
	value:        1,
	writable:     true,
	enumerable:   true,
	configurable: true
}
###

Т.к. свойства дескриптора являются внутренними свойствами ECMAScript, Object.getOwnPropertyDescriptor() единственный метод с помощью которого можно получить такую информацию. При этом стоить заметить, что Object.getOwnPropertyDescriptor() работает только с собственными свойствами:

Object.getOwnPropertyDescriptor {}, 'valueOf' # undefined

Потому что свойство toString унаследованное и находится в цепочке прототипов:

{}.hasOwnProperty 'valueOf' # false
'valueOf' of {} # true

Как уже я отмечал метод hasOwnProperty() в отличии от оператора of проверяет только собственные свойства объекта.

Обратиться к свойству valueOf напрямую, можно так:

{}.constructor::toString # function

Для большей ясности мне бы хотелось рассмотреть следующий пример:

object = {}
object.constructor::valueOf = 1

object.valueOf # 1

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

object = {}
object.valueOf = 1
object.constructor::valueOf = 2

object.toString # 1

Как видите, если свойство объекту задается явно (own property), то его значение «перекрывает» свойство заданное через объект-прототип. Точнее, значение свойства в объекте-прототипе никуда не пропадает, оно просто не резоливится и всегда доступно напрямую:

object = {}

object.toString = 1
object.constructor::valueOf = 2

object.toString # 1
object.constructor::valueOf # 2

'valueOf' of object # true
object.hasOwnProperty 'valueOf' # false

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

object = {}

object.constructor::constructor::constructor::property = 1
object.property # 1

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

fn = (x) -> x * x

fn 2 # 4
fn::constructor 4 # 8

Object.keys(object)

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

Рассмотрим типичный пример:

Object.keys
	a: 1
	b: 2
.length # 2

Довольно часто Object.keys применяется в связке с другими методами объекта Array:

object =
	a: 1
	b: 2

Object.keys(object).filter (i) -> i if object[i] > 1 # 2

Реализация метода Object.keys() довольна простая:

Object.keys = (object) -> i for own i of object

Однако я настоятельно рекомендую всегда использовать проверку входных аргументов.
После небольших поправок типичная реализация Object.keys() должна иметь примерно такой вид:

Object.keys = (object) ->
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.keys: # {object.toString()} is not an Object"

	i for own i of object

Имплементация Object.keys() согласно спецификации:

Object.keys = (object) ->

	# 1. If the Type(O) is not Object, throw a TypeError exception
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.keys: # {object.toString()} is not an Object"

	# 2. Let count be the number of own enumerable properties of O
	count = Object.getOwnPropertyNames(object).length

	# 3. Let array be the result of creating a new Object as if by the expression new Array(n)
	# where Array is the standard built-in constructor with that name
	array = new Array count

	# 4. Let index be 0
	index = 0;

	# 5. For each own enumerable property of O whose name String is P
	for own property of object
		if !object.propertyIsEnumerable property
			continue

		# a. Call the [[DefineOwnProperty]] internal method of array with arguments
		# ToString(index), the PropertyDescriptor
		# {[[Value]]: P, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false
		# b. Increment index by 1
		Object.defineProperty array, index++,
			value: property
			writable: on
			enumerable: on
			configurable: on

	# 6. Return array
	array

Object.getOwnPropertyNames(object)

Возвращает массив, содержащий имена собственных свойств объекта.
В отличии от Object.keys(object) этот метод не учитывает значение атрибута enumerable:

object =
	a: 1
	b: 2

Object.defineProperty object, 'с'
	value: 3,
	enumerable: off,

Object.keys(object).length # 2
Object.getOwnPropertyNames(object).length # 3

Имплементация Object.getOwnPropertyNames() согласно спецификации:

Object.getOwnPropertyNames = (object) ->

	# 1. If the Type(O) is not Object, throw a TypeError exception
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.getOwnPropertyNames: # {object.toString()} is not an Object"

	# 2. Let array be the result of creating a new Object as if by the expression new Array(n)
	# where Array is the standard built-in constructor with that name
	array = new Array

	# 3. Let index be 0
	index = 0;

	# 4. For each named own property P of O
	for own name of object

		# a. Let name be the String value that is the name of P.
		# b. Call the [[DefineOwnProperty]] internal method of array with arguments
		# ToString(n), the PropertyDescriptor {[[Value]]: name, [[Writable]]: true,
		# [[Enumerable]]: true, [[Configurable]]: true}, and false.
		# c. Increment n by 1.
		Object.defineProperty array, index++,
			value: name
			writable: on
			enumerable: on
			configurable: on

	# console.log array
	# 5. Return array
	array

Object.getPrototypeOf(object)

Возвращает ссылку на свойство [[Prototype]] заданного объекта.

object = {}
Object.getPrototypeOf(object).property = 1

object.property # 1

Реализация метода:

Object.getPrototypeOf = (object) ->
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.getPrototypeOf: # {object.toString()} is not an Object"

	object.__proto__ or object.constructor::

Object.preventExtensions(object)

Блокирует расширение объекта.

object = a: 1
Object.preventExtensions object
object.b = 1

'b' of object # false

Object.getOwnPropertyDescriptor object, 'a'
 # { configurable: true, enumerable: true, value: 1, writable: true }

Особое внимание хочу обратить на то, что попытка расширить заблокированный объект с помощью методов: Object.defineProperty и Object.defineProperties приведет к выбросу исключения типа TypeError!

object = {}
Object.preventExtensions object

Object.defineProperty object, 'property'
	value: 1

# TypeError: Object.defineProperties(object, 'property', ...) is not extensible

Также, TypeError всегда выбрасывается в строгом режиме:

do ->
	'use strict'
	object = {}
	Object.preventExtensions object
	object.property = 1

 # "TypeError: object.property is not extensible

В этом случае, будьте особо внимательны, потому что если не перехватить TypeError, приложение прекратит свою работу!

Object.isExtensible(object)

Определяет доступность расширения объекта. Возвращает булево значение.

object = {}
Object.preventExtensions object
Object.isExtensible object # false

Object.seal(object)

Опечатывает свойства объекта.

Object.seal = Object.preventExtensions + {[[Configurable]]: off}

Рассмотрим пример:

object = property: 1
Object.seal object

delete object.property # false

Object.getOwnPropertyDescriptor object, 'property'
# { configurable: false, enumerable: true, value: 1, writable: true }

Object.seal() устанавливает значение false атрибуту configurable для всех свойств объекта:

Object.getOwnPropertyDescriptor(Object.seal property: 1, 'property').configurable # false

Стоит заметить, что опечатывается не сам объект, а только его свойства:

object = {}

Object.defineProperty, 'property'
	value: 1

Object.seal object

delete object.property # false
delete object # false

object.property # 1
object # Object

Как видите, удалить object не получится, потому что оператор delete [[Delete]] удаляет только свойства объекта.
Это происходит потому что на этапе трансляции в JavaScript код все переменные предваряют ключевым словом var.

Чтобы удалить сам объект нужно сделать его свойством глобального объекта global:

global.object = {} # или @object, если текущий контекст глобальный
Object.seal object
delete object

object # ReferenceError: object is not defined 'object'

Однако подобное определение объектов не является хорошей практикой, потому что object попадает в глобальную область видимости:

do ->
	global.object = {}

object # Object

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

object = {}
object = undefined

object # undefined

Если в какой-то момент вам вновь потребуется обратиться к этой переменной, то следует использовать оператор null, который будет сигнализировать что переменная когда-то содержала ссылку на объект.

1. B CoffeeScript отсутствует оператор void, вместо него следует использовать undefined, который транслируется в void 0.
2. На счет удаления предопределенных свойств нет четкого правила.
Hапример, вполне допустимо удалить свойство now объекта Date, но в тоже время, нельзя удалить метод call объекта Function:

	delete Date.now # true
	Date.now # undefined

	delete Function.call
	Function.call # [Function: call]

При этом, допускается удалить сам конструктор, в т.ч. и Object ([Configurable]]: true):

	delete Object # true
	typeof Object # undefined
	Object # ReferenceError: Object is not defined

2. Я не стал рассматривать оператор delete более подробно, т.к. это довольно большая тема, чтобы ее включать в эту статью. Если вас все-таки интересует этот вопрос, то рекомендую прочитать статью kangax'a Understanding delete

Помимо того что Object.seal() запрещает удаление свойств объекта, но еще и блокирует добавление новых:

object = {}

Object.seal object
object.property = 1

object.property # undefined

Примечание: серверная реализация выбросит TypeError!

Однако опечатывание не распространяется на модификацию значений свойств объекта:

object = property: 1
Object.seal object
object.property = 2

object.property # 1

Как и в случае с Object.preventExtensions(), попытка модифицировать свойства объекта в строгом режиме или с помощью Object.defineProperty/Object.defineProperties приведет к выбросу исключения типа TypeError!

Имплементация Object.seal() согласно спецификации:

Object.seal = (object) ->

	# 1. If Type(O) is not Object throw a TypeError exception
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.seal: # {object} is not callable!"

	# 2. For each named own property name P of O,
	Object.getOwnPropertyNames(object).forEach (property) ->

		# a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P
		__desc__ = Object.getOwnPropertyDescriptor object, property

		# b. If desc.[[Configurable]] is true, set desc.[[Configurable]] to false.
		if __desc__.configurable is on
			__desc__.configurable = off

		# c. Call the [[DefineOwnProperty]] internal method of O with P, desc, and true as arguments
		Object.defineProperty object, property, __desc__

	# 3. Set the [[Extensible]] internal property of O to false
	# 4. Return O.
	Object.preventExtensions object

Object.isSealed(object)

Определяет запечатан ли объект. Возвращает булево значение.

object = {}
Object.seal object

Object.isSealed object # true

Если пустой объект сделать не расширяемым, то он станет запечатанным:

object = {}
Object.preventExtensions object

Object.isSealed object # true

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

object = property: 1
<source lang="coffeescript">
Object.preventExtensions object

Object.isSealed object # false

Имплементация Object.isSealed() согласно спецификации:

Object.isSealed = (object) ->
	# 1. If Type(O) is not Object throw a TypeError exception.
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.isSealed: # {object} is not callable!"

	# 2. For each named own property name P of O then
	Object.getOwnPropertyNames(object).forEach (property) ->
		# a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P.
		__desc__ = Object.getOwnPropertyDescriptor object, property

		# b. If desc.[[Configurable]] is true, then return false.
		if __desc__.configurable is on
			return off

	# 3. If the [[Extensible]] internal property of O is false, then return true.
	# 4. Otherwise, return false.
	if !Object.isExtensible(object) then on else off

Object.freeze(object)

Замораживает объект.

Object.freeze = Object.preventExtensions + Object.seal + {[[Writable]]: off}

Иными словами Object.freeze() предотвращает добавление новых свойств в объект, модификацию и удаление существующих.

object = a: 1

object.a = 0 # false нельзя модифицировать свойства
object.b = 0 # false нельзя добавлять новые свойства
delete object.a # false нельзя удалять свойства

Object.getOwnPropertyDescriptor object, 'a'

# { configurable: false, enumerable: true, value: 1, writable: false}

Т.к. мы уже подробно рассмотрели Object.preventExtensions() и Object.seal() нет смысла повторяться.
Единственное на что бы мне бы хотелось обратить ваше внимание, так это на глубину «замораживания»:

object =
	property:
		internal: 1

Object.freeze object

object.property = 0          # false
object.property.internal = 0 # true

Object.getOwnPropertyDescriptor(object.property, 'internal').writable # true

Как видите, блокируется только дочерние свойства первого уровня!
На самом деле, в том, что в ECMASctipt 5 отсутствует метод глубокого замораживания ничего страшного нет. Попробуем реализовать Object.deepFreeze() сами:

Object.deepFreeze = (object) ->
	isObject = (value) ->
		Object::toString.call(value) is '[object Object]'

	if !isObject object
		throw new TypeError "Object.deepFreeze: # {object} is not callable!"

	for own key, value of object
		if isObject(value) and !Object.isFrozen value
			Object.deepFreeze(value)

	Object.freeze object

object =
	property:
		internal: 1

Object.deepFreeze object

Object.getOwnPropertyDescriptor(object.property, 'internal').writable # false

Имплементация Object.freeze() согласно спецификации:

Object.freeze = (object) ->

	# 1. If Type(O) is not Object throw a TypeError exception
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.freeze: # {object} is not callable!"

	# 2. For each named own property name P of O,
	Object.getOwnPropertyNames(object).forEach (property) ->

		# a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P
		__desc__ = Object.getOwnPropertyDescriptor object, property

		# b. If IsDataDescriptor(desc) is true, then
		#   If desc.[[Writable]] is true, set desc.[[Writable]] to false
		if __desc__.value and __desc__.writable is on
			__desc__.writable = off

		# c. If desc.[[Configurable]] is true, set desc.[[Configurable]] to false
		if __desc__.configurable is on
			__desc__.configurable = off

		# d. Call the [[DefineOwnProperty]] internal method of O with P, desc, and true as arguments
		Object.defineProperty object, property, __desc__

	# 3. Set the [[Extensible]] internal property of O to false
	# 4. Return O.
	Object.preventExtensions object

Object.isFrozen(object)

Определяет заморожен ли объект.

object =
	property: 1

Object.isFrozen object  # false
Object.freeze object
Object.isFrozen object  # true

Имплементация Object.isFrozen() согласно спецификации:

Object.isFrozen = (object) ->
	# 1. If Type(O) is not Object throw a TypeError exception.
	if Object::toString.call(object) is not '[object Object]'
		throw new TypeError "Object.isFrozen: # {object} is not callable!"

	# 2. For each named own property name P of O then
	Object.getOwnPropertyNames(object).forEach (property) ->
		# a. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with P.
		__desc__ = Object.getOwnPropertyDescriptor object, property

		# b. If IsDataDescriptor(desc) is true then
		#  i. If desc.[[Writable]] is true, return false.
		if __desc__.value and __desc__.writable is on
			return off

		# c. If desc.[[Configurable]] is true, then return false.
		if __desc__.configurable is on
			return off

	# 3. If the [[Extensible]] internal property of O is false, then return true.
	# 4. Otherwise, return false.
	if !Object.isExtensible(object) then on else off

Object.prototype.hasOwnProperty(property)

Определяет является ли свойство объекта собственным. Возвращает логическое значение.

object =
	property: 1

object.hasOwnProperty 'property' # true
object.hasOwnProperty 'toString' # false

Если нужно проверить не только собственные свойства, но и унаследованные, в этом случае, следует использовать оператор of, который анализирует цепочку прототипов:

object =
	property: 1

'property' of object # true
'toString' of object # true

Метод .hasOwnProperty() особенно полезен в связке с циклом for-of:

Object::inherited = 0

object =
	property: 1

(i for i of object) # [inherited, property]

for i of object
	i if object.hasOwnProperty i # property

Как видите, метод hasOwnProperty() может гарантировать что в перечисление не попадут унаследованные свойства.
Тем не менее, есть вероятность того, что в перечисляемом объекте уже может присутствовать свойство с именем hasOwnProperty:

Object::inherited = ->

object =
	own: 1
	hasOwnProperty: -> @

for i of object
	i if object.hasOwnProperty i

# inherited, own, hasOwnProperty

Безусловно это не тот результат который мы хотели получить. Так как же быть?

Разрешить такую ситуацию можно очень просто, для этого достаточно вызвать метод hasOwnProperty относительно Object.prototype в контексте требуемого объекта:

Object::inherited = ->

object =
	own: 1
	hasOwnProperty: -> @

for i of object
	i if Object::hasOwnProperty.call object, i

# own, hasOwnProperty

Такой способ вызова метода .hasOwnProperty является наиболее предпочтительным. Во-первых, это разрешает возможный конфликт имен, а во-вторых, обеспечивает более быструю фильтрацию, за счет прямой ссылки на метод.

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

Object.getOwnPropertyNames Object.prototype

[
  'toString',
  'toLocaleString',
  'hasOwnProperty',
  'valueOf',
  'constructor',
  'propertyIsEnumerable',
  'isPrototypeOf',
]

В зависимости от реализации этот список может быть расширен. К примеру для движков V8, Presto, Gecko это будут следующие свойства:

  '__lookupGetter__',
  '__defineGetter__',
  '__defineSetter__',
  '__lookupSetter__'

В Gecko еще дополнительно будут свойств watch и unwatch.

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

object = {}

own = Object::hasOwnProperty

for i of object
	i if own.call object, i

Так как метод hasOwnProperty() довольно часто используется совместно с инструкцией for-of, в CoffeeScript предусмотрен специальный оператор own:

alert i for own i of object

Результат трансляции:

var i, __hasProp = {}.hasOwnProperty;

for (i in object) {
	if (!__hasProp.call(object, i))
		continue;
	alert(i);
}

Более подробное описание оператора own и инструкции for-of смотрите в статье: CoffeeScript: Подробное руководство по циклам.

Напомню что для получения имен собственных свойств объекта есть методы Object.keys() и Object.getOwnPropertyNames(), которые больше подходит для этой задачи чем инструкция for-own-of.

Object.prototype.isPrototypeOf(object)

Проверят находится ли заданный объект в цепи прототипов. Возвращает логическое значение.

object = {}

Object::isPrototypeOf object # true
Object.isPrototypeOf object # false
Function::isPrototypeOf Object # true
Function::isPrototypeOf (new ->).constructor # true

fn = ->
instance = new fn
fn::.isPrototypeOf instance # true

Object.prototype.propertyIsEnumerable(object)

Проверяет является ли указанное свойство перечисляемым. Возвращает логическое значение.

object = property: 1

object.propertyIsEnumerable 'property'    # true
object.propertyIsEnumerable 'toString'    # false
object.propertyIsEnumerable 'prototype'   # false
object.propertyIsEnumerable 'constructor' # false

list = ['']

list.propertyIsEnumerable 0         # true
list.propertyIsEnumerable 'length'  # false

Заключение:

Мы полностью рассмотрели все методы объекта Object и их внутреннюю реализацию согласно спецификации. К сожалению реализовать многие методы из ECMAScript 5 для браузеров, которые их не поддерживают довольно проблематично. Частичную реализацию можно найти у меня на githab'e или тут

Eсли вы будете транслировать исходный код в JavaScript, то вам также следует ознакомиться с этой таблицей совместимости.

Автор: monolithed

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js