모던 JS DeepDive(16장)

Minji Lee·2025년 2월 18일
0

16.1 내부 슬롯과 내부 메서드

내부 슬롯와 내부 메서드: 자바스크립트 엔진의 구현 알고리즘 설명을 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드이중 대괄호([[…]])로 감싼 이름들

  • 자바스크립트 엔진에서 실제로 동작하지만, 개발자가 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출 X
  • 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단 제공
    const o = {};
    // 모든 객체는 [[Prototype]]이라는 내부 슬롯 가짐
    // 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근 X
    o.[[Prototype]] // Uncaught SyntaxError: Unexpected token '['
    // 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있음!
    o.__proto__ // Object.prototype

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

  • 자바스크립트 엔진은 프로퍼티 생성할 때 프로퍼티 상태를 나타내는 프로퍼티 어트리뷰트를 값으로 자동 정의
    • 프로퍼티 상태 ⇒ 프로퍼티 값, 값의 갱신 여부, 열거 가능 여부, 재정의 가능 여부
    • 프로퍼티 어트리뷰트 ⇒ 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
  • 프로퍼티 어트리뷰트에 직접 접근할 수는 없지만, Object.getOwnPropertyDescriptor 메서드 사용하여 간접적으로 확인 가능 ⇒ 프로퍼티 어트리뷰트 정보 제공하는 프로퍼티 디스크립터 객체 반환(존재하지 않으면 undefined 반환) Object.getOwnPropertyDescriptor(객체 참조, 프로퍼티 키)
    const person = {
      name: 'Lee',
    };
    
    person.age = 20; // 프로퍼티 동적 생성
    
    console.log(Object.getOwnPropertyDescriptor(person, 'name'));
    // { value: 'Lee', writable: true, enumerable: true, configurable: true }
    
    // ES8에서 도입된 getOwnPropertyDescriptors 메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보 제공
    console.log(Object.getOwnPropertyDescriptors(person));
    /*
    {
      name: {
        value: 'Lee',
        writable: true,
        enumerable: true,
        configurable: true
      },
      age: { value: 20, writable: true, enumerable: true, configurable: true }
    }
    */

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

데이터 프로퍼티: 키와 값으로 구성된 일반적인 프로퍼티

ex) 함수 객체의 prototype데이터 프로퍼티

프로퍼티 어트리뷰트프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Value]]value프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값
- 프로퍼티 키를 통해 값 변경하면 [[Value]]에 값을 재할당함!
프로퍼티 없으면 프로퍼티를 동적 생성하고 값 저장
[[Writable]]writable프로퍼티 값 변경 가능 여부 나타냄(boolean 값)
- false인 경우 읽기 전용 프로퍼티가 됨
[[Enumerable]]enumerable프로퍼티의 열거 가능 여부 나타냄(boolean 값)
- false인 경우 열거 X
[[Configurable]]configurable프로퍼티의 재정의 가능 여부 나타냄(boolean 값)
- false인 경우 해당 프로퍼티 삭제, 값 변경 X - [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용
- 데이터 프로퍼티가 생성될 때, [[Value]]의 값은 프로퍼티 값으로 초기화되고, [[Writable]], [[Enumerable]], [[Configurable]]의 값은 true로 초기화됨]]

접근자 프로퍼티: 자체적으로 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티 ⇒ getter/setter 함수라고도 부름

ex) 일반 객체의 __proto__접근자 프로퍼티

프로퍼티 어트리뷰트프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Get]]get접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수
- getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Set]]set접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수
- setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장
[[Enumerable]]enumerable프로퍼티의 열거 가능 여부 나타냄(boolean 값)
- false인 경우 열거 X
[[Configurable]]configurable프로퍼티의 재정의 가능 여부 나타냄(boolean 값)
- false인 경우 해당 프로퍼티 삭제, 값 변경 X
- [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용
const person = {
  // 데이터 프로퍼티
  firstName: 'Gildong',
  lastName: 'Hong',

  // 접근자 프로퍼티
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(' ');
  },
};

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

// 접근자 프로퍼티를 통한 프로퍼티 값 저장
person.fullName = 'Jjanggu Shin';
console.log(person); // { firstName: 'Jjanggu', lastName: 'Shin', fullName: [Getter/Setter] }
console.log(person.fullName); // Jjanggu Shin

[접근자 프로퍼티 동작 원리]

  1. 프로퍼티 값이 유효한지 확인(프로퍼티 값은 문자열 혹은 심벌이어야 함)
  2. 유효하면, 프로토타입 체인에서 프로퍼티 검색

    프로토타입: 어떤 객체의 상위(부모)객체의 역할을 하는 객체
    즉, 상위(부모) 객체는 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속하여 상속받은 하위 객체는 프로퍼티와 메서드를 자유롭게 사용


    ❗️프로토타입 체인: 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조로, 프로토타입 체인을 따라 프로토타입의 프로퍼티와 메서드 차례대로 검색함!
    ⇒ 접근하려고 하는 객체에 프로퍼티 또는 메서드가 없는 경우 위로 올라가면서 검색

  3. 검색된 프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인
  4. 접근자 프로퍼티인 경우 해당 프로퍼티 어트리뷰트 [[Get]]의 값인 getter 함수 호출하여 결과 반환

16.4 프로퍼티 정의

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

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

프로퍼티 디스크립터 객체의 프로퍼티대응하는 프로퍼티 어트리뷰트생략했을 때 기본값
value[[Value]]undefined
get[[Get]]undefined
set[[Set]]undefined
writable[[Writable]]false
enumerable[[Enumerable]]false
configurable[[Configurable]]false
const person = {}; // 빈 객체

// 데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
  value: 'Jjanggu',
  writable: true,
  enumerable: true,
  configurable: true,
});
Object.defineProperty(person, 'lastName', {
  value: 'Shin',
});
console.log('firstName', Object.getOwnPropertyDescriptor(person, 'firstName')); // firstName { value: 'Jjanggu', writable: true, enumerable: true, configurable: true }
// 디스크립터 객체의 프로퍼티 누락시키면 undefined, false가 기본값!
console.log('lastName', Object.getOwnPropertyDescriptor(person, 'lastName')); // lastName { value: 'Shin', writable: false, enumerable: false, configurable: false }

// lastName 프로퍼티는 [[Enumerable]]의 값이 false이므로 열거되지 X
console.log(Object.keys(person)); // [ 'firstName' ]

// [[Writable]]의 값이 false인 경우 해당 프로퍼티의 값 변경 X
// false인데 값 변경하려고 하면 에러는 발생하지 않고 무시
person.lastName = 'Kim';

// [[Configurable]]의 값이 false인 경우 해당 프로퍼티의 값 삭제 X
// false인데 값 삭제하려고 하면 에러는 발생하지 않고 무시
delete person.lastName;

// [[Configurable]]의 값이 false인 경우 해당 프로퍼티 재정의 X
Object.defineProperty(person, 'lastName', { enumerable: true }); // TypeError: Cannot redefine property: lastName
const person = {}; // 빈 객체

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

console.log(Object.getOwnPropertyDescriptor(person, 'fullName')); // { get: [Function: get], set: [Function: set], enumerable: true, configurable: true }

person.fullName = 'HiHi Kim';
console.log(person); // { fullName: [Getter/Setter], firstName: 'HiHi', lastName: 'Kim' }

Object.defineProperties 메서드 사용하면 여러 개의 프로퍼티 한 번에 정의 가능

const person = {}; // 빈 객체

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

person.fullName = 'HaHa Kim';
console.log(person); // { firstName: 'HaHa', fullName: [Getter/Setter], lastName: 'Kim' }

16.5 객체 변경 방지

  • 객체는 변경 가능한 값이므로 재할당 없이 직접 변경 가능 ⇒ 프로퍼티 추가 혹은 삭제, 갱신 가능, Object.definePropertyObject.defineProperties 메서드 사용하여 프로퍼티 어트리뷰트 재정의 가능
  • 객체 변경 방지하는 메서드 존재
    구분메서드프로퍼티 추가프로퍼티 삭제프로퍼티 값 읽기(getter)프로퍼티 값 쓰기(setter)프로퍼티 어트리뷰트 재정의
    객체 확장 금지Object.preventExtensionsXOOOO
    객체 밀봉Object.sealXXOOX
    객체 동결Object.freezeXXOXX
  1. 객체 확장 금지, Object.preventExtensions
  • 프로퍼티 동적 추가와 Object.defineProperty 메서드 추가 X
  • Object.isExtensible 메서드로 확장이 가능한 객체인지 확인 가능
    const person = { name: 'Lee' };
    
    console.log(Object.isExtensible(person)); // true
    
    // person 객체의 확장 금지하여 프로퍼티 추가를 금지
    **Object.preventExtensions(person);**
    
    console.log(Object.isExtensible(person)); // false
    
    // 프로퍼티 추가 X
    person.age = 20; // 무시
    console.log(person); // { name: 'Lee' }
    
    // 프로퍼티 추가 X
    Object.defineProperty(person, 'age', { value: 20 }); // TypeError: Cannot define property age, object is not extensible
  1. 객체 밀봉, Object.seal
  • 프로퍼티 추가 및 삭제, 어트리뷰트 재정의 X
  • 오로지 읽기와 쓰기만 가능
  • Object.isSealed 메서드로 밀봉된 객체인지 확인 가능
    const person = { name: 'Lee' };
    
    console.log(Object.isSealed(person)); // false
    
    // person 객체 밀봉하여 프로퍼티 추가, 삭제, 재정의 금지
    Object.seal(person);
    
    console.log(Object.isSealed(person)); // true
    
    person.age = 20; // 무시
    delete person.name; // 무시
    Object.defineProperty(person, 'name', { configurable: true }); // TypeError: Cannot redefine property: name
    
    person.name = 'Kim'; // 프로퍼티 값 갱신은 가능
    console.log(person); // { name: 'Kim' }
  1. 객체 동결, Object.freeze
  • 프로퍼티 추가, 삭제, 재정의, 값 갱신 X
  • 오로지 읽기만 가능!
  • Object.isFrozen 메서드로 동결된 객체인지 확인 가능
    const person = { name: 'Lee' };
    
    console.log(Object.isFrozen(person)); // false
    
    // person 객체를 동결하여 프로퍼티 추가, 삭제, 재정의, 쓰기 금지
    Object.freeze(person);
    
    console.log(Object.isFrozen(person)); // true
    
    person.age = 20; // 무시
    delete person.name; // 무시
    person.name = 'Kim'; // 무시
    Object.defineProperty(person, 'name', { configurable: true }); // TypeError: Cannot redefine property: name
  1. 불변 객체

    • Object.preventExtensions, Object.seal, Object.freeze는 얕은 변경 방지로 직속 프로퍼티만 변경 방지됨! 중첩 객체까지 동결 X
    • 따라서, 객체의 중첩 객체까지 동결하여 변경 불가능한 읽기 전용 불변 객체로 만들기 위해서는 모든 프로퍼티에 대해 재귀적으로 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);

0개의 댓글