[Modern JS Deep Dive] CH16-프로퍼티 어트리뷰트(Property Attribute)

Boo Sung Jun·2022년 7월 4일
0

JavaScript

목록 보기
8/27
post-thumbnail

Modern JavaScript Deep Dive 스터디 - CH16 프로퍼티 어트리뷰트(Property Attribute)

참고 자료: ⟪모던 자바스크립트 Deep Dive⟫"(이웅모 지음,위키북스, 2020)


1. 내부 슬롯(internal slot)과 내부 메서드(internal method)

  • 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(pseudo property)와 의사 메서드(pseudo method)
  • ECMAScript 사양에 등장하는 이중 대괄호([[...]])로 감싼 이름들
  • 원칙적으로 자바스크립트는 내부 슬롯과 내부 메서드에 직접 접근은 불가능(자바스크립트 엔진의 내부 로직이므로)
  • BUT 일부 내부 슬롯 & 메서드에 대해서는 접근할 수 있는 방법을 제공. 예) [[Prototype]] 내부 슬롯은 __proto__ 를 통해 간접적으로 접근 가능
const o = {};

// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근할 수 없다.
o.[[Prototype]] // -> Uncaught SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기는 한다.
o.__proto__ // -> Object.prototype

2. 프로퍼티 어트리뷰트(Property Atrribute)와 프로퍼티 디스크립터 객체(Property Descriptor Object)

Attribute vs Property

  • Attribute
    1. Html element에 대한 속성
    ex) title, alt, class, ...
    attribute

  • Property
    1. CSS에 대한 속성
    ex) color, width, text-align
    property-css
    attribute-property

    1. JavaScript에서 오브젝트에 대한 특징(MDN)
      property-javascript

1) Property Attribute

  • 자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의
  • 프로퍼티 상태: 값, 값의 갱신 가능 여부, 열거 가능 여부, 재정의 가능 여부
  • 프로퍼티 어트리뷰트: 자바스크립트 엔진이 관리하는 내부 상태 값(meta-property)인 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
프로퍼티 상태프로프티 어트리뷰트
[[Value]]
값 갱신 여부[[Writable]]
열거 가능 여부[[Enumerable]]
재정의 가능 여부[[Configurable]]

property_attribute

2) Property Descriptor Object

  • 프로퍼티 어트리뷰트 정보를 제공하는 객체
  • Object.getOwnPropertyDescriptor(객체, 프로퍼티 키) 메서드를 이용해 프로퍼티 어트리뷰트를 간접적으로 확인 가능
const person = {
  name: 'Lee'
};

// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}
  • 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구 -> undefined 리턴
  • 기본적으로 하나의 프로퍼티에 대해 프로퍼티 디스크립터 객체를 반환
  • ES8 부터 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}
}
*/

3. 데이터 프로퍼티(data property)와 접근자 프로퍼티(accessor property)

  • 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다

1) 데이터 프로퍼티

  • (key : value) 로 구성된 일반적인 프로퍼티
  • 데이터 프로퍼티 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의
프로프티 어트리뷰트프로퍼티
디스크립터 객체의
프로퍼티
설명
[[Value]]value- 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환
- 프로퍼티 키를 통해 프로퍼티 값을 변경하면
[[Value]]에 값을 재할당
- 이때 프로퍼티 가 없으면 동적으로 생성
[[Writable]]writable- 프로퍼티 값 변경 가능 여부
- 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을
변경할 수 없음->읽기 전용 프로퍼티가 됨(삭제는 가능)
[[Enumerable]]enumerable- 열거 가능 여부
- 값이 false인 경우 for문이나 Object.keys메서드 등으로
열거 불가
[[Configurable]]configurable- 재정의 가능 여부
- 값이 false인 경우 프로퍼티의 삭제,
어트리뷰트 값의 변경 불가
- 단, [[Writable]]이 true인 경우
[[Value]]의 변경과 [[Writable]]
false로 변경하는 것은 허용

2) 접근자 프로퍼티

  • 자체적으로는 값을 갖지 않고 ,다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수(accessor function)로 구성된 프로퍼티
  • 접근자 함수 == getter / setter 함수
  • getter 와 setter 는 모두 정의하거나 하나만 정의할 수도 있고, 아예 정의 안할 수도 있음
프로프티 어트리뷰트프로퍼티
디스크립터 객체의
프로퍼티
설명
[[Get]]get- 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수
- getter함수가 호출되고 그 결과가 반환
[[Set]]set- 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수
- setter함수가 호출되고 그 결과가 프로퍼티 값으로 저장
[[Enumerable]]enumerable- 데이터 프로퍼티의 [[Enumerable]]과 같음
[[Configurable]]configurable- 데이터 프로퍼티의 [[Configurable]]과 같음
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}
// 일반 객체의 __proto__는 접근자 프로퍼티다.
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

// 함수 객체의 prototype은 데이터 프로퍼티다.
Object.getOwnPropertyDescriptor(function() {}, 'prototype');
// {value: {...}, writable: true, enumerable: false, configurable: false}

4. 프로퍼티 정의

  • 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의 하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의 하는 것
  • 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}

// 디스크립터 객체의 프로퍼티를 누락시키면 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

// ====== 
// 질문: 처음 정의할 때 [[configurable]]을 실수로 빠뜨리면 false값이 되어 계속 남아있게 되는지... 
// =====

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"}
  • Object.defineProperties 메서드를 사용하면 여러개의 프로퍼티를 한 번에 정의 가능
const person = {};

Object.defineProperties(person, {
  // 데이터 프로퍼티 정의
  firstName: {
    value: 'Ungmo',
    writable: true,
    enumerable: true,
    configurable: true
  },
  lastName: {
    value: 'Lee',
    writable: true,
    enumerable: true,
    configurable: true
  },
  // 접근자 프로퍼티 정의
  fullName: {
    // getter 함수
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    // setter 함수
    set(name) {
      [this.firstName, this.lastName] = name.split(' ');
    },
    enumerable: true,
    configurable: true
  }
});

person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
  • 각 프로퍼티에 대해 모든 프로퍼티 어트리뷰트를 설정할 필요는 없음. 설정하지 않을 경우 default 로 설정되는 값들이 존재
프로퍼티
디스크립터 객체의
프로퍼티
프로프티 어트리뷰트default value
value[[Value]]undefined
writable[[Value]]false
enumerable[[Value]]false
configurable[[Value]]false
get[[Value]]undefined
set[[Value]]undefined

5. 객체 변경 방지

  • 자바스크립트는 객체의 변경을 방지하는 다양한 메서드를 제공

1) Object.preventExtensions

  • 객체의 확장을 금지 -> 프로퍼티 추가 불가
const person = { name: 'Lee' };

// person 객체의 확장을 금지하여 프로퍼티 추가를 금지
Object.preventExtensions(person);

console.log(Object.isExtensible(person)); // false

person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 삭제는 가능
delete person.name;
console.log(person); // {}

// 프로퍼티 정의에 의한 프로퍼티 추가도 금지
Object.defineProperty(person, 'age', { value: 20 });
// TypeError: Cannot define property age, object is not extensible

2) Object.seal

  • 읽기와 쓰기만 가능
const person = { name: 'Lee' };

Object.seal(person);

console.log(Object.isSealed(person)); // true

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

person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 값 갱신은 가능
person.name = 'Kim';
console.log(person); // {name: "Kim"}

// 프로퍼티 어트리뷰트 재정의가 금지
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name

3) Object.freeze

  • 객체를 동결 -> 읽기만 가능 -> 프로퍼티 추가, 삭제 불가 + 어트리뷰트 재정의 불가 + 프로퍼티 값 갱신 불가
const person = { name: 'Lee' };

Object.freeze(person);

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

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

person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

person.name = 'Kim'; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 어트리뷰트 재정의가 금지
Object.defineProperty(person, 'name', { configurable: true });
// TypeError: Cannot redefine property: name
구분메서드프로퍼티
추가
프로퍼티
삭제
프로퍼티 값
읽기
프로퍼티

쓰기
프로퍼티
어트리뷰트
재정의
객체 확장 금지Object.preventExtensionsXOOOO
객체 밀봉Object.isSealedXXOOX
객체 동결Object.freezeXXOXX

4) 불변 객체

  • 이전 메서드들은 얕은 변경 방지(shallow only) -> 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못함
  • 한번의 Object.freeze 메서드 로 객체를 동결하여도 중첩 객체까지 동결할 수는 없음
  • => 중첩 객체까지 불변 객체로 구현하려면 재귀적으로 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);

console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결한다.
console.log(Object.isFrozen(person.address)); // true

person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Seoul"}}

0개의 댓글