본문 바로가기

Javascript

[모던 자바스크립트 Deep Dive] 13. 프로퍼티 어트리뷰트

내부 슬롯과 내부 메서드

  • 내부 슬롯과 내부 메서드는 ECMA Script 사양에 정의된 대로 구현되어 자바스크립트 엔진에서 실제로 동작
  • 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아님
  • 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 내부 로직
  • 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단 제공
  • [[prototype]] 내부 슬롯의 경우 __proto__를 통해 간접적으로 접근 가능

 

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본 값으로 자동 정의

프로퍼티 상태 : 프로퍼티의 값, 값의 갱신 가능 여부, 열거 가능 여부, 정의 가능 여부를 말함

 

프로퍼티 어트리뷰트 : 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯

(Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인 가능)

프로퍼티 디스크립터 객체 : Object.getOwnPropertyDescriptor 메서드가 제공하는 프로퍼티 어트리뷰트 정보

const person = {
 name : "Echichi"
}
// 프로퍼티 동적 생성
person.age = 100;

// 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체 반환
console.log(Object.getOwnPropertyDescriptors(person));

/*
{
age : {value: 100, writable: true, enumerable: true, configurable: true}
name : {value: 'Echichi', writable: true, enumerable: true, configurable: true}
}
*/

 

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

  • 데이터 프로퍼티 : 키와 값으로 구성된 일반적인 프로퍼티. 지금까지 살펴본 모든 프로퍼티는 데이터 프로퍼티
  • 접근자 프로퍼티 : 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티

데이터 프로퍼티

자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Value]] value 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값 
[[Writable]] writable 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다
[[Enumerable]] enumerable 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다
[[Configurable]] configurable 프로퍼티의 재정의 기능 여부를 나타내며 불리언 값을 갖는다

프로퍼티 생성 시 [[value]] 값은 프로퍼티 값으로 초기화, [[Writable]], [[Enumerable]], [[Configurable]] 은 true로 초기화

 

접근자 프로퍼티

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

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티  설명
[[Get]] get 데이터 프로퍼티의 값을 읽었을 때 호출되는 접근자 함수 (getter)
[[Set]] set 데이터 프로퍼티 값을 저장할 때 호출되는 접근자 함수 (setter)
[[Enumerable]] enumerable 데이터 프로퍼티의 [[Enumerable]]과 같음
[[Configurable]] configurable 데이터 프로퍼티의 [[Configurable]]과 같음
const person = {
  firstName: "chichi",
  lastName: "E",

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + " " + person.lastName); // chichi E

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장 setter 함수 호출
person.fullName = "chichi E";
console.log(person); // { firstName: 'chichi', lastName: 'E' }

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조 getter 함수 호출
console.log(person.fullName); // chichi E

// firstName 데이터 프로퍼티
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 가짐
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
/*
{
  value: 'chichi',
  writable: true,
  enumerable: true,
  configurable: true
} 
*/

// fullName은 접근자 프로퍼티
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 가짐
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor);
/*
{
  get: [Function: get fullName],
  set: [Function: set fullName],
  enumerable: true,
  configurable: true
}
*/

 

접근자 프로퍼티 fullName 으로 프로퍼티 값에 접근하면 내부적으로 [[Get]] 내부 메서드가 호출되어 다음과 같이 동작

  1. 프로퍼티 키가 유효한지 확인한다. 프로퍼티 키는 문자열 또는 심벌이여야 한다. 프로퍼티 키 'fullName'은 문자열이므로 유효한 프로퍼티 키이다.
  2. 프로토타입 체인에서 프로퍼티를 검색한다. person객체에 fullName 프로퍼티가 존재한다.
  3. 검색된 fullName프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인한다. fullName 프로퍼티는 접근자 프로퍼티다.
  4. 접근자 프로퍼티 fullName프로퍼티 어트리뷰트 [[Get]]의 값은 Object.getOwnPropertyDescriptor 메서드가 반환하는 프로퍼티 디스크립터 객체의 get 프로퍼티 값과 같다.

 

프로퍼티 정의

프로퍼티 정의 : 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것 (Object.defineProperty 메서드를 사용하면 프로퍼티의 어트리뷰트 정의 가능)

 

// 객체에 프로퍼티 하나 정의
Object.defineProperty(객체, 추가할 프로퍼티, {
  value: 값,
  writable: boolean,
  enumerable: boolean,
  configurable: boolean
})

// 객체에 프로퍼티 여러 개 정의
Object.defineProperties(객체, {
  데이터 프로퍼티 1: {
   value: 값,
   writable: boolean,
   enumerable: boolean,
   configurable: boolean
 },
  데이터 프로퍼티 2: {
   value: 값,
   writable: boolean,
   enumerable: boolean,
   configurable: boolean
 },
  ...
  접근자 프로퍼티 1: {
   get() { ... },
   set() { ... },
   enumerable: boolean,
   configurable: boolean
  },
  접근자 프로퍼티 2: {
   get() { ... },
   set() { ... },
   enumerable: boolean,
   configurable: boolean
  }
})

 

객체 변경 방지

객체는 변경 가능한 값으로 재할당 없이 직접 변경할 수 있기 때문에 무분별한 객체 변경을 방지하는 다양한 메서드 제공

구분메서드프로퍼티 추가프로퍼티 삭제프로퍼티 값 읽기프로퍼티 값 쓰기프로퍼티 어트리뷰트 재정의

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

 

1. 객체 확장 금지

확장이 금지된 객체는 프로퍼티 추가 금지 (동적 추가, Object.definedProperty 금지)

Object.preventExtensions(객체);

// 확장 가능한 객체인지 판단 메서드 
Object.isExtensible(객체);

 

2. 객체 밀봉

밀봉된 객체는 읽기와 쓰기만 가능

Object.seal(객체);

// 밀봉된 객체인지 판단 메서드
Object.isSealed(객체);

 

3. 객체 동결

동결된 객체는 읽기만 가능

Object.freeze(객체);

// 동결된 객체인지 판단 메서드
Object.isFrozen(객체);

 

4. 불변 객체

변경 방지 메서드들은 얕은 변경 방지로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못함

→ 객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드 호출

function deepFreezen(target) {
  if (target && typeof target === "object" && !Object.isFrozen(target)) {
    // 동결되지 않은 객체 동결
    Object.freeze(target);

    // 모든 프로퍼티를 순회하며 재귀적으로 동결
    // Object.keys 메서드 : 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환
    // forEach 로 배열을 순회하며 배열의 각 요소에 대하여 콜백 함수 실행
    Object.keys(target).forEach((key) => deepFreezen(target[key]));
  }
}

const person = {
  name: "Echichi",
  age: 100,
  address: {
    city: "Daejeon",
  },
};
// 깊은 객체 동결
deepFreezen(person);

console.log(Object.isFrozen(person)); // true
console.log(Object.isFrozen(person.address)); // true 

person.address.city = "Busan";
console.log(person); // { name: 'Echichi', age: 100, address: { city: 'Daejeon' } }