공부한 내용/javascript

프로퍼티 어트리뷰트

hyongti 2022. 3. 4. 13:26

코드 내에서 함수를 정의하고, 해당 함수를 console.dir로 조회하면 함수 포스팅에서 살펴봤던 arguments, caller와 같은 프로퍼티 외에 [[FunctionLocation]], [[Prototype]], [[Scopes]]가 있는 것을 볼 수가 있습니다.

이렇게 이중 대괄호([[...]])로 감싼 이름들을 내부 슬롯(internal slot)과 내부 메서드(internal method)라고 합니다.

 

내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(pseudo property)와 의사 메서드(pseudo method)입니다.

 

내부 슬롯과 내부 메서드는 ECMAScript 사양에 정의된 대로 구현되어 자바스크립트 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아닙니다.

 

모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖습니다.

내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 원칙적으로 직접 접근할 수 없지만 [[Prototype]]의 경우 __proto__를 통해 간접적으로 접근할 수 있습니다.


프로퍼티 어트리뷰트

자바스크립트 엔진은

프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의

합니다.

 

프로퍼티 상태란 프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)를 말합니다.

 

프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값(meta-property)내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]입니다. 내부 상태 값은 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수 있습니다.

const person = {
  name: 'Lee',
};

console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

Object.getOwnPropertyDescriptor 메서드의 첫 번째 매개변수에는 객체의 참조를, 두 번째 매개변수에는 프로퍼티 키를 문자열로 전달합니다. 반환값은 프로퍼티 어트리뷰트의 정보를 제공하는 프로퍼티 디스크립터 객체입니다.

 

ES8에서는 객체가 가지고 있는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 Object.getOwnPropertyDescriptors 메서드가 있습니다.


데이터 프로퍼티와 접근자 프로퍼티

데이터 프로퍼티(data property) 

키와 값으로 구성된 일반적인 프로퍼티.

데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 가짐.

 

위에 있던 예제 코드를 다시 가져와서 보면,

const person = {
  name: 'Lee',
};

console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

person 객체 내부에 있는 프로퍼티 name: 'Lee'는 키와 값으로 구성된 일반적인 프로퍼티입니다. 즉, 데이터 프로퍼티입니다.

프로퍼티 디스크립터 객체 내부에 있는 프로퍼티들을 조금 살펴보면,

  • value: 프로퍼티 어트리뷰트 [[Value]]에 해당하는 프로퍼티로, person.name과 같이 프로퍼티 키를 통해 접근하면 반환되는 값입니다. 프로퍼티 키를 통해 값을 변경하면 [[Value]]에 값을 재할당하고 프로퍼티가 없으면 프로퍼티를 동적으로 생성한 후 [[Value]]에 저장합니다.
  • writable: 프로퍼티 어트리뷰트 [[Writable]]에 해당하는 프로퍼티로, 불리언 값을 가지며 프로퍼티 값의 변경 가능 여부를 나타냅니다. false일 경우 읽기 전용 프로퍼티가 됩니다.
  • enumerable: 프로퍼티 어트리뷰트 [[Enumarable]]에 해당하는 프로퍼티로, 불리언 값을 가지며 프로퍼티의 열거 가능 여부를 나타냅니다. false인 경우 해당 프로퍼티는 for ... in문이나 Object.keys 메서드 등으로 열거할 수 없습니다.
  • configurable: 프로퍼티 어트리뷰트 [[Configurable]]에 해당하는 프로퍼티로, 불리언 값을 가지며 프로퍼티의 재정의 가능 여부를 나타냅니다. false일 경우 해당 프로퍼티의 삭제나 프로퍼티 어트리뷰트 값의 변경이 금지됩니다.

접근자 프로퍼티(accessor property)

자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출하는 접근자 함수로 구성된 프로퍼티.

접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 가짐.

 

새로운 예제 코드를 살펴보겠습니다.

const person = {
  // 데이터 프로퍼티
  firstName: 'Ungmo',
  lastName: 'Lee',
  
  // 접근자 프로퍼티
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set fullNanme(name) {
    [this.firstName, this.lastName] = name.split(' ');
  },
};

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출됨
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: 'Heegun', lastName: 'Lee');

// 접근자 프로퍼티 fullName으로 값을 조회하면 getter 함수가 호출됨
console.log(person.fullName); // Heegun Lee

// 접근자 프로퍼티 fullName을 조회하면
const descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 가짐
// {get: f, set: f, enumarable: true, configurable: true}

fullName 프로퍼티는 자체적으로는 값을 갖지 않습니다. 대신 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용됩니다. 즉, 접근자 프로퍼티입니다.

 

접근자 프로퍼티의 프로퍼티 디스크립터 객체 내부를 살펴보면,

  • get: 프로퍼티 어트리뷰트 [[Get]]에 해당하는 프로퍼티로, 접근자 프로퍼티의 키로 프로퍼티 값에 접근하면 예제 코드에서 정의한 getter 함수가 호출됩니다.
  • set: 프로퍼티 어트리뷰트 [[Set]]에 해당하는 프로퍼티로, 접근자 프로퍼티의 키로 프로퍼티 값을 저장하면 예제 코드에서 정의한 setter 함수가 호출됩니다.
  • enumarable, configurable: 데이터프로퍼티의 enumarable, configurable과 같습니다.

 

 

프로퍼티 정의

프로퍼티 정의란

  • 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나,
  • 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것

을 말합니다.

 

Object.defineProperty 메서드를 사용하면 프로퍼티 어트리뷰트를 정의할 수 있습니다.

 

예제 코드를 통해 살펴보겠습니다.

const person = {};

// 데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
  value: 'Ungmo',
  writable: true,
  enumerable: true,
  configurable: true,
});

Object.defineProperty(person, 'lastName', {
  value: 'Lee',
});

let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log('firstName', descriptor);
// firstName {value: 'Ungmo', writable: true, enumerable: true, configurable: true}

descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log('lastName', descriptor);
// lastName {value: 'Lee', writable: false, enumerable: false, configurable: false}

lastName과 같이 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본값입니다.

 

위의 예제 코드에 이어서,

// [[Enumerable]]이 false인 경우
// 해당 프로퍼티는 for ... in문이나 Object.keys를 통해 열거할 수 없음
console.log(Object.keys(person)); // ["firstName"]

// [[Writable]]이 false인 경우 해당 프로퍼티의 [[Value]] 값을 변경할 수 없음
// 값을 변경하면 에러는 발생하지 않고 무시됨
person.lastName = 'Kim';

// [[Configurable]]이 false인 경우 해당 프로퍼티를 삭제할 수 없음
// 프로퍼티를 삭제하면 에러는 발생하지 않고 무시됨
delete person.lastName;

// [[Configurable]]이 false인 경우 해당 프로퍼티를 재정의 할 수도 없음
Object.definePropery(person, 'lastName', { enumerable: true });
// Uncaught TypeError: Cannot redefine property: lastName

descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log('lastName', descriptor);
// lastName {value: 'Lee', writable: false, enumerable: false, configurable: false}

 

Object.defineProperties를 통해 여러 개의 프로퍼티를 한번에 정의할 수도 있습니다.


객체 변경 방지

구분 메서드 프로퍼티 추가 프로퍼티 삭제 프로퍼티 값 읽기 프로퍼티 값 쓰기 프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X
객체 동결 Object.freeze X X O X X

위 표에 언급된 메서드들을 통해 객체의 변경을 방지할 수 있습니다.

확장이 가능한 객체인지 여부는 Object.isExtensible 메서드를 통해 확인할 수 있습니다.

밀봉된 객체인지 여부는 Object.isSealed 메서드를 통해,

동결된 객체인지 여부는 Obejct.isFrozen 메서드를 통해 확인할 수 있습니다.

 

위의 메서드들은 얕은 변경 방지(shallow only)로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못합니다. 따라서 중첩 객체까지 동결하기 위해서는 아래와 같이 재귀적으로 Object.freeze 메서드를 호출해야 합니다.

function deepFreeze(target) {
  // 객체가 아니거나 동결된 객체를 제외하고
  // 동결되지 않은 객체만 동결
  if (target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
    
    Object.keys(target).forEach(key => deepFreeze(target[key]);
  }
  return target
}

const person = {
  name: 'Lee',
  address: {
    city: 'Seoul',
  },
};

deepFreeze(person);