JavaScript

1. 프로퍼티와 어트리뷰트

문정훈 2021. 10. 4. 23:37

 

이 글은 이옹모님의 모던 자바스크립트 DEEP DIVE 교재를 보고 정리한 글입니다.
더보기

목차

  1. 내부 슬롯 , 내부 메소드
  2. 프로퍼티 어트리뷰트
  3. 프로퍼티 디스크립터 객체
  4. 데이터 프로퍼티
  5. 접근자 프로퍼티티
  6. 프로퍼티 정의

1. 내부 슬롯 , 내부 메소드

내부 슬롯과 내부 메소드는 JS 엔진의 구현 알고리즘을 설명하기 위해 ECMAScipt 사양에서 사용하는 

의사 프로퍼티(pseudo property)와 의사 메서드(pseudo property)이다. 

ECMAScipt 사양에서 등장하는 이중 대괄호 [[...]]로 감싼 이름들이 내부 슬롯과 내부 메소드이다. 

 

내부 슬롯, 내부 메소드는 [[..]] 이렇게 생긴 개념은 실제로 없다. 이는 JS 엔진에서 실제로 동작하는 무엇(?)인가를 설명하기 위해 도입된 개념이라 생각하면 될 것 같다. 

내부 슬롯, 내부 메소드는 JS 엔진의 내부 로직이므로 원칙적으로 JS는 내부 슬롯과 내부 메소드에 직접적으로 접근하거나 호출하는 방법을 제공하지 않는다.

 

간단한 예로 프로퍼티의name의 값으로 'Lee'라는 값을 지정했다. 프로퍼티는 내부 슬롯으로 [[value]] 값을 가지는데 [[value]]는 실제 Lee라는 값을 가리킨다. ECMAScript문서에서 JS 내부 동작에서 프로퍼티 값이 어떻게 'Lee' 라는 값을 가리키냐? "에 대한 대답으로 "내부 슬롯 [[value]]가 가리키는 값이야!!" 와 같은 매개적인 문장의 설명으로 JS 내부 동작 원리를 설명하기 위해 존재하는 개념으로 정리하자. 

 

2. 프로퍼티 어트리뷰트

JS 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트(속성)를 기본값으로 자동 결정한다. 

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

프로퍼티 어트리뷰트는 JS 엔진이 관리하는 내부 상태 값인 내부 슬롯으로 정의한다.

 

3. 프로퍼티 디스크립터 객체

Object.getOwnPropertyDescriptor 메서드를 사용하면 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다. 이 메소드의 첫 매개변수로 객체의 참조를 전달하고 두번째 매개변수로 프로퍼티 키를 문자열로 전달한다. 위 코드를 참조하자.

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

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

 

 

Object.getOwnPropertyDescriptors 메소드를 사용하여 첫 매개변수로 객체를 전달하면 그 객체의 프로퍼티들이 가진 프로퍼티 어트리뷰트 정보를 보여준다. 

const person = {
  name: 'Lee'
};

// 프로퍼티 동적 생성
person.age = 20;

// 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
  name: {value: "Lee", writable: true, enumerable: true, configurable: true},
  age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/

 

4. 데이터 프로퍼티

프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분한다. 그 중 데이터 프로퍼티는 키와 값으로 구성된 일반적인 프로퍼티이다. 데이터 프로퍼티가 생성될때 아래와 같은 프로퍼티 어트리뷰트가 JS엔진에 의해 기본값으로 자동 정의된다.

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티                                            설명
[[Value]] value - 프로퍼티 키를 통해 접근하면 반환되는 값이다.
- 프로퍼티 키를 통해 값을 재할당하면 [[Value]]에 값을 재할당한다.
 
[[Writable]] writable - 프로퍼티 값의 변경 가능 여부를 나타내며 boolean값을 가진다.
- false인 경우 [[Value]]의 값을 변경할 수 없는 읽기 전용이 된다. 
[[Enumerable]] Enumerable - 프로퍼티 열거 가능 여부를 나타내며 불리언 값을 가진다. 
- false인 경우 for...in 문이나 Object.key 메서드 등으로 열거 불가능
[[Configurable]] Configurable - 프로퍼티의 재정의 기능 여부를 불리언 값으로 나타낸다. 
- false인 경우 해당 프로퍼티의 삭제, 값의 변경 금지가 된다. 
- [[Writable]]의 값이 true인 경우 [[Value]]의 변경과 [[Writable]]의 값을    false로 변경하는 것은 허용된다.

 

5. 접근자 프로퍼티

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

const person = {
  // 데이터 프로퍼티
  firstName: 'Ungmo',
  lastName: 'Lee',

  // fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set fullName(name) {
    // 배열 디스트럭처링 할당: "31.1 배열 디스트럭처링 할당" 참고
    [this.firstName, this.lastName] = name.split(' ');
  }
};

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

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

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName); // Heegun Lee

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

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

fullName 프로퍼티는 접근자 프로퍼티이다. 위 코드와 같이 get fullName() {}, set fullName(name) {} 선언이 가능하다. 접근자 프로퍼티는 [[Value]] 내부 슬롯을 가지지 않으며 다만 데이터 프로퍼티의 값을 읽거나 쓸때 관여하는 프로퍼티이다. 

 

6. 데이터 프로퍼티를 만들고 동시에 어트리뷰트를 명시해보자.

Object.defineProperty(<객체의 참조>, <데이터 프로퍼티의 키>, <프로퍼티 디스크립터 객체>)

매개변수를 아래의 코드 예시와 같이 지정하여 객체 리터럴의 데이터 프로퍼티를 생성 (또는 수정)함과 동시에 데이터 어트리뷰르를 명시해보는 예시이다. 

'lastName' 프로퍼티와 같이 데이터 프로퍼티의 지정을 하지 않는 내부 슬롯들은 undefined, false가 기본 값이며 지정된다. 

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}

// 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본값이다.
descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log('lastName', descriptor);
// lastName {value: "Lee", writable: false, enumerable: false, configurable: false}

// [[Enumerable]]의 값이 false인 경우
// 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다.
// lastName 프로퍼티는 [[Enumerable]]의 값이 false이므로 열거되지 않는다.
console.log(Object.keys(person)); // ["firstName"]

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

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

// [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 재정의할 수 없다.
// Object.defineProperty(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.defineProperty(person, 'fullName', {
  // getter 함수
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set(name) {
    [this.firstName, this.lastName] = name.split(' ');
  },
  enumerable: true,
  configurable: true
});

descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log('fullName', descriptor);
// fullName {get: ƒ, set: ƒ, enumerable: true, configurable: true}

person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}