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

unhyif·2022년 7월 2일
1

16.1 내부 슬롯과 내부 메서드

JS 엔진의 내부 로직을 위해 ECMAScript 사양에 정의된 pseudo property와 pseudo method

JS를 통해 내부 슬롯/메소드에 직접적으로 접근할 수는 없지만, 간접적으로 일부에 접근할 수 있음

e.g. [[Prototype]] 내부 슬롯에 __proto__를 통해 접근함

const o = {};

console.log(o.[[Prototype]]) // Uncaught SyntaxError: Unexpected token '['
console.log(o.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

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

JS 엔진이 property 생성 시, property attribute를 기본값으로 자동 정의함

Property attribute: property의 상태를 나타내는 내부 슬롯

e.g. [[Value]], [[Writable]], [[Enumerable]], [Configurable]]


  • Object.getOwnPropertyDescriptor: 한 property의 property attribute 정보를 제공하는 PropertyDescriptor 객체를 반환함
const person = {
  name: "J",
};

console.log(Object.getOwnPropertyDescriptor(person, "name")); // {value: 'J', writable: true, enumerable: true, configurable: true}
  • Object.getOwnPropertyDescriptors: 모든 property의 PropertyDescriptor 객체들을 반환함
const person = {
  firstName: "J",
};
person.lastName = "H";

console.log(Object.getOwnPropertyDescriptors(person));
//  {
//     "firstName": {
//         "value": "J",
//         "writable": true,
//         "enumerable": true,
//         "configurable": true
//     },
//     "lastName": {
//         "value": "H",
//         "writable": true,
//         "enumerable": true,
//         "configurable": true
//     }
// }

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

16.3.1 데이터 프로퍼티

키와 값으로 구성된 property

Property attributes

  • [[Value]]: key를 통해 property에 접근 시 반환되는 값
  • [[Writable]]: 값 변경 가능 여부
    • false인 경우 읽기 전용 property가 됨
  • [[Enumerable]]: property의 열거 가능 여부
    • false인 경우 for ... in 또는 Object.keys로 property를 열거할 수 없음
  • [[Configurable]]: property의 재정의 가능 여부
    • false인 경우 property를 삭제 or 값 변경이 불가능함
      • but [[Writable]]true라면 값 변경은 가능함

16.3.2 접근자 프로퍼티

값이 아닌 접근자 함수로 구성된 property로, data property의 값을 참조/저장 시 사용됨

Property attributes

  • [[Get]]: data property 값 참조 시 호출되는 접근자 함수(getter)
  • [[Set]]: data property 값 저장 시 호출되는 접근자 함수(setter)
  • [[Enumerable]], [[Configurable]]
const person = {
  // 데이터 프로퍼티
  firstName: "J",
  lastName: "K",

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

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
person.fullName = "first last";
console.log(person); // {firstName: 'first', lastName: 'last'}

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.fullName); // first last

// 데이터 프로퍼티의 property attributes
console.log(Object.getOwnPropertyDescriptor(person, "firstName")); // {value: 'first', writable: true, enumerable: true, configurable: true}

// 접근자 프로퍼티의 property attributes
console.log(Object.getOwnPropertyDescriptor(person, "fullName")); // {enumerable: true, configurable: true, get: ƒ, set: ƒ}

16.4 프로퍼티 정의

Property attribute를 정의하는 것

  • Object.defineProperty: 한 property의 property attribute를 정의
const person = {};

Object.defineProperty(person, "firstName", {
  value: "J",
  writable: true,
  enumerable: true,
  configurable: true,
});

// Descriptor 객체의 property를 일부 생략
Object.defineProperty(person, "lastName", {
  value: "K",
});

console.log(Object.getOwnPropertyDescriptor(person, "firstName")); // {value: 'J', writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(person, "lastName")); // {value: 'K', writable: false, enumerable: false, configurable: false}

// writable === false
person.lastName = "L"; // lastName의 값이 변경되지 않고, 에러가 발생하지 않음
console.log(person.lastName); // K

// enumerable === false
// lastName을 열거할 수 없음
console.log(Object.keys(person)); // ['firstName']

// configurable === false
delete person.lastName; // lastName을 삭제할 수 없고, 에러가 발생하지 않음
console.log(person); // {firstName: 'J', lastName: 'K'}
// lastName을 재정의할 수 없음
Object.defineProperty(person, "lastName", {
  writable: true,
}); // j.js:28 Uncaught TypeError: Cannot redefine property: lastName
const person = { firstName: "J", lastName: "K" };

Object.defineProperty(person, "fullName", {
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  set(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
  enumerable: true,
  configurable: true,
});

person.fullName = "H L";
console.log(person); // {firstName: 'H', lastName: 'L'}
  • Object.defineProperties: 여러 property의 property attribute를 정의
const person = {};

Object.defineProperties(person, {
  firstName: {
    value: "J",
    writable: true,
    enumerable: true,
    configurable: true,
  },

  lastName: {
    value: "K",
    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 = "H L";
console.log(person); // {firstName: 'H', lastName: 'L'}

16.5 객체 변경 방지

16.5.1 객체 확장 금지

  • Object.preventExtensions: property 추가 금지
    • Object.isExtensible: 확장 가능한 객체인지 확인
const person = {
  firstName: "J",
};
console.log(Object.isExtensible(person)); // true

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

// property 추가 금지 but strict mode 아니면 에러 X
person.lastName = "K";
console.log(person); // {firstName: 'J'}

16.5.2 객체 밀봉

  • Object.seal: property 값 읽기/쓰기만 가능
    • Object.isSealed: 밀봉된 객체인지 확인

cf) 밀봉된 객체의 configurable property attribute는 false

const person = {
  firstName: "J",
};
console.log(Object.isSealed(person)); // false

Object.seal(person);
console.log(Object.isSealed(person)); // true
console.log(Object.getOwnPropertyDescriptors(person));
// {
//     "firstName": {
//         "value": "J",
//         "writable": true,
//         "enumerable": true,
//         "configurable": false
//     }
// }

// property 값 읽기/쓰기 가능
person.firstName = "H";
console.log(person.firstName); // H

// property 추가 금지 but strict mode 아니면 에러 X
person.lastName = "K";
console.log(person); // {firstName: 'H'}

// property 삭제 금지 but strict mode 아니면 에러 X
delete person.firstName;
console.log(person); // {firstName: 'H'}

// property attribute 재정의 금지
Object.defineProperty(person, "firstName", {
  configurable: true,
}); // Uncaught TypeError: Cannot redefine property: firstName

16.5.3 객체 동결

  • Object.freeze: property 값 읽기만 가능
    • Object.isFrozen: 동결된 객체인지 확인

cf) 동결된 객체의 writable/configurable property attribute는 false

const person = {
  name: "J",
};
console.log(Object.isFrozen(person)); // false

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

// property 값 쓰기 금지 but strict mode 아니면 에러 X
person.name = "H";
console.log(person.name); // J

16.5.4 불변 객체

위의 객체 변경 방지 메소드들은 얕은 변경만 방지, 즉 직속 property만 변경이 방지된다.

const person = {
  name: {
    first: "J",
    last: "K",
  },
};

Object.freeze(person);
console.log(Object.isFrozen(person.name)); // false
person.name.first = "H";
console.log(person.name.first); // H

따라서 중첩 객체까지 동결된 읽기 전용의 불변 객체를 구현하려면, 객체를 값으로 갖는 모든 property에 대해 재귀적으로 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: {
    first: "J",
    last: "K",
  },
};

deepFreeze(person);
person.name.first = "H";
console.log(person.name.first); // "J"

0개의 댓글