Object.defineProperty или как сделать код капельку лучше

  • Tutorial
Этот краткий пост-заметку или температурный бред (в Одессе похолодало, да) хочу посвятить такой прекрасной функции, как Object.defineProperty (и Object.defineProperties). Активно использую её уже около двух месяцев, так как поддержка старых браузеров (в том числе и IE8) в проекте, который я сейчас реализую, не требуется (завидуйте).

Как положено статье на хабре, приведу краткое описание того, что она делает. Object.defineProperty добавляет новое свойство, обладающее неким нестандартным для обычного свойства поведением, и принимает три аргумента:
  • Объект, который мы модифицируем, добавляя новое свойство
  • Свойство (строка), которое, собственно, хотим добавить
  • Дескриптор: объект, содержащий «настройки» нового свойства, например аццессоры (геттер, сеттер)

Дескриптор может содержать следующие свойства:
  • value (любое значение: строка, функция...) — значение, которое получит определяемое свойство объекта (геттер и сеттер в данном случае определить нельзя)
  • writable (true/false) — можно ли перезаписать значение свойства (аццессоры тоже не доступны)
  • get (функция) — геттер (value и writable определить нельзя)
  • set (функция) — сеттер (value и writable определить нельзя)
  • configurable (true/false) — можно ли переопределить дескриптор (использовать Object.defineProperty над тем же свойством)
  • enumerable (true/false) — будет ли свойство перечисляться через for..in и доступно в Object.keys (плохая формулировка)

Пример
Содержимое
// Код сперт с MDN
var o = {};
Object.defineProperty(o, "a", {value : 37,
                               writable : true,
                               enumerable : true,
                               configurable : true});

 
var bValue;
Object.defineProperty(o, "b", {get : function(){ return bValue; },
                               set : function(newValue){ bValue = newValue; },
                               enumerable : true,
                               configurable : true});


Лучше меня объяснит MDN Object/defineProperty. Благо, даже английский знать не надо, и так всё понятно.

Если нужно определить сразу несколько свойств, можно использовать Object.defineProperties, который принимает два аргумента: объект, требующий изменений и объект с определяемыми ключами.
MDN: Object/defineProperties.

Пример
Содержимое
// Код сперт с MDN
Object.defineProperties(obj, {
  "property1": {
    value: true,
    writable: true
  },
  "property2": {
    value: "Hello",
    writable: false
  }
  // etc. etc.
});



Теперь соль. Чего я вообще решил это запостить?

Так как в упомянутом выше проекте мне приходится использовать defineProperty не просто активно, а очень активно, код стал, мягко говоря, некрасивым. Пришла в голову простейшая идея (как я до этого раньше-то не додумался?), расширить прототип Object, сделав код сильно компактнее. Плохой тон, скажете вы, засерать прототип Object новыми методами.

Откуда вообще взялось это мнение? Потому что все объекты унаследуют это свойство, которое, при обычной модификации прототипа, становится перечисляемым в for..in. На душе становится тепло, когда вспоминаешь о том, что я описал выше, а именно, о свойстве дескриптора enumerable. Действительно, расширив прототип таким образом:

Object.defineProperty( Object.prototype, 'logOk' {
	value: function() { console.log('ok') },
	enumerable: false
});

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

var o = {a: 1, b: 2}
for( var i in o ) console.log( i, o[ i ] );
> a 1
> b 2
o.logOk();
> ok


Собственно то, ради чего я тут графоманю.

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

Object.defineProperties( Object.prototype, {
	define: {
		value: function( key, descriptor ) {
			if( descriptor ) {
				Object.defineProperty( this, key, descriptor );
			} else {
				Object.defineProperties( this, key );
			}
			return this;
		},
		enumerable: false
	},
	extendNotEnum: {
		value: function( key, property ) {
			if( property ) {
				this.define( key, {
					value: property,
					enumerable: false,
					configurable: true
				});
			} else {
				for( var prop in key ) if( key.hasOwnProperty( prop ) ){
					this.extendNotEnum( prop, key[ prop ] );
				}
			}
		},
		enumerable: false
	}
});


Использование:
var o = { a: 1 };
o.define( 'randomInt', {
	get: function() {
		return 42;
	}
});

o.extendNotEnum({
	b: 2;
});

for( var i in o ) console.log( i, o[ i ] );
> a 1
> randomInt 42

console.log( o.b );
> 2


И пошла… Еще два удобных метода:

Object.prototype.extendNotEnum({
	extend: function() {
		var args = Array.prototype.slice.call( arguments );
		args.unshift( this );
		return $.extend.apply( null, args ); // если jQuery надоест, можно просто переписать под себя
	},
	
	each: function( callback ) {
		return $.each( this, callback ); // аналогично
	}
});


o.extend({c: 3}); // тупо добавляет новые свойства в объект
o.each(function( key, value ) {
	// просто повторяет механизм $.each, перебирая все ключи и свойства
});


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

Вывод

Играйте в Денди, пишите на Javascript, который становится всё лучше и лучше.

(Если заметили опечатку или неточность, обращайтесь, пожалуйста, в личку)
  • +16
  • 20,7k
  • 9
Поделиться публикацией

Похожие публикации

Комментарии 9

    +1
    Еще один финт ушами для тех, кто определяет свойства прототипа только однажды, после определения функции-конструктора:
    // запускается в консоли
    Object.defineProperty( Object.prototype, 'awesomeprototype', {
    	set: function( object ) {
    		for( var prop in object ) {
    			Object.defineProperty( this.prototype, prop, {
    				value: object[ prop ],
    				enumerable: false
    			});
    		}
    	}
    });
    
    var X = function() {}
    X.awesomeprototype = {
    	method: function() { alert( 'ok' ) }
    };
    
    var x = new X
    
    x.method()
    
      +3
      Стоит отметить что в литералах объектов свойства можно задавать через геттер и сеттер:
      var o = {
          __someProperty : 42,
          get someProperty() { return this.__someProperty; },
          set someProperty(v) { this.__someProperty = v; }
      };
      
      o.someProperty; // 42
      o.someProperty = 56;
      o.someProperty; // 56
      


      Ну это так, к слову :)
        +1
        Завидую =)
          0
          Аналогично.
          поддержка старых браузеров (в том числе и IE8) в проекте, который я сейчас реализую, не требуется (завидуйте).

          Сначала посмеялась над фразой, а потом заплакала…
            0
            Ну зачем так близко к сердцу-то?
        • НЛО прилетело и опубликовало эту надпись здесь
          • НЛО прилетело и опубликовало эту надпись здесь
            +1
            Кстати также давно действует SugarJS: если в браузере есть defineProperty, то добавляет методы не-enumerable, если же нет — то как обычно.

            А считается, что расширять прототипы нельзя не из-за того, что новые методы enumerable (точнее, не только), а из-за принципов инкапсуляции, которые говорят, что системные объекты должны быть неприкосновенны.
              –2
              Честно говоря, не вижу причин следовать вакуумным принципам инкапсуляции.

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

            Самое читаемое