목표
16장 프로퍼티 어트리뷰트
- 프로퍼티 어트리뷰트를 이해하기 위해서는 먼저 내부 슬롯(internal slot)과 내부 메서드(internal method)를 알아야 한다.
16-1. 내부 슬롯과 내부 메서드
- 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(persudo property)와 의사 메서드(persudo method)이다.
- 이중 대괄호([[...]])를 사용하여 표현한다.
- 내부 슬롯과 내부 메서드는 ECMAScript 사양에 정의된 대로 구현되어 자바스크립트 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아니다.
- 즉, 직접 접근하거나 호출할 수 있는 방법은 제공하지 않는다. 단, 일부 내부 슬롯과 내부 메서드에 한해 간접적으로 접근할 수 있는 수단을 제공한다.
const object = {};
object.__proto__
- 예를 들어, 모든 객체는 [[prototype]] 이라는 내부 슬롯을 갖는다. 위의 코드처럼 간접적으로 접근 할 수 있다.
16-2. 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체
- 자바스크립트 엔진은 객체의 프로퍼티를 생성할 때 프로퍼티 어트리뷰트라는 것을 기본 값으로 자동 정의한다.
- 여기서 프로퍼티 어트리뷰트는 프로퍼티의 상태를 말한다.
프로퍼티의 상태란?
- 프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)를 말한다.
- 다시 말하자면 자바스크립트 엔진이 관리하는 내부 상태 값인 내부 슬롯이다.
- 프로퍼티 어트리뷰트에 직접 접근할 수는 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수는 있다.
const product = {
price: 1000,
};
console.log(Object.getOwnPropertyDescriptor(product, "price"));
{ value: 1000, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor
- getOwnPropertyDescriptor 메서드를 호출할 때 첫 번째 매개 변수에는 객체의 참조를 전달하고, 두 번째 매개변수에는 프로퍼티 키를 문자열로 전달한다.
- 이 때, 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터(PropertyDescriptor) 객체를 반환한다.
- 만약 존재하지 않는 프로퍼티나 상속받은 프로퍼티일 경우 undefined가 반환된다.
16-3. 데이터 프로퍼티와 접근자 프로퍼티
- 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.
- 데이터 프로퍼티(data property): 키와 값으로 구성된 일반적인 프로퍼티
- 접근자 프로퍼티(accessor property): 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티
16-3-1. 데이터 프로퍼티
- 데이터 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 자바스크립트 엔진에 의해 기본값으로 자동 정의된다.
- 다음 코드는 프로퍼티를 동적으로 추가한 코드이다.
- Object.getOwnPropertyDescriptors(객체 변수명)을 사용하면 객체의 모든 프로퍼티 디스크립터 객체를 반환한다.
const product = {
price: 1000,
};
product.createdAt = "2021-12-04";
console.log(Object.getOwnPropertyDescriptors(product));
price: { value: 1000, writable: true, enumerable: true, configurable: true },
createdAt: {
value: '2021-12-04', writable: true, enumerable: true, configurable: true
}
16-3-2. 접근자 프로퍼티
- 데이터 프로퍼티와 달리 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수(accessor function)로 구성된 프로퍼티이다.
const product = {
price: 1000,
createdAt: "2021-12-04",
get print() {
return `해당 물건의 가격은 ${this.createdAt}일 기준 ${this.price}원 입니다.`;
},
set setPrice(price) {
this.price = price;
},
set setCreatedAt(createdAt) {
this.createdAt = createdAt;
},
};
console.log(product.print);
product.setPrice = 2000;
product.setCreatedAt = "2021-12-05";
console.log(product.print);
"해당 물건의 가격은 2021-12-04일 기준 1000원 입니다."
"해당 물건의 가격은 2021-12-05일 기준 2000원 입니다."
- product 객체의 price와 createdAt 프로퍼티는 데이터 프로퍼티이고 get, set이 붙은 메서드의 이름 setPrice, setCreatedAt이 접근자 프로퍼티이다.
- 접근자 프로퍼티는 자체적으로 값([[Value]])을 가지지 않으며 데이터 프로퍼티의 값을 읽어나 저장할 때 관여한다.
- 이를 내부 슬롯 및 내부 메서드 관점에서 접근자 프로퍼티 print로 프로퍼티 값에 접근한다고 한다면 다음과 같이 동작한다.
- 프로퍼티 키가 유효한지 확인한다. 프로퍼티 키는 문자열 또는 심벌 타입이어야 한다. "print"는 문자열 이므로 유효한 프로퍼티 키이다.
- 프로토타입 체인에서 프로퍼티를 검색한다. product 객체에 print 프로퍼티가 존재한다.
프로토타입(prototype)
- 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체이다. 즉, 프로토 타입은 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속한다. 상속을 받은 하위 객체는 자신의 프로퍼티 또는 메서드인 것처럼 자유롭게 사용할 수 있다.
- 프로토타입 체인은 프로토타입이 단방향 링크드 리스트 형태도 연결되어 있는 상속 구조를 말한다.
- 검색된 print 프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인한다.
- 접근자 프로퍼티 print 어트리뷰트 [[Get]]의 값, 즉 getter 함수를 호출하여 그 결과를 반환한다. 이 때, [[Get]]의 값은 Object.getOwnPropertyDescriptor 메서드가 반환하는 프로퍼티 디스크립터 객체의 get 프로퍼티 값과 같다.
프로퍼티 구분
- 접근자 프로퍼티와 데이터 프로퍼티를 구별하는 방법은 다음과 같다.
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
Object.getOwnPropertyDescriptor(function() {}, 'prototype');
- 접근자 프로퍼티와 데이터 프로퍼티의 프로퍼티 디스크립터 객체의 프로퍼티가 다른 것을 알 수 있다.
16-4. 프로퍼티 정의
- 프로퍼티를 정의한다는 것은 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의함을 의미한다.
- 이는 Object.defineProperty 메서드를 사용하여 정의할 수 있다. 인수로는 객체의 참조, 데이터 프로퍼티 키(문자열), 프로퍼티 디스크립터 객체를 전달하면 된다.
const product = {};
Object.defineProperty(product, 'price', {
value: 1000,
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(product, 'createdAt', {
value: "2021-12-04",
});
let descriptor = Object.getOwnPropertyDescriptor(product, 'price');
console.log('price', descriptor);
price {value: 1000, writable: true, enumerable: true, configurable: true}
- 디스크립터 객체의 프로퍼티를 누락시키면 value 프로퍼티는 undefined, value를 제외한 나머지 프로퍼티는 false가 기본 값이 된다.
const createdAtDescriptor = Object.getOwnPropertyDescriptor(
product,
"createdAt"
);
descriptor = Object.getOwnPropertyDescriptor(product, 'createdAt');
console.log('createdAt', descriptor);
createdAt {value: '2021-12-04', writable: false, enumerable: false, configurable: false}
- writable 프로퍼티의 값이 false인 경우 해당 프로퍼티의 value의 값을 변경할 수 없다. false일 경우 값을 변경하면 에러는 발생하지 않고 무시된다.
product.price = 2000;
descriptor = Object.getOwnPropertyDescriptor(product, "price");
console.log("price", descriptor);
product.createdAt = "2021-12-05";
descriptor = Object.getOwnPropertyDescriptor(product, "createdAt");
console.log("createdAt", descriptor);
price { value: 2000, writable: true, enumerable: true, configurable: true }
createdAt { value: '2021-12-04', writable: false, enumerable: false, configurable: false }
- 위 코드를 보면 writeable의 값이 false인 createdAt 프로퍼티의 값이 변경되지 않은 것을 확인할 수 있다.
- 위의 코드에는 없지만 enumerable의 값이 false이면 fon...in 문이나 Object.keys 등으로 열거할 수 없고 configurable의 값이 false이면 프로퍼티를 삭제하거나 재정의할 수 없다.
- 참고로, Object.defineProperty 메서드는 하나의 프로퍼티만 정의 가능하지만 Object.defineProperties 메서드를 사용하면 여러 개의 프로퍼티를 한 번에 정의할 수 있다.
const product = {};
Object.defineProperties(product, {
price: {
value: 1000,
writable: true,
enumerable: true,
configurable: true,
},
createdAt: {
value: "2021-12-04",
},
print: {
get() {
return `해당 물건의 가격은 ${this.createdAt}일 기준 ${this.price}원 입니다.`;
},
},
setPrice: {
set(price) {
this.price = price;
},
},
});
console.log(product.print);
"해당 물건의 가격은 2021-12-04일 기준 1000원 입니다."
16-5. 객체 변경 방지
- 객체는 변경 가능한 값으로 재할당 없이 직접 변경할 수 있다.
- 즉, 프로퍼티를 추가, 삭제할 수 있고 프로퍼티의 값을 갱신할 수 있다. 또한, Object.defineProperty 메서드를 사용하여 프로퍼티 어트리뷰트를 재정의할 수도 있다.
- 자바스크립트는 객체 변경을 방지하는 다양한 메서드를 제공한다. 각 메서드들은 객체 변경을 금지하는 강도가 다르다.
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.job = "programmer";
console.log(person);
true
false
{ name: "Lee" }
- 동적으로 job 프로퍼티를 추가했지만 프로퍼티가 추가되지 않았다.
16-5-2. 객체 밀봉
- 프로퍼티 추가뿐만 아니라 삭제와 프로퍼티 어트리뷰트 재정의 금지를 말한다. 이는 Object.seal 메서드를 사용한다. 즉, 읽기와 쓰기만 가능하다.
- 밀봉된 객체인지 여부는 Object.isSealed 메서드로 확인할 수 있다.
const person = { name: "Lee" };
console.log(Object.isSealed(person));
Object.seal(person);
console.log(Object.isSealed(person));
delete person.name;
console.log(person);
person.name = "kim";
console.log(person);
false
true
{ name: 'Lee' }
{ name: 'kim' }
- 프로퍼티 삭제가 무시되었고 프로퍼티 값 갱신은 가능하다.
16-5-3. 객체 동결
- 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지를 의미한다. 즉, 읽기만 가능하다. 이는 Object.freeze 메서드를 사용한다.
- 동결된 객체인지 여부는 Object.isFrozen 메서드로 확인할 수 있다.
const person = { name: "Lee" };
console.log(Object.isFrozen(person));
Object.freeze(person);
console.log(Object.isFrozen(person));
delete person.name;
console.log(person);
person.name = "kim";
console.log(person);
console.log(Object.getOwnPropertyDescriptors(person));
false
true
{ name: 'Lee' }
{ name: 'Lee' }
{
name: {
value: 'Lee',
writable: false,
enumerable: true,
configurable: false
}
}
16-5-4. 불변 객체
- preventExtensions, seal, freeze 메서드는 직속 프로퍼티만 변경이 방지되는 얕은 변경 방지이다.
- 따라서, Object.freeze 메서드로 동결하여도 중첩 객체까지는 동결할 수 없다.
- 객체의 중첩 객체까지 동결하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 한다.
const person = {
name: "Lee",
address: { city: "seoul" },
};
function freeze(obj) {
Object.keys(obj).forEach((key) =>
typeof obj[key] === "object" ? freeze(obj[key]) : Object.freeze(obj)
);
}
freeze(person);
console.log(Object.isFrozen(person.name));
console.log(Object.isFrozen(person.address.city));
person.name = "Kim";
person.address.city = "incheon";
console.log(person);
true
true
{ name: 'Lee', address: { city: 'seoul' }}