16장 프로퍼티 어트리뷰트
16.1 내부 슬롯과 내부 메서드
- ECMAScript 사양에서 이중 대괄호([[...]])로 감싼 이름들이 내부 슬롯과 내부 메서드다.
- 내부 슬롯과 내부 메서드는 외부로 공개된 객체의 프로퍼티가 아니기 때문에, 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않는다.
- 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공한다.
- 예를 들어, 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. 원칙적으로 접근할 수 없지만 proto 를 통해 간접적으로 접근할 수 있다.
const o = {};
o.[[Prototype]]
o.__proto__
16.2 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체
- 자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
- 프로퍼티 어트리뷰트 : 자바스크립트 엔진이 관리하는 내부 상태 값 내부슬롯
- [[Value]] : 값
- [[Writable]] : 값의 갱신 여부
- [[Enumerable]] : 열거 가능 여부
- [[Configurable]] : 재정의 가능 여부
- 직접 접근 할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수 있다.
- Object.getOwnPropertyDescriptor(객체, 프로퍼티 키)를 사용하면, 프로퍼티 디스크립터 객체를 반환한다. 존재하지 않는 프로퍼티나 상속받는 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면 undefined가 반환된다.
const person = {
name: 'Lee',
};
person.age = 20;
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
16.3 데이터 프로퍼티와 접근자 프로퍼티
- 프로퍼티의 종류
- 데이터 프로퍼티 : 키와 값으로 구성된 일반적인 프로퍼티. 대부분 모든 프로퍼티는 데이터 프로퍼티다.
- 접근자 프로퍼티 : 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티다.
16.3.1 데이터 프로퍼티
- 프로퍼티 어트리뷰트
- [[Value]]
- 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값.
- 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]]에 값을 재할당함. 이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고 생성된 프로퍼티의 [[Value]]에 값을 저장함.
- [[Writable]]
- 프로퍼티 값의 변경 가능 여부, 불리언 값.
- [[Writable]]의 값이 false이면, 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티가 된다.
- [[Enumerable]]
- 프로퍼티의 열거 가능 여부, 불리언 값.
- [[Enumerable]]의 값이 false이면, 해당 프로퍼티는 for...in 문이나 Object.keys 메서드 등으로 열거할 수 없다
- [[Configurable]]
- 프로퍼티의 재정의 가능 여부, 불리언 값.
- [[Configurable]]의 값이 false이면, 해당 프로퍼티의 삭제, 값변경이 금지된다. 단, [[Writable]]이 true일때, [[Value]]의 변경과 [[Writable]]을 false로 변경하는 거슨 허용 된다.
- 프로퍼티가 생성될때, [[Value]]의 값은 프로퍼티 값으로 초기화, [[Writable]], [[Enumerable]], [[Configurable]]의 값은 true로 초기화 된다. 동적 추가시에도 동일하다.
16.3.2 접근자 프로퍼티
- 프로퍼티 어트리뷰트
- [[Get]]
- 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수.
- 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환됨.
- [[Set]]
- 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수
- 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장됨
- [[Enumerable]]
- 프로퍼티의 열거 가능 여부, 불리언 값
- 데이터 프로퍼티의 [[Enumerable]]와 같다.
- [[Configurable]]
- 프로퍼티의 재정의 가능 여부, 불리언 값
- 데이터 프로퍼티의 [[Configurable]]와 같다.
- 접근자 함수는 getter/setter 함수라고도 부른다.
- getter/setter 함수는 모두 정의할 수도 있고, 하나만 정의할 수도 있다
const person = {
firstName: 'Ungmo',
lastName: 'Lee',
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
}
console.log(person.firstName + ' ' + person.lastName);
person.fullName = 'Heegun Lee';
console.log(person);
console.log(person.fullName);
- 접근자 프로퍼티는 자체적으로 값을 가지지 않으며, 다만 데이터 프로퍼티의 값을 읽거나 저장할 때 관여한다.
- 접근자 프로퍼티와 데이터 프로퍼티를 구별하는 방법은 Object.getOwnPropertyDescriptor 메서드를 이용하는 것이다. 반환하는 값을 보면 구별이 가능하다.
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
Object.getOwnPropertyDescriptor(function() {}, 'prototype');
16.4 프로퍼티 정의
- 프로퍼티 정의 : 프로퍼티의 값을 갱신 가능하도록 할 것인지, 프로퍼티를 열거 가능하도록 할 것인지, 프로퍼티를 재정의 가능하도록 할 것인지 정의하는 것.
- Object.defineProperty(객체, 데이터 프로퍼티 키, 프로퍼티 디스크립터 객체) 메서드를 사용하면 프로퍼티의 어트리뷰트를 정의할 수 있음.
- 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본값이다.
const person = {};
Object.defineProperty(person, 'firstName', {
value: 'Ungmo',
writable: true,
enumerable: true,
configurable: true,
})
Object.defineProperty(person, 'LastName', {
value: 'Lee',
})
console.log(Object.getOwnPropertyDescriptor(person, 'firstName'));
console.log(Object.getOwnPropertyDescriptor(person, 'lastName'));
Object.defineProperty(person, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
}
set(name) {
[this.firstName, this.lastName] = name.split(' ');
},
enumerable: true,
configurable: true,
})
console.log(Object.getOwnPropertyDescriptor(person, 'fullName'));
- Object.defineProperties 메서드를 사용하면 여러 개의 프로퍼티를 한 번에 정의할 수 있다.
const person = {};
Object.defineProperties(person, {
firstName: {
value: 'Ungmo',
writable: true,
enumerable: true,
configurable: true,
},
lastName: {
value: 'Lee',
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 = 'Heegun Lee';
console.log(person);
16.5 객체 변경 방지
- 자바스크립트는 객체의 변경을 방지하는 메서드를 제공한다
16.5.1 객체 확장 금지
- Object.preventExtensions 메서드 : 객체의 확장 금지(= 프로퍼티 추가 금지)
- 프로퍼티 동적 추가와 Object.defineProperty 메서드로 추가할수 있는데, 이 두가지 모두 금지 된다.
- Object.isExtensible 메서드로 확장이 가능한 객체인지 확인할 수 있다.
const person = {name: 'Lee'};
console.log(Object.isExtensible(person));
Object.preventExtensions(person);
console.log(Object.isExtensible(person));
person.age = 20;
console.log(person);
delete person.name;
console.log(person);
16.5.2 객체 밀봉
- Object.seal 메서드 : 프로퍼티 추가, 삭제, 프로퍼티 어트리뷰트 재정의 금지. 읽기와 쓰기만 가능
- Object.isSealed 메서드로 밀복된 객체인지 확인할 수 있다.
- 밀봉된 객체는 configurable이 false다.
const person = {name: 'Lee'};
console.log(Object.isSealed(person));
Object.seal(person);
console.log(Object.isSealed(person));
person.age = 20;
console.log(person);
delete person.name
console.log(person);
person.name = 'Kim';
console.log(person);
Object.defineProperty(person, 'name', {configurable: true});
16.5.3 객체 동결
- Object.freeze 메서드 : 프로퍼티 추가, 삭제, 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지. 읽기만 가능
- Object.isFrozen 메서드로 동결된 객체인지 확인할 수 있다.
- 동결된 객체는 writable과 configurable가 false다
const person = {name: 'Lee'};
console.log(Object.isFrozen(person));
Object.freeze(person);
console.log(Object.isFrozen(person));
person.age = 20;
console.log(person);
delete person.name
console.log(person);
person.name = 'Kim';
console.log(person);
Object.defineProperty(person, 'name', {configurable: true});
16.5.4 불변 객체
- 지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지로, 중첩 객체까지는 영향을 주지 못한다.
- 객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 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);
console.log(Object.isFrozen(person));
console.log(Object.isFrozen(person.address));
person.address.city = 'Busan';
console.log(person);