ECMAScript 스펙에 등장하는 [[ ]]로 감싼 내부 슬롯, 내부 메소드들은 이름 그대로 개발자가 직접 접근 및 호출할 수 없는 영역이다.
그러나 일부 내부 슬롯, 메소드에 한해서 마치 getter를 이용해서 private property의 값을 받아오듯, 간접적으로 접근할 수 있는 방법을 제공하고 있다.
const obj = {};
obj.[[Prototype]]; // Uncaught SyntaxError: Unexpected token '['
obj.__proto__ // -> Object.prototype
[[Prototype]]은 내부 슬롯이므로 직접 접근할 수 없지만 __proto__
를 통해 간접적으로 내부 슬롯에 접근할 수 있는 것을 볼 수 있다.
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 객체를 한 번에 받아올 수 있다.
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}
person
의 firstName
, lastName
은 일반적인 Data Property고, getter
, setter
인 fullName
이 Accessor Property다.
직접 값을 갖지 않고 Data Property의 값을 읽어서 반환하거나 Data Property의 값을 갱신하는 역할만 한다.
이 과정을 자세히 살펴보자.
1. Accessor Property인 fullName
에 접근하면 내부 메소드 [[Get]]이 호출된다.
2. Property Key(fullName
)가 유효한지 확인한다.
3. Prototype Chain에서 fullName
Property가 존재하는지 검색한다.
4. fullName
이 Data 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가 되기도 한다고 한다.(이 부분은 서적에서 자세히 설명하지 않고 있어서 잘은 모르겠다)
지금까지 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 추가 방식으로 생성되면 Attribute가 writable: true, enumerable: true, configurable: true
와 같이 자동으로 true
로 설정된다.
그러나 Object.defineProperty
로 생성된 Property는 명시적으로 true
로 설정하지 않거나 누락되면 false
로 설정하기 때문에 2번째 코드의 person.lastName
의 Attribute는 writable: 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
},
});
JS에서 Primitive Type의 변수에 값을 대입하면, 이전의 메모리 공간을 버리고 운영체제로부터 새로운 공간을 할당 받아 사용한다.
그래서 Immutable하다고 한다.
반대로 유일한 Reference Type, object
는 변경 가능하다.
Property 추가, 삭제, 갱신이 모두 가능하며, Object.defineProperty
를 통해 Attribute 재정의도 가능하다.
그러나 이를 절대 변경해서는 안 되는 경우도 있기 때문에, object
의 변경을 막는 다양한 방법도 존재한다.
말 그대로 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
삭제는 확장이 아닌 축소이므로 가능하다.
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
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
를 호출해야 한다.