프로퍼티 어트리뷰트(property attribute)

Yoo Jong Hyeon·2023년 5월 23일
0

Front-end & JS

목록 보기
1/8
post-thumbnail

내부 슬롯(internal slot), 내부 메서드(internal method)

내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(pseudo property)의사 메서드(pseudo method)이다.

ECMAScript 사양에 등장하는 이중 대괄호로 감싼 이름들이 모두 내부 슬롯과 내부 메서드이다.

Object Internal Methods and Internal Slots

내부 슬롯과 내부 메서드는 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티가 아니다. 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 내부 로직이므로, 원칙적인 규칙은 내부 슬롯과 내부 메서드에 개발자가 직접 접근 및 호출하는 방법은 제공되지 않는다.

그러나 일부 내부 슬롯과 내부 메서드는 간접적으로 접근할 수 있는 수단을 제공한다.

예를 들어 자바스크립트의 모든 객체는 [[prototype]] 이라는 내부 슬롯을 갖는다. [[prototype]] 내부 슬롯의 경우, __proto__ 를 통해 간접적으로 접근할 수 있다.


const o = {};

o.__proto__ // Object.prototype

프로퍼티 디스크립터 객체(property descriptor object)

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

이 때 상태(status)란 다음을 뜻한다.

  • 프로퍼티의 값(value)
  • 값의 갱신 가능 여부(writable)
  • 열거 가능 여부(enumerable)
  • 재정의 가능 여부(configurable)

즉 프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값(meta-property)인 내부 슬롯이다.

  • [[Value]]
  • [[Writable]]
  • [[Enumerable]]
  • [[Configurable]]

이 프로퍼티 어트리뷰트는 개발자가 직접 접근할 수 없지만, Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인이 가능하다.

const student = {
  name: 'jong'
};

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

getOwnPropertyDescriptor 메서드의 첫 번째 파라미터는 객체의 참조(Reference)를 전달하고, 두 번째 파라미터는 프로퍼티 키를 문자열로 전달한다.

이 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 객체를 반환하는데, 이 객체를 프로퍼티 디스크립터 객체(property descriptor object)라 한다.

존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면 undefined가 리턴된다.

ES8에서 도입된 Object.getOwnPropertyDescriptors 메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.

const student = {
  name: 'jong',
  age : 23
};

console.log(Object.getOwnPropertyDescriptors(student)); // ->
// {
//   name: {
//     value: 'jong',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   age: {
//     value: 23, 
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

데이터 프로퍼티(data property)

키(key)와 값(value)으로 구성된 일반적인 프로퍼티이다.

[[Value]]

프로퍼티 키(property key)를 통해 프로퍼티 값(property value)에 접근하면 반환되는 값.

프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]]에 값을 재 할당한다. 또한 프로퍼티가 생성될 때 [[Value]]의 값은 프로퍼티 값으로 초기화 된다.

만약 프로퍼티가 존재하지 않는다면, 프로퍼티를 동적 생성하고, 생성된 프로퍼티의 [[Value]]에 값을 저장한다.

[[Writable]]

프로퍼티 값(property value)의 변경 가능 여부를 나타내며, 불리언(boolean) 값을 갖는다.

[[Writable]]의 값은 기본적으로 true로 초기화 된다.

[[Writable]]의 값이 false인 경우, [[value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티가 된다.

[[Enumerable]]

프로퍼티의 열거(enumeration) 가능 여부를 나타내며, 불리언(boolean) 값을 갖는다.

[[Enumerable]]의 값은 기본적으로 true로 초기화 된다.

[[Enumerable]]의 값이 false인 경우, for ... in 문이나, Object.keys 메서드 등으로 열거할 수 없다.

[[Configurable]]

프로퍼티의 재정의 가능 여부를 나타내며, 불리언 값을 갖는다.

[[Configurable]]의 값은 기본적으로 true로 초기화 된다.

[[Configurable]]의 값이 false인 경우, 프로퍼티 어트리뷰트 값의 변경과 삭제가 금지된다.

[[Writable]]이 true인 경우, [[Value]]의 변경과 [[Writable]]의 값을 false로 변경하는 것은 허용된다.


접근자 프로퍼티(accessor property)

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

[[Get]]

접근자 프로퍼티(accessor property)를 통해 데이터 프로퍼티(data property)의 값을 읽을 때, 호출되는 접근자 함수이다.

접근자 프로퍼티 키로 프로퍼티 값([[Value]])에 접근하면, 프로퍼티 어트리뷰트 [[Get]]의 값이 반환된다.

이는 접근자 함수인 getter 함수가 호출되기 때문이다.

[[Set]]

접근자 프로퍼티(accessor property)를 통해 데이터 프로퍼티(data property)의 값을 저장할 때, 호출되는 접근자 함수이다.

접근자 프로퍼티 키로 프로퍼티 값([[Value]])을 저장하면, 프로퍼티 어트리뷰트 [[Set]]의 값이 저장된다.

이는 접근자 함수인 setter 함수가 호출되기 때문이다.

[[Enumerable]]

프로퍼티의 열거(enumeration) 가능 여부를 나타내며, 불리언(boolean) 값을 갖는다.

[[Enumerable]]의 값은 기본적으로 true로 초기화 된다.

[[Enumerable]]의 값이 false인 경우, for ... in 문이나, Object.keys 메서드 등으로 열거할 수 없다.

[[Configurable]]

프로퍼티의 재정의 가능 여부를 나타내며, 불리언 값을 갖는다.

[[Configurable]]의 값은 기본적으로 true로 초기화 된다.

[[Configurable]]의 값이 false인 경우, 프로퍼티 어트리뷰트 값의 변경과 삭제가 금지된다.

[[Writable]]이 true인 경우, [[Value]]의 변경과 [[Writable]]의 값을 false로 변경하는 것은 허용된다.


생성자 함수(constructor function)

함수는 객체이므로 일반 객체(ordinary object)와 동일하게 동작이 가능하다. 당연히 함수 객체 또한 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드를 모두 가지고 있다.

그러나 함수는 일반 객체와는 달리 호출(invoke)이 가능하다. 따라서 함수는 일반 객체보다 추가적인 내부 슬롯(internal slot)과 내부 메서드(internal method)를 가지고 있다.

추가적으로 갖는 내부 슬롯은 아래와 같다.

[[Enviroment]]

[[formalParameters]]

추가적으로 갖는 내부 메서드는 아래와 같다.

[[Call]]

함수를 일반 함수로서 호출하면, 함수 객체의 내부 메서드 [[Call]]이 호출된다.

[[Construct]]

함수를 new 연산자와 함께 생성자 함수로서 호출하면 내부 메서드 [[Construct]]가 호출된다. 이 때 non-cnstructor인 함수 객체는 [[Construct]] 내부 메서드를 갖지 않으므로, 에러가 발생한다.

이를 정리하면 아래와 같다.

내부 메서드 [[Call]]을 갖는 함수 객체를 callable이라 한다.

내부 메서드 [[Construct]]를 갖는 함수 객체를 constructor이라 한다.

내부 메서드 [[Construct]]를 가지고 있지 않은 함수 객체를 non-constructor이라 한다.

호출할 수 없는 함수는 상상하기 힘들다. 즉 함수 객체는 반드시 callable이어야 한다. 그러나 모든 함수 객체가 constructor인 것은 아니다. 만약 어떤 함수 객체가 constructor이라면, 생성자 함수로서 호출할 수 있는 함수라는 것이다.

자바스크립트 엔진이 함수를 constructor로 분류하는 기준은 다음과 같다.

  • 함수 선언문(function declarations)
  • 함수 표현식(function expression)
  • 클래스(class)

자바스크립트 엔진이 함수를 non-constructor로 분류하는 기준은 다음과 같다.

  • 메서드(method)
  • 화살표 함수(arrow function)

프로퍼티 정의

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

이는 프로퍼티 값의 갱신 여부, 열거 가능 여부, 재정의 가능 여부를 정의한다는 뜻이다.

프로퍼티의 어트리뷰트를 정의하기 위해 Object.defineProperty 메서드를 사용한다. 이 메서드는 파라미터로 객체의 참조, 데이터 프로퍼티의 키(문자열), 프로퍼티 디스크립터 객체를 전달한다.

let student = {};

Object.defineProperty(student, 'name', 
{
  value: 'jong',
  writable: true,
  enumerable: true,
  configurable: true
});

Object.defineProperty(student, 'age',
{
  value : 23,
  writable: false,
  enumerable: false,
  configurable: false
});

console.log(student); // { name: 'jong' }

// [[Enumerable]]의 값이 false인 경우 열거할 수 없다.
console.log(Object.keys(student)); // [ 'name' ]

// [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없다.
student.age = 33
console.log(student.age); // 23

 // [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 삭제할 수 없다.
delete student.age; // 다음 코드는 무시 된다.

// [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 재정의 할 수 없다.
// Object.defineProperty(student, 'age', { enumerable: true});
// TypeError: Cannot redefine property: age
//     at Function.defineProperty (<anonymous>)
//     at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:28:8)
//     at Module._compile (node:internal/modules/cjs/loader:1126:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
//     at Module.load (node:internal/modules/cjs/loader:1004:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:839:12)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
//     at node:internal/main/run_main_module:17:47

Object.defineProperty(student, 'infomation',
{
  get(){
    return `name is ${this.name}, age is ${this.age}`;
  },
  set(info){
    [this.name, this.age] = info.split(' ');
  },
  enumerable: true,
  configurable: true
});

console.log(student.infomation);

또한 Object.defineProperties 메서드를 사용하면, 여러 개의 프로퍼티를 한 번에 정의할 수 있다.


객체 변경 금지 메서드

객체는 기본적으로 변경 가능한 값으로 이루어져 있다. 따라서 재할당 없이 직접 변경할 수 있다.

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

Object.preventExtensions

객체의 확장(extend)을 금지한다.

확장이 금지된 객체는 다음과 같은 작업을 수행할 수 없다.

  • 프로퍼티 추가

프로퍼티를 추가하는 방법은 프로퍼티 동적 추가와 Object.defineProperty 메서드가 있는데, 이 두 가지 방법이 모두 금지된다.

let student = {
  name: 'jong'
}

console.log(Object.isExtensible(student)); // true

Object.preventExtensions(student);

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

// 해당 코드는 무시된다.
student.age = 23;

// 프로퍼티 삭제는 가능하다.
delete student.name;
console.log(student); // {}

Object.defineProperties(student, 'age', { value: 23});
// TypeError: Property description must be an object: a
//     at Function.defineProperties (<anonymous>)
//     at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:18:8)
//     at Module._compile (node:internal/modules/cjs/loader:1126:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
//     at Module.load (node:internal/modules/cjs/loader:1004:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:839:12)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
//     at node:internal/main/run_main_module:17:47

Object.seal

객체를 밀봉(seal)한다.

밀봉된 객체는 다음과 같은 작업을 수행할 수 없다.

  • 프로퍼티 추가
  • 프로퍼티 삭제
  • 프로퍼티 어트리뷰트 재정의

이는 밀봉된 객체는 오직 읽기와 쓰기만 가능하다는 것을 뜻한다. 따라서 밀봉된 객체는 configurable이 false 이다.

let student = {
  name: 'jong'
}

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

Object.seal(student);

console.log(Object.isSealed(student)); // false

// 해당 코드는 무시된다.
student.age = 23;

// 해당 코드는 무시된다.
delete student.name;
console.log(student); // {name: 'jong' }

Object.defineProperties(student, 'age', { value: 23});
// TypeError: Property description must be an object: a
//     at Function.defineProperties (<anonymous>)
//     at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:18:8)
//     at Module._compile (node:internal/modules/cjs/loader:1126:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
//     at Module.load (node:internal/modules/cjs/loader:1004:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:839:12)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
//     at node:internal/main/run_main_module:17:47

Object.freeze

객체를 동결(freeze) 한다.

동결된 객체는 다음과 같은 작업을 수행할 수 없다.

  • 프로퍼티 추가
  • 프로퍼티 삭제
  • 프로퍼티 어트리뷰트 재정의
  • 프로퍼티 값 갱신

즉 동결된 객체는 오직 읽기만 가능하다. 따라서 동결된 객체는 writable과 configurable이 false 이다.

let student = {
  name: 'jong'
}

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

Object.freeze(student);

console.log(Object.isFrozen(student)); // false

// 해당 코드는 무시된다.
student.age = 23;

// 해당 코드는 무시된다.
delete student.name;
console.log(student); // { name: 'jong' }

// 해당 코드는 무시된다.
student.name = 'park'
console.log(student.name) // { name: 'jong' }

Object.defineProperties(student, 'age', { value: 23});
// TypeError: Property description must be an object: a
//     at Function.defineProperties (<anonymous>)
//     at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:18:8)
//     at Module._compile (node:internal/modules/cjs/loader:1126:14)
//     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
//     at Module.load (node:internal/modules/cjs/loader:1004:32)
//     at Function.Module._load (node:internal/modules/cjs/loader:839:12)
//     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
//     at node:internal/main/run_main_module:17:47

불변 객체(immutable object)

위에서 살펴본 변경 방지 메서드들은 얕은 변경 방지(shallow only)만을 지원한다. 이는 직속 프로퍼티의 변경만을 방지해주고, 중첩 객체까지는 영향을 주지 못한다는 뜻이다.

따라서 객체의 중첩 객체까지 동결(freeze)하여, 변경이 불가능한 읽기 전용의 완전한 불변 객체(immutable object)를 구현하기 위해선 모든 프로퍼티에 대해 Object.freeze 메서드를 호출하여 객체의 불변식(invariant)을 만족시켜야 한다.

function deepFreeze(target){
  if(typeof target === 'object' && !Object.isFrozen(target)){
    Object.freeze(target);

    Object.keys(target).forEach(key => deepFreeze(target[key]));
  }

  return target;
}

위 함수는 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출한다.

Java에서 불변 객체를 만드는 방법


Reference

모던 자바스크립트 Deep Dive
이웅모 저 | 위키북스 | 2020년 09월 25일

0개의 댓글

관련 채용 정보