내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드다. ECMAScript 사양에 등장하는 이중 대괄호 ([[...]])로 감싼 이름들이 내부 슬롯과 내부 메서드다.
❗️예를 들어 모든 객체는 [[prototype]]이라는 내부 슬롯을 갖는다. 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 원칙적으로 접근할 수 없지만 [[prototype]]내부 슬롯의 경우, proto를 통해 간접적으로 접근할 수 있다.
const o ={};
o.[[prototype]] //error
o.__proto__ // object.prototype
자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티의 어트리뷰트를 기본값으로 자동 정의한다. 프로퍼티의 상태란 프로퍼티의 값(value), 값의 갱신 가능 여부(writeable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)를 말한다.
❗️따라서 프로퍼티 어트리뷰트에 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor메서드를 사용하여 간접적으로 확인할 수는 있다.
const age= Symbol("age");
const obj={
name:'andy',
};
console.log(Object.getOwnPropertyDescriptor(obj,'name'));
//{ value: 'andy', writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor 메서드를 호출할 때 첫 번째 매개변수는 객체의 참조를 전달하고, 두 번째 매개변수에는 프로퍼티 키를 문자열로 전달한다. 이때 Object.getOwnPropertyDescriptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립트 객체를 반환한다. 만약 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면 undefined가 반환된다.
❗️하지만 ES8에서 도입된 Object.getOwnPropertyDescriptors 메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
const person={
name: 'andy',
}
person.age='25';
console.log(Object.getOwnPropertyDescriptors(person));
/* 출력결과
{
name: {
value: 'andy',
writable: true,
enumerable: true,
configurable: true
},
age: { value: '25', writable: true, enumerable: true, configurable: true }
}
*/
키와 값으로 구성된 일반적인 프로퍼티다. 지금까지 살펴본 모든 프로퍼티는 데이터 프로퍼티다.
자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티다.
데이터 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다. 이 프로퍼티 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의된다.
❗️프로퍼티가 생성될 때 [[value]]의 값은 프로퍼티 값으로 초기화되며 [[writeable]], [[enumerable]], [[configurable]]의 값은 true로 초기화된다. 이것은 프로퍼티를 동적 추가해도 마찬가지다.
접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.
const person ={
//데이터 프로퍼티
firstName: 'Andy',
lastName: 'Han',
//fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
//getter 함수
get fullName(){
return `${this.firstName} ${this.lastName}`;
},
//setter 함수
set fullName(name){
[this.firstName,this.lastName] =name.split(' ');
}
};
//데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName+' '+person.lastName); //andyHan
//접근자 프로퍼티를 통한 프로퍼티 값의 저장
//접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = 'Han Jaehyeok';
console.log(person); //{ firstName: 'Han', lastName: 'Jaehyeok', fullName: [Getter/Setter] }
//접근자 프로퍼티를 통한 프로퍼티 값의 참조
//접근자 프로퍼티 fullName에 접근하면 getter함수가 호출된다.
console.log(person.fullName); //Han Jaehyeok
//firstName은 데이터 프로퍼티다.
//데이터 프로퍼티는 [[value]] ,[[writeable]], [[enumerable]], [[configurable]]
//프로퍼티 어트리뷰트를 갖는다
let descriptor=Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor); //{ value: 'Han', writable: true, enumerable: true, configurable: true }
//fullName은 접근자 프로퍼티다.
//접근자 프로퍼티는 [[Get]] ,[[Set]], [[Enumerable]], [[Configurable]]
//프로퍼티 어트리뷰트를 갖는다
descriptor=Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
/* {
get: [Function: get fullName],
set: [Function: set fullName],
enumerable: true,
configurable: true
} */
//일반 객체의 __proto__는 접근자 프로퍼티다.
const objDescriptor=Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
console.log(objDescriptor);
/* 출력 결과
{
get: [Function: get __proto__],
set: [Function: set __proto__],
enumerable: false,
configurable: true
}
*/
const funcDescriptor=Object.getOwnPropertyDescriptor(function(){},'prototype');
console.log(funcDescriptor);
/* 출력 결과
{ value: {}, writable: true, enumerable: false, configurable: false }
*/
프로퍼티 정의란 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.
❗️Object.defineProperty 메서드를 사용하면 프로퍼티의 어트리뷰트를 정의할수 있다. 인수로는 객체의 참조와 데이터 프로퍼티의 키인 문자열, 프로퍼티 디스크립터 객체를 전달한다.
const person={};
//데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
value: 'andy',
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(person,'lastName',{
value: 'han',
});
Object.defineProperty(person,'middleName',{
});
let descriptor=Object.getOwnPropertyDescriptor(person,'firstName');
console.log('firstName', descriptor); //firstName { value: 'andy', writable: true, enumerable: true, configurable: true }
//디스크립터 객테의 프로퍼티를 누락시키면 undefined, false가 기본값이다.
descriptor=Object.getOwnPropertyDescriptor(person,'lastName');
console.log('lastName', descriptor);
/*lastName {
value: 'han',
writable: false,
enumerable: false,
configurable: false
} */
descriptor=Object.getOwnPropertyDescriptor(person,'middleName');
console.log(descriptor);
/*{
value: undefined,
writable: false,
enumerable: false,
configurable: false
}*/
//[[Enumerable]]의 값이 false인 경우
//해당 프로퍼티는 for...in 문법이나 Object.keys 등으로 열거할수 없다.
//lastName 프로퍼티는 [[Enumerable]]의 값이 false이므로 열거되지 않는다.
console.log(Object.keys(person)); //[ 'firstName' ]
//[[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]] 값을 변경할 수 없다.
//lastName 프로퍼티는 [[Writeable]] 의 값이 false이므로 값을 변경할 수 없다.
//이때 값을 변결하면 에러는 발생하지 않고 무시된다.
person.lastName='kim';
//[[Configuralble]]의 값이 false인 경우 해당 프로퍼티를 삭제할수 없다.
//lastName은 프로퍼티는 [[Configuralble]]의 값이 false이므로 삭제할 수 없다.
//이때 프로퍼티를 삭제하면 에러는 방생하지 않고 무시된다.
delete person.lastName;
//[[Configurable]]의 값이 false인 경우 해당 프로퍼티를 재정의 할 수 없다.
descriptor=Object.getOwnPropertyDescriptor(person,'lastName');
console.log('lastName', descriptor);
/*{
value: 'han',
writable: false,
enumerable: false,
configurable: false
}*/
//접근자 프로퍼티 정의
Object.defineProperty(person,'fullName',{
//getter함수
get(){
return `${this.firstName} ${this.lastName}`;
},
//setter함수
set(name){
[this.firstName,this.lastName]= name.split(' ');
},
enumerable:true,
configurable:true,
});
descriptor=Object.getOwnPropertyDescriptor(person,'fullName');
console.log('fullName', descriptor);
/*fullName {
get: [Function: get],
set: [Function: set],
enumerable: true,
configurable: true
}*/
person.fullName='Han JaeHyeok';
console.log(person); //{ firstName: 'Han', fullName: [Getter/Setter] }
😃Object.defineProperties 메서드를 사용하면 여러 개의 프로퍼티를 한 번에 정의할 수 있다.
const person={};
Object.defineProperties(person,{
//데이터 프로퍼티 정의
firstName: {
value:'andy',
writable: true,
configurable: true,
enumerable: true,
},
lastName:{
value: 'han',
writable: true,
configurable: true,
enumerable: true,
},
fullName:{
//getter함수
get(){
return `${this.firstName} ${this.lastName}`;
},
set(name){
[this.firstName,this.lastName]=name.split(' ');
},
configurable: true,
enumerable: true,
}
});
person.fullName='han jaehyeok';
console.log(person); //{ firstName: 'han', lastName: 'jaehyeok', fullName: [Getter/Setter] }
객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다. 즉 프로퍼티를 추가하거나 삭제할 수 있고 프로퍼티 값을 생실할 수 있으며, Object.defineProperty 또는 Object.defineProperties 메서드를 사용하여 프로퍼티 어트리뷰트를 재정의할 수도 있다.
Object.preventExtensions 메서드는 객체의 확장을 금지한다. 객체 확장 금지란 프로퍼티 추가 금지를 의미한다. ❗️즉 확장이 금지된 객체는 프로퍼티 추가가 금지된다.
const person={
name:'andy',
};
//person객체는 확장이 금지된 객체가 아니다.
console.log(Object.isExtensible(person)); //true
//person객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person);
console.log(Object.isExtensible(person)); //false
//프로퍼티 추가가 금지된다.
person.age =20; //무시
console.log(person);
//프로퍼티 추가는 금지되지만 삭제는 가능하다
delete person.name;
console.log(person); //{}
//프로퍼티 정의에 의한 추가도 금지된다.
Object.defineProperty(person,'gender',{ //error
value: 'male',
enumerable: true,
configurable: true,
writable: true,
})
Object.seal 메서드는 객체를 밀봉한다. 객체 밀봉seal 이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미한다. 즉 ❗️밀봉된 객체는 읽기와 쓰기만 가능하다.
const person={
name: 'andy'
};
//person객체는 밀봉(seal)된 객체가 아니다.
console.log(Object.isSealed(person)); //false출력
//person 객체를 밀봉 하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.seal(person);
//person객체는 밀봉(seal)된 객체다.
console.log(Object.isSealed(person)); //true
//밀봉된 객체는 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
/*{
name: {
value: 'andy',
writable: true,
enumerable: true,
configurable: false
}
}*/
//프로퍼티 추가가 금지된다.
person.age =20; //무시
console.log(person); //{ name: 'andy' }
//프로퍼티 삭제가 금지된다.
delete person.name;
console.log(person); //{ name: 'andy' }
//프로퍼티 값 갱신은 가능하다.
person.name='kim';
console.log(person); //{ name: 'kim' }
//프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person,'name',{configurable:true}); //error
Object.freeze 메서드는 객체를 동결한다. 객체 동결이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지를 의미한다. ❗️즉 동결된 객체는 읽기만 가능하다.
const person={
name: 'andy'
};
//person 객체는 동결(freeze)객체가 아니다
console.log(Object.isFrozen(person));//false;
//person 객체를 동결(freeze)하여 프로퍼티 추가, 삭제, 재정의, 쓰기를 금지한다.
Object.freeze(person);
console.log(Object.isFrozen(person));//true;
//동결(freeze)된 객체는 writable 과 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
/*{
name: {
value: 'andy',
writable: false,
enumerable: true,
configurable: false
}
}*/
//프로퍼티 추가가 금지된다.
person.age=25;
console.log(person); //{ name: 'andy' }
//프로퍼티 삭제가 금지된다.
delete person.name;
console.log(person); //{ name: 'andy' }
//프로퍼티 값 갱신이 금지된다.
person.name="han";
console.log(person); //{ name: 'andy' }
//프로퍼티 어트리뷰트 재정의가 금지된다.
Object.defineProperty(person,'name',{ //error
configurable:true
})
지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지로 지속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지는 못한다. 따라서 Object.freeze메서드로 객체를 동결하여도 중첩 객체까지 동결할 수 없다.
const person={
name: 'andy',
address:{city: 'seoul'}
};
//얖은 객체 동결
Object.freeze(person);
//직속 프로퍼티만 동결한다.
console.log(Object.isFrozen(person)); //true
//중첩 객체까지 동결하지 못한다.
console.log(Object.isFrozen(person.address));//false
person.address.city='goyang';
console.log(person); //{ name: 'andy', address: { city: 'Busan' } }
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:'han',
address: {city:'seoul'}
};
//깊은 객체 동결
deepFreeze(person);
console.log(Object.isFrozen(person)); //true
//중첩 객체까지 동결한다.
console.log(Object.isFrozen(person.address)); //true
person.address.city="goyang"
console.log(person); //{ name: 'han', address: { city: 'seoul' } }