내부 슬롯와 내부 메서드: 자바스크립트 엔진의 구현 알고리즘 설명을 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드 ⇒ 이중 대괄호([[…]])
로 감싼 이름들
const o = {};
// 모든 객체는 [[Prototype]]이라는 내부 슬롯 가짐
// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근 X
o.[[Prototype]] // Uncaught SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있음!
o.__proto__ // Object.prototype
[[Value]]
, [[Writable]]
, [[Enumerable]]
, [[Configurable]]
Object.getOwnPropertyDescriptor
메서드 사용하여 간접적으로 확인 가능 ⇒ 프로퍼티 어트리뷰트 정보 제공하는 프로퍼티 디스크립터 객체 반환(존재하지 않으면 undefined
반환) Object.getOwnPropertyDescriptor(객체 참조, 프로퍼티 키)
const person = {
name: 'Lee',
};
person.age = 20; // 프로퍼티 동적 생성
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// { value: 'Lee', writable: true, enumerable: true, configurable: true }
// ES8에서 도입된 getOwnPropertyDescriptors 메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보 제공
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
name: {
value: 'Lee',
writable: true,
enumerable: true,
configurable: true
},
age: { value: 20, writable: true, enumerable: true, configurable: true }
}
*/
데이터 프로퍼티: 키와 값으로 구성된 일반적인 프로퍼티
ex) 함수 객체의 prototype
은 데이터 프로퍼티
프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명 |
---|---|---|
[[Value]] | value | 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값 - 프로퍼티 키를 통해 값 변경하면 [[Value]]에 값을 재할당함! 프로퍼티 없으면 프로퍼티를 동적 생성하고 값 저장 |
[[Writable]] | writable | 프로퍼티 값 변경 가능 여부 나타냄(boolean 값) - false인 경우 읽기 전용 프로퍼티가 됨 |
[[Enumerable]] | enumerable | 프로퍼티의 열거 가능 여부 나타냄(boolean 값) - false인 경우 열거 X |
[[Configurable]] | configurable | 프로퍼티의 재정의 가능 여부 나타냄(boolean 값) - false인 경우 해당 프로퍼티 삭제, 값 변경 X - [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용 - 데이터 프로퍼티가 생성될 때, [[Value]]의 값은 프로퍼티 값으로 초기화되고, [[Writable]], [[Enumerable]], [[Configurable]]의 값은 true로 초기화됨]] |
접근자 프로퍼티: 자체적으로 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티 ⇒ getter/setter 함수라고도 부름
ex) 일반 객체의 __proto__
는 접근자 프로퍼티
프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명 |
---|---|---|
[[Get]] | get | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수 - getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환 |
[[Set]] | set | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수 - setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장 |
[[Enumerable]] | enumerable | 프로퍼티의 열거 가능 여부 나타냄(boolean 값) - false인 경우 열거 X |
[[Configurable]] | configurable | 프로퍼티의 재정의 가능 여부 나타냄(boolean 값) - false인 경우 해당 프로퍼티 삭제, 값 변경 X - [[Writable]]이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용 |
const person = {
// 데이터 프로퍼티
firstName: 'Gildong',
lastName: 'Hong',
// 접근자 프로퍼티
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
},
};
// 데이터 프로퍼티를 통한 프로퍼티 값 참조
console.log(person.firstName + ' ' + person.lastName); // Gildong Hong
// 접근자 프로퍼티를 통한 프로퍼티 값 저장
person.fullName = 'Jjanggu Shin';
console.log(person); // { firstName: 'Jjanggu', lastName: 'Shin', fullName: [Getter/Setter] }
console.log(person.fullName); // Jjanggu Shin
[접근자 프로퍼티 동작 원리]
❗프로토타입: 어떤 객체의 상위(부모)객체의 역할을 하는 객체
즉, 상위(부모) 객체는 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속하여 상속받은 하위 객체는 프로퍼티와 메서드를 자유롭게 사용
❗️프로토타입 체인: 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조로, 프로토타입 체인을 따라 프로토타입의 프로퍼티와 메서드 차례대로 검색함!
⇒ 접근하려고 하는 객체에 프로퍼티 또는 메서드가 없는 경우 위로 올라가면서 검색
새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것
Object.defineProperty(객체 참조, 데이터 프로퍼티 키, 프로퍼티 디스크립터 객체)
이용
프로퍼티 디스크립터 객체의 프로퍼티 | 대응하는 프로퍼티 어트리뷰트 | 생략했을 때 기본값 |
---|---|---|
value | [[Value]] | undefined |
get | [[Get]] | undefined |
set | [[Set]] | undefined |
writable | [[Writable]] | false |
enumerable | [[Enumerable]] | false |
configurable | [[Configurable]] | false |
const person = {}; // 빈 객체
// 데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
value: 'Jjanggu',
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(person, 'lastName', {
value: 'Shin',
});
console.log('firstName', Object.getOwnPropertyDescriptor(person, 'firstName')); // firstName { value: 'Jjanggu', writable: true, enumerable: true, configurable: true }
// 디스크립터 객체의 프로퍼티 누락시키면 undefined, false가 기본값!
console.log('lastName', Object.getOwnPropertyDescriptor(person, 'lastName')); // lastName { value: 'Shin', writable: false, enumerable: false, configurable: false }
// lastName 프로퍼티는 [[Enumerable]]의 값이 false이므로 열거되지 X
console.log(Object.keys(person)); // [ 'firstName' ]
// [[Writable]]의 값이 false인 경우 해당 프로퍼티의 값 변경 X
// false인데 값 변경하려고 하면 에러는 발생하지 않고 무시
person.lastName = 'Kim';
// [[Configurable]]의 값이 false인 경우 해당 프로퍼티의 값 삭제 X
// false인데 값 삭제하려고 하면 에러는 발생하지 않고 무시
delete person.lastName;
// [[Configurable]]의 값이 false인 경우 해당 프로퍼티 재정의 X
Object.defineProperty(person, 'lastName', { enumerable: true }); // TypeError: Cannot redefine property: lastName
const person = {}; // 빈 객체
// 접근자 프로퍼티 정의
Object.defineProperty(person, 'fullName', {
// getter 함수
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set(name) {
[this.firstName, this.lastName] = name.split(' ');
},
enumerable: true,
configurable: true,
});
console.log(Object.getOwnPropertyDescriptor(person, 'fullName')); // { get: [Function: get], set: [Function: set], enumerable: true, configurable: true }
person.fullName = 'HiHi Kim';
console.log(person); // { fullName: [Getter/Setter], firstName: 'HiHi', lastName: 'Kim' }
Object.defineProperties
메서드 사용하면 여러 개의 프로퍼티 한 번에 정의 가능
const person = {}; // 빈 객체
Object.defineProperties(person, {
// 데이터 프로퍼티 정의
firstName: {
value: 'GuGu',
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 = 'HaHa Kim';
console.log(person); // { firstName: 'HaHa', fullName: [Getter/Setter], lastName: 'Kim' }
Object.defineProperty
나 Object.defineProperties
메서드 사용하여 프로퍼티 어트리뷰트 재정의 가능구분 | 메서드 | 프로퍼티 추가 | 프로퍼티 삭제 | 프로퍼티 값 읽기(getter) | 프로퍼티 값 쓰기(setter) | 프로퍼티 어트리뷰트 재정의 |
---|---|---|---|---|---|---|
객체 확장 금지 | Object.preventExtensions | X | O | O | O | O |
객체 밀봉 | Object.seal | X | X | O | O | X |
객체 동결 | Object.freeze | X | X | O | X | X |
Object.preventExtensions
Object.isExtensible
메서드로 확장이 가능한 객체인지 확인 가능const person = { name: 'Lee' };
console.log(Object.isExtensible(person)); // true
// person 객체의 확장 금지하여 프로퍼티 추가를 금지
**Object.preventExtensions(person);**
console.log(Object.isExtensible(person)); // false
// 프로퍼티 추가 X
person.age = 20; // 무시
console.log(person); // { name: 'Lee' }
// 프로퍼티 추가 X
Object.defineProperty(person, 'age', { value: 20 }); // TypeError: Cannot define property age, object is not extensible
Object.seal
Object.isSealed
메서드로 밀봉된 객체인지 확인 가능const person = { name: 'Lee' };
console.log(Object.isSealed(person)); // false
// person 객체 밀봉하여 프로퍼티 추가, 삭제, 재정의 금지
Object.seal(person);
console.log(Object.isSealed(person)); // true
person.age = 20; // 무시
delete person.name; // 무시
Object.defineProperty(person, 'name', { configurable: true }); // TypeError: Cannot redefine property: name
person.name = 'Kim'; // 프로퍼티 값 갱신은 가능
console.log(person); // { name: 'Kim' }
Object.freeze
Object.isFrozen
메서드로 동결된 객체인지 확인 가능const person = { name: 'Lee' };
console.log(Object.isFrozen(person)); // false
// person 객체를 동결하여 프로퍼티 추가, 삭제, 재정의, 쓰기 금지
Object.freeze(person);
console.log(Object.isFrozen(person)); // true
person.age = 20; // 무시
delete person.name; // 무시
person.name = 'Kim'; // 무시
Object.defineProperty(person, 'name', { configurable: true }); // TypeError: Cannot redefine property: name
불변 객체
Object.preventExtensions
, Object.seal
, Object.freeze
는 얕은 변경 방지로 직속 프로퍼티만 변경 방지됨! 중첩 객체까지 동결 XObject.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);