ECMAScript 5 Objects and Properties

이글은 John Resig님의 'ECMAScript 5 Objects and Properties'을 번역한 것이다.

ECMAScript 5 Objects and Properties

EMCAScript 5는 진행중이다(역주, 변역하는 이 시점은 5.1도 발표됨). ECMAScript 4는 안드로메다로 가버렸기 때문에 ECMAScript 3.1이 나왔다. 우리가 사랑해 마지않는 ECMAScript 3을 개선한 EMCAScript3.1은 결국 ECMAScript 5가 됐다.

새로운 API들이 여러개 추가됐지만 가장 유심히 들여다봐야 하는 것은 Object/Property를 다루는 코드다. 이제 객체를 꼼꼼하게 관리할 수 있는 새로운 길이 열렸다. getter/setter를 정의할 수도 있고 프로퍼티의 enumeration/manipulatation/삭제/추가를 하지 못하도록 막을 수도 있다. 간단히 말해서 다른 도구없이 JavaScript만으로 DOM같은 기존의 JavaScript API를 확장하거나 수정할 수 있게 됐다.

중요한 것은 역시 모든 주요 브라우저들이 이 기능들을 지원하는 것이다. 주요 브라우저 벤더들은 모두 이 표준에 참여 했고 자신들의 JavaScript 엔진에 구현하기로 했다. 정확한 시점은 명확하지 않지만 곧 구현될 것 같다.

아직 구현중이고 ES5를 전부 구현한 브라우저는 없다. 하지만 ECMAScirpt 5 표준을 읽거나 Google에서 ECMA덕들을 쫓아다닐 수 있다. 이 글은 ECMAScript 5 표준의 107-109 페이지를 다룬다.

주의: 나는 새로운 메소드들이 어떻게 동작하는 지 예를 들어 설명할 것이다. 예제들은 다른 코드들이 필요하기 때문에 그냥은 동작하지 않는다. 그리고 표준과 100%일치하지도 않는다(예를 들어 error checking이 없다).

Objects

ECMAScript 5는 객체의 확장 여부를 끄고 킬수 있다. 객체를 확장하지 못하도록 하면 객체에 새로운 프로퍼티를 추가할 수 없다.

ES5는 객체의 extensibility를 위해 두가지 메소드를 제공한다.

Object.preventExtensions( obj ) & Object.isExtensible( obj )

preventExtensions()는 객체를 잠가서 더 이상 프로퍼티를 추가할 수 없게 만든다. isExtensible()은 객체가 잠겼는지 확인하는 것이다.

Example Usage:

#!js
var obj = {};
obj.name = "John";
print( obj.name );
// John

print( Object.isExtensible( obj ) );
// true

Object.preventExtensions( obj );

obj.url = "http://ejohn.org/"; // Exception in strict mode

print( Object.isExtensible( obj ) );
// false

Properties and Descriptors

프로퍼티는 재조명받고 있다. 더이상 객체를 따라다니는 꼬봉같은게 아니다. 프로퍼티가 무엇인지 이해하고 잘 활용하면 정말 강력하지만 조금 복잡하다.

프로퍼티는 두 가지 관점으로 나눌 수 있다.

프로퍼티는 실제로 두 가지다 값을 나타내는 "Data" 프로퍼티와 Getter/Setter인 "Accessor" 프로퍼티로 나뉜다. 전자는 Value를 의미하는 것으로 ECMAScript 3부터 우리가 익히 알고 있던 것이고 후자는 Gecko와 WebKit으로 부터 도입된 것들이다.

  • Value. property의 값이 들어 있다.
  • Get. Property에 접근할때 호출되는 함수.
  • Set. Property에 값을 할당할 때 호출되는 함수.

그리고 프로퍼티는 Writable/Configurable/Enumerable이 될 수 있다.

  • Writable. false면 프로퍼티의 값이 변경되지 않는다.
  • Configurable. false면 프로퍼티를 삭제하거나 Writable/Configurable/Enumerable을 변경할 수 없다.
  • Enumerable. true면 for (var prop in obj){} (or similar)과 같은 코드에서 iterated될 수 있다.

여기서 설명한 것들이 모두 프로퍼티 descriptor의 구성요소다. 이 descriptor는 다음과 같이 생겼다:

{
  value: "test",
  writable: true,
  enumerable: true,
  configurable: true
}

writable, enumerable, configurable은 모두 생략할 수 있고 기본값은 true다. 그래서 그냥 프로퍼티가 필요한 것이라면 value/get/set만 사용하면 된다.

그리고 Object.getOwnPropertyDescriptor() 메소드로 이미 정의된 property의 정보를 읽어올 수 있다.

Object.getOwnPropertyDescriptor( obj, prop )

이 메소드로 property descriptor를 얻을 수 있다. 이 메소드말고 property descriptor를 얻을 수 있는 다른 방법은 없다. ECMAScript engine 내부에 저장되는 것이기 때문에 일반적으로 접근할 수 없는 것이다.

Example Usage:

var obj = { foo: "test" };
print(JSON.stringify(
  Object.getOwnPropertyDescriptor( obj, "foo" )
));
// {"value": "test", "writable": true,
//  "enumerable": true, "configurable": true}

Object.defineProperty( obj, prop, desc )

이 메소드로 객체의 Property를 새로 정의하거나 property descriptor를 변경할 수 있다. 이 메소드는 property descriptor를 인자로 받아서 property를 설정한다.

Example Usage:

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

(function(){
  var name = "John";

  Object.defineProperty( obj, "name", {
    get: function(){ return name; },
    set: function(value){ name = value; }
  });
})();

print( obj.value )
// true

print( obj.name );
// John

obj.name = "Ted";
print( obj.name );
// Ted

for ( var prop in obj ) {
  print( prop );
}
// value
// name

obj.value = false; // Exception if in strict mode

Object.defineProperty( obj, "value", {
  writable: true,
  configurable: false
});

obj.value = false;
print( obj.value );
// false

delete obj.value; // Exception

Object.defineProperty는 ECMAScript 5의 한 축이다. 사실 ECMAScript 5에 추가된 다른 많은 기능들은 이 메소드가 필요하다.

Object.defineProperties( obj, props )

다수의 property를 한꺼번에 정의하는 것이다.

Example Implementation:

Object.defineProperties = function( obj, props ) {
  for ( var prop in props ) {
    Object.defineProperty( obj, prop, props[prop] );
  }
};

Example Usage:

var obj = {};
Object.defineProperties(obj, {
  "value": {
    value: true,
    writable: false
  },
  "name": {
    value: "John",
    writable: false
  }
});

프로퍼티 Descriptor가 ECMAScript 5의 가장 핵심적인 특징이다. 이제 개발자들은 객체를 꼼꼼하게 제어할 수 있다. 누가 엄하게 수정하는 것을 미연에 방지할 수 있고 web api들을 실질적으로 관리할 수 있게 됐다.

New Features

이 절에서 설명하는 것은 위에서 설명한 것에 기초하는 것이다. 그래서 어떤 것들이 추가됐는지 살펴보자.

객치의 프로퍼티를 모으는 메소드가 두 가지 추가됐다. 어떤 점이 다른지 보자.

Object.keys( obj )

이 메소드는 Enumerable한 Property 이름을 Array에 담아서 반환한다. Prototype.js에 있는 것과 똑같다.

Example Implementation:

Object.keys = function( obj ) {
  var array = new Array();
  for ( var prop in obj ) {
    if ( obj.hasOwnProperty( prop ) ) {
      array.push( prop );
    }
  }
  return array;
};

Example Usage:

var obj = { name: "John", url: "http://ejohn.org/" };
print( Object.keys(obj).join(", ") );
// name, url

Object.getOwnPropertyNames( obj )

Enumerable하지 않은 Property도 포함되는 것을 제외하면 Object.keys와 똑같다.

ECMAScript는 원래 Enumerable한 property만 Enumerated된다고 정의하기 때문에 불가능했었다. 이제 이 메소드를 이용하면 되고 결과나 사용법은 Object.keys()와 동일하다.

Object.create( proto, props )

proto에는 객체의 prototype을, props는 defineProperties를 사용하여 초기화할 property들을 인자로 받아 객체를 새로 만든다.

다음은 샘플 구현인데 Object.defineProperties() 메소드가 필요하다.

Example Implementation: (by Ben Newman)

Object.create = function( proto, props ) {
  var ctor = function( ps ) {
    if ( ps )
      Object.defineProperties( this, ps );
  };
  ctor.prototype = proto;
  return new ctor( props );
};

Other implementation:

Object.create = function( proto, props ) {
  var obj = new Object();
  obj.__proto__ = proto;
  if ( typeof props !== "undefined" ) {
    Object.defineProperties( obj, props );
  }

  return obj;
};

주의: 이 코드는 Mozilla만의 __proto__ 프로퍼티를 사용했다. __proto__를 이용하면 객체 내부 prototype에 접근할 수 있을 뿐만 아니라 값을 설정할 수도 있다. ES5에서 추가된 Object.getPrototypeOf()는 접근할 수는 있지만 값을 바꿀 수 없다. 즉, __proto__를 이용한 구현은 표준이 아니고 일반적인 방법도 아니다.

Object.getPrototypeOf()는 다른 글에서 다뤘었으니 이 글에서는 생략한다.

Example Usage:

function User(){}
User.prototype.name = "Anonymous";
User.prototype.url = "http://google.com/";
var john = Object.create(new User(), {
  name: { value: "John", writable: false },
  url: { value: "http://google.com/" }
});

print( john.name );
// John

john.name = "Ted"; // Exception if in strict mode

Object.seal( obj )
Object.isSealed( obj )

객체를 Seal한다는 것은 객체의 모든 프로퍼티의 Descriptor들을 수정할 수 없도록 막는 것이다. 프로퍼티를 새로 추가하는 것도 안된다.

Example Implementation:

Object.seal = function( obj ) {
  var props = Object.getOwnPropertyNames( obj );

  for ( var i = 0; i < props.length; i++ ) {
    var desc = Object.getOwnPropertyDescriptor( obj, props[i] );

    desc.configurable = false;
    Object.defineProperty( obj, props[i], desc );
  }

  return Object.preventExtensions( obj );
};

디시 말해서 프로퍼티의 값은 바꿀 수 있지만 기존의 프로퍼티를 변경하지 못하고 새로 추가하지도 못하게 하려면 객체를 Seal하면 된다.

Object.freeze( obj ) & Object.isFrozen( obj )

객체를 Freeze하는 것은 프로퍼티의 값도 변경할 수 없다는 점을 빼면 Seal과 똑같다.

Example Implementation:

Object.freeze = function( obj ) {
  var props = Object.getOwnPropertyNames( obj );

  for ( var i = 0; i < props.length; i++ ) {
    var desc = Object.getOwnPropertyDescriptor( obj, props[i] );

    if ( "value" in desc ) {
      desc.writable = false;
    }

    desc.configurable = false;
    Object.defineProperty( obj, props[i], desc );
  }

  return Object.preventExtensions( obj );
};

객체를 freeze하는 것이 가장 강력한 잠금장치다. 한번 잠그면 되돌릴 수 없기 때문에 누구도 바꿀 수 없다. freeze하는 것이 해당 객체를 의도한 상태 그대로 유지하는 가장 확실한 방법이다.

추가된 기능들은 매우 흥미롭다. 이전에 없었던 아주 새로운 관점에서 객체를 다룰 수 있다. 그리고 좀 더 많은 것을 Pure JavaScript로 구현할 수 있게 됐다. DOM 모듈이나 browser에 구현해서 제공했어야 했던 API들을 이제 JavaScript로 구현할 수 있다. 모든 브라우저 벤더들이 결정에 참여 했기 때문에 곧 구현될 거다.