목차6개 항목
출처와 참고
web.dev의 「Learn JavaScript」 강좌의 「Property descriptors」 모듈과 javascript.info(모던 JavaScript 튜토리얼)의 「프로퍼티 플래그와 설명자」, 「프로퍼티 getter와 setter」 글을 바탕으로, MDN으로 확인해 재구성한 노트다. 「Frontend_TIL: 프로퍼티 어트리뷰트」을 바탕으로 보강했다.
속성 설명자란
객체의 속성은 "키 → 값"이 전부가 아니다. 각 속성에는 그 속성을 바꿀 수 있는지, 순회에 나오는지, 지우거나 재설정할 수 있는지를 정하는 숨은 속성(attribute)이 붙어 있다. 이 묶음을 속성 설명자(property descriptor) 라고 한다. 명세에서는 [[Writable]]처럼 이중 대괄호로 쓰는데, 코드로 직접 접근하는 이름이 아니라 내부 슬롯이라는 표시다([[Prototype]]과 같은 표기).
설명자는 두 종류다. 한 속성은 둘 중 하나만 될 수 있다.
| 종류 | 가지는 설정 | 뜻 |
|---|---|---|
| 데이터 속성(data) | value, writable | 값을 그대로 저장한다 |
| 접근자 속성(accessor) | get, set | 읽고 쓸 때 함수가 실행된다. 값 자체는 저장하지 않는다 |
| 공통 | enumerable, configurable | 순회 노출 여부, 삭제·재설정 가능 여부 |
| 설정 | 뜻 | 리터럴·대입으로 만든 속성 | defineProperty 기본값 |
|---|---|---|---|
value | 속성의 값 | 대입한 값 | undefined |
writable | 값을 바꿀 수 있나 | true | false |
get / set | 읽기·쓰기 때 부를 함수 | 없음 | undefined |
enumerable | for...in·Object.keys에 나오나 | true | false |
configurable | 지우거나 설정을 바꿀 수 있나 | true | false |
기본값이 정반대라는 점이 핵심 함정이다. { a: 1 }이나 obj.a = 1로 만든 속성은 세 플래그가 모두 true지만, Object.defineProperty로 만들면서 생략한 플래그는 모두 false가 된다.
읽고 정의하기
const user = { name: 'river' }
Object.getOwnPropertyDescriptor(user, 'name')
// { value: 'river', writable: true, enumerable: true, configurable: true }
Object.defineProperty(user, 'id', { value: 42 }) // 나머지는 전부 false
user.id = 7
user.id // 42: sloppy mode에서는 조용히 무시된다
Object.keys(user) // ['name'], enumerable: false라 안 나온다
delete user.id // false, configurable: false (strict mode에서는 TypeError)| API | 하는 일 |
|---|---|
Object.getOwnPropertyDescriptor(obj, key) | 속성 하나의 설명자 |
Object.getOwnPropertyDescriptors(obj) | 모든 자기 속성의 설명자. Symbol 키와 열거 불가 속성까지 포함한다. getter·setter까지 보존해 복사할 때 Object.defineProperties({}, Object.getOwnPropertyDescriptors(src)) |
Object.defineProperty(obj, key, desc) | 속성 하나를 만들거나 고친다 |
Object.defineProperties(obj, { key: desc, … }) | 여러 개를 한 번에 |
Object.create(proto, { key: desc, … }) | 프로토타입을 정해 새 객체를 만들면서, 두 번째 인자로 자기 속성을 설명자 형식으로 정의한다 |
writable: false인 속성에 쓰면 sloppy mode에서는 조용히 실패하고 strict mode에서는 TypeError 가 난다(strict mode). 모듈 코드는 strict이므로 대개 에러로 드러난다.
configurable: false여도 writable: true면 값은 바꿀 수 있다. configurable은 값이 아니라 속성의 "설정"을 잠근다. 한 번 false로 만들면 defineProperty로도 되돌릴 수 없다.
configurable: false일 때 | 가능 여부 |
|---|---|
delete obj.key | 불가 |
configurable·enumerable 바꾸기 | 불가 |
writable을 false → true | 불가 |
writable을 true → false | 가능(더 잠그는 방향만 허용) |
접근자의 get·set 교체 | 불가 |
writable: false와 configurable: false를 함께 주면 영원히 바뀌지 않는 상수 속성이 된다. 내장 Math.PI가 그렇다. 세 플래그가 모두 false라 Math.PI = 3은 무시(strict mode에서는 TypeError)되고 지울 수도 없다.
접근자 속성: getter와 setter
접근자 속성은 속성처럼 읽고 쓰지만 실제로는 함수가 돈다. 객체 리터럴과 class에서는 get·set 키워드로 쓰고, 그 결과가 접근자 설명자다.
const temp = {
celsius: 20,
get fahrenheit() { return this.celsius * 1.8 + 32 },
set fahrenheit(f) { this.celsius = (f - 32) / 1.8 },
}
temp.fahrenheit = 212
temp.celsius // 100
Object.getOwnPropertyDescriptor(temp, 'fahrenheit')
// { get: ƒ, set: ƒ, enumerable: true, configurable: true }: value·writable이 없다- getter만 있고 setter가 없으면 대입이 무시된다(strict mode에서는
TypeError). - 한 설명자에
value와get을 함께 주면TypeError다. - class의
get·set은 인스턴스가 아니라prototype에 정의된다(class 문법). Object.defineProperty(obj, 'x', { get() {…}, set(v) {…} })로도 만든다. 이때enumerable을 주지 않으면false라서for...in에 나오지 않는다.
접근자는 두 가지 용도로 쓴다.
| 용도 | 방법 |
|---|---|
| 값 검증·통제 | 실제 값은 _name(관례상 내부용) 또는 #name(언어가 막는 private)에 두고, setter에서 검사한 뒤 저장한다 |
| 호환성 유지 | 저장 형식이 바뀌어도 옛 속성 이름을 계산된 getter로 남긴다. age를 저장하다 birthday로 바꿨다면 get age()가 생일로 나이를 계산해, user.age를 읽는 기존 코드를 고치지 않아도 된다 |
function User(name, birthday) {
this.name = name
this.birthday = birthday
Object.defineProperty(this, 'age', {
get() { return new Date().getFullYear() - this.birthday.getFullYear() },
})
}enumerable이 정하는 것
열거 불가능한 속성은 "있지만 순회에는 안 나오는" 속성이다. 내장 메서드(Object.prototype.toString, 배열의 length, class 메서드)가 모두 enumerable: false라서 for...in에 끼어들지 않는다.
| 기능 | 열거 불가 속성 | 상속 속성 | Symbol 키 |
|---|---|---|---|
for...in | 제외 | 포함(열거 가능한 것만) | 제외 |
Object.keys·values·entries | 제외 | 제외 | 제외 |
{ ...obj }, Object.assign | 제외 | 제외 | 포함 |
JSON.stringify | 제외 | 제외 | 제외 |
Object.getOwnPropertyNames | 포함 | 제외 | 제외 |
Reflect.ownKeys | 포함 | 제외 | 포함 |
key in obj | 포함 | 포함 | 포함 |
그래서 spread로 만든 복사본은 프로토타입과 열거 불가 속성을 잃고, getter는 그 순간의 값으로 바뀐다. 프로토타입·플래그·접근자까지 그대로인 얕은 복사본은 Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))로 만든다. 자기 속성인지는 Object.hasOwn(obj, key)로 판별한다(프로토타입과 상속).
객체 전체를 잠그기
속성 하나가 아니라 객체 단위로 플래그를 일괄 적용하는 메서드도 있다. 모두 얕게 동작한다. 중첩 객체는 따로 잠가야 한다.
| 메서드 | 속성 추가 | 속성 삭제 | 값 변경 | 판별 |
|---|---|---|---|---|
Object.preventExtensions(obj) | 불가 | 가능 | 가능 | Object.isExtensible |
Object.seal(obj) | 불가 | 불가(모든 속성 configurable: false) | 가능 | Object.isSealed |
Object.freeze(obj) | 불가 | 불가 | 불가(모든 데이터 속성 writable: false) | Object.isFrozen |
중첩 객체까지 모두 얼린 불변 객체가 필요하면 객체인 속성을 재귀로 따라가며 freeze한다.
function deepFreeze(target) {
if (target && typeof target === 'object' && !Object.isFrozen(target)) {
Object.freeze(target)
Object.values(target).forEach(deepFreeze)
}
return target
}const와 Object.freeze의 차이는 변수와 데이터 타입의 「const와 Object.freeze」에 있다.
정리
- 속성마다
writable·enumerable·configurable(또는get·set) 설정이 숨어 있다. - 리터럴로 만든 속성은 모두
true,defineProperty로 만들면서 생략한 플래그는 모두false다. - 쓰기 실패는 sloppy mode에서 조용히 무시되고 strict mode에서
TypeError가 된다. enumerable: false는for...in·Object.keys·spread·JSON에서 빠진다. 내장 메서드가 순회에 안 나오는 이유다.- 객체 단위 잠금은
preventExtensions<seal<freeze순으로 강하고, 모두 얕다.
관련 노트: 프로토타입과 상속, class 문법, 배열·객체·문자열 다루기, strict mode, Symbol, Proxy와 Reflect
원본 기록8
- 원문 (새 창) web.dev/learn/javascript/objects/property-descriptors
- 원문 (새 창) web.dev/learn/javascript/control-flow
- 원문 (새 창) web.dev/learn/javascript/collections/indexed
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- 원문 (새 창) developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
- 원문 (새 창) ko.javascript.info/property-descriptors
- 원문 (새 창) ko.javascript.info/property-accessors
- GitHub 원문 (새 창) github.com/AI-Dev-Lex-Kim/Frontend_TIL/blob/master/Javascript/프로퍼티 어트리뷰트.md