[JavaScript] 10. Property Attribute

100tick·2022년 12월 23일
0

JavaScript Deep Dive

목록 보기
10/16
post-thumbnail

1. Internal Slots of Objects

ECMAScript 스펙에 등장하는 [[ ]]로 감싼 내부 슬롯, 내부 메소드들은 이름 그대로 개발자가 직접 접근 및 호출할 수 없는 영역이다.
그러나 일부 내부 슬롯, 메소드에 한해서 마치 getter를 이용해서 private property의 값을 받아오듯, 간접적으로 접근할 수 있는 방법을 제공하고 있다.

const obj = {};

obj.[[Prototype]]; // Uncaught SyntaxError: Unexpected token '['
obj.__proto__ // -> Object.prototype

[[Prototype]]은 내부 슬롯이므로 직접 접근할 수 없지만 __proto__를 통해 간접적으로 내부 슬롯에 접근할 수 있는 것을 볼 수 있다.


2. Property Attributes and Property Descriptor Objects

JS 엔진은 Property를 생성할 때, Property 상태를 나타내는 Attribute를 기본값으로 자동 정의한다.

Property 상태란 Property의 값(value), 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable) 등을 말한다.
Property Attribute들은 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]와 같은 내부 슬롯 형태로 저장되어 직접 접근할 수 없지만, Object.getOwnPropertyDescriptor 메소드를 통해 간접적으로 확인할 수 있다.

const obj = {
	value: 1,
};

Object.getOwnPropertyDescriptor(obj, "value"); // {value: 1, writable: true, enumerable: true, configurable: true}

참조할 객체, Property Key와 함께 호출하면 PropertyDescriptor 객체를 반환한다.
존재하지 않는 Property 혹은 상속받은 Property인 경우, undefined가 반환된다.

const obj = {
	value1: 1,
  	value2: 2,
    value3: 3,
};

Object.getOwnPropertyDescriptors(obj); // {value1: {...}, value2: {...}, value3: {...}}

ES8부터 도입된 Object.getOwnPropertyDescriptors 메소드를 통해 모든 Property Attributes에 대한 Property Descriptor 객체를 한 번에 받아올 수 있다.


3. Data Property, Accessor Property

Object.getOwnPropertyDescriptor 메소드로 받아왔던 Property들을 Data Property라고 한다.

조금 더 자세히 살펴보자.

[[Value]], [[Writable]], [[Enumerable]], [[Configurable]]는 내부 슬롯으로 접근 불가능.
그래서 메소드를 통해 value, writable, enumerable, configurable 등을 갖고 있는 Property Descriptor 객체를 통해 간접적으로 접근했었음.

이들은 Property가 생성될 때 자동으로 초기화 됨.

const obj = { value: 1 };

위의 obj를 생성할 때, value라는 Property가 추가되고, 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]들도 이 시점에 초기화 되는 것.

이외에 Accessor Property가 존재하는데, [[Get]], [[Set]] 등이 바로 그것이다.

const person = {
	firstName: "a",
  	lastName: "b",
  
	get fullName() {
		return `${this.firstName} ${this.lastName}`; 
    }
  
	set fullName(name) {
		[this.firstName, this.lastName] = name.split(' '); 
    }
};

let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
// {value: "Heegun", writable: true, enumerable: true, configurable: true}

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

personfirstName, lastName은 일반적인 Data Property고, getter, setterfullNameAccessor Property다.

직접 값을 갖지 않고 Data Property의 값을 읽어서 반환하거나 Data Property의 값을 갱신하는 역할만 한다.

이 과정을 자세히 살펴보자.
1. Accessor PropertyfullName에 접근하면 내부 메소드 [[Get]]이 호출된다.
2. Property Key(fullName)가 유효한지 확인한다.
3. Prototype Chain에서 fullName Property가 존재하는지 검색한다.
4. fullNameData or Accessor Property인지 확인한다. Accessor Property다.
5. [[Get]]을 호출하여 결과를 반환한다.

Accessor, Data Property를 구별하는 방법은 아래와 같다.

Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

Object.getOwnPropertyDescriptor(function() {}, 'prototype');
// {value: {...}, writable: true, enumerable: false, configurable: false}

그냥 특정 Property를 찍어 보면 그 Property가 갖고 있는 Property Attributes를 포함한 object가 출력된다.

get, set이 있으면 Accessor Property, value가 있으면 Data Property일 것이다.

참고로 [[Enumerable]], [[Configurable]]Data Property이면서 Accessor Property가 되기도 한다고 한다.(이 부분은 서적에서 자세히 설명하지 않고 있어서 잘은 모르겠다)


4. Object.defineProperty

지금까지 object에 동적으로 Property를 추가할 때는 아래와 같이 했다.

const person = {};
person.firstName = "a";

그러나 Property 내부의 Attribute까지 손대려면 다른 방법을 사용해야 한다.

const person = {};
// 데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
 value: 'a',
 writable: true,
 enumerable: true,
 configurable: true
});
Object.defineProperty(person, 'lastName', {
 value: 'b'
});

person.firstName = "a";와 같이 일반적으로 사용되는 동적 Property 추가 방식으로 생성되면 Attributewritable: true, enumerable: true, configurable: true와 같이 자동으로 true로 설정된다.

그러나 Object.defineProperty로 생성된 Property는 명시적으로 true로 설정하지 않거나 누락되면 false로 설정하기 때문에 2번째 코드의 person.lastNameAttributewritable: false, enumerable: false, configurable: false가 된다.

[[Enumerable]]은 뒤에서 볼 for ... in, Object.keys 등으로 열거 가능한지 여부를 뜻하고 [[Writable]]은 해당 Property[[Value]]의 변경 여부를 뜻하며 [[Configurable]]false인 경우 해당 Property를 삭제, 재정의할 수 없음을 뜻한다.

마찬가지로 Accessor Property도 아래와 같이 Object.defineProperty를 통해 설정할 수 있다.

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

Object.defineProperties를 사용하면 다수의 Property들을 한 번에 설정할 수 있다.

Object.defineProperties(person, {
   firstName: {
   value: 'Ungmo',
   writable: true,
   enumerable: true,
   configurable: true
   },
   lastName: {
   value: 'Lee',
   writable: true,
   enumerable: true,
   configurable: true
   },
});

5. Object Immutability

JS에서 Primitive Type의 변수에 값을 대입하면, 이전의 메모리 공간을 버리고 운영체제로부터 새로운 공간을 할당 받아 사용한다.
그래서 Immutable하다고 한다.

반대로 유일한 Reference Type, object변경 가능하다.
Property 추가, 삭제, 갱신이 모두 가능하며, Object.defineProperty를 통해 Attribute 재정의도 가능하다.

그러나 이를 절대 변경해서는 안 되는 경우도 있기 때문에, object의 변경을 막는 다양한 방법도 존재한다.

1. Object.preventExtensions

말 그대로 object가 확장하는 것을 막는다.
아래 코드를 보자.

const person = { name: 'Lee' };
// person 객체는 확장이 금지된 객체가 아니다.
console.log(Object.isExtensible(person)); // true
// person 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person);
// 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

seal은 봉한다는 뜻.
Property 추가, 삭제, Attribute 재정의를 금하며 읽기, 쓰기만 가능하다.

const person = { name: 'Lee' };
// person 객체는 밀봉(seal)된 객체가 아니다.
console.log(Object.isSealed(person)); // false
// person 객체를 밀봉(seal)하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.seal(person);
// person 객체는 밀봉(seal)된 객체다.
console.log(Object.isSealed(person)); // true
// 밀봉(seal)된 객체는 configurable이 false다.
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

Object.seal에서 한단계 더 나아가 쓰기조차 금지된다.
고로 읽기만 가능한 object가 된다.

const person = { name: 'Lee' };
// person 객체는 동결(freeze)된 객체가 아니다.
console.log(Object.isFrozen(person)); // false
// person 객체를 동결(freeze)하여 프로퍼티 추가, 삭제, 재정의, 쓰기를 금지한다.
Object.freeze(person);
// person 객체는 동결(freeze)된 객체다.
console.log(Object.isFrozen(person)); // true
// 동결(freeze)된 객체는 writable과 configurable이 false다.
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 변경을 방지하기 위한 모든 대책은 Child Property에만 적용된다.
만약 object 내부에 또 다른 object가 있다면, 적용되지 않는다.

그래서 각 내부 object에 대해 재귀적으로 Object.freeze를 호출해야 한다.

0개의 댓글