TL;DR
Object.defineProperty()의 특징
- 객체의 속성을 세밀하게 추가하거나 수정할 수 있습니다.
- 객체에 getter / setter 를 지정할 수 있습니다.
- IE 9 부터 지원합니다.
Object.defineProperty()의 문법
`Object.defineProperty(obj, propertyName, descriptor)`
- `obj`와 `propertyName`: 각각 설명자(속성 서술자)를 적용하고 싶은 객체와 그 객체의 프로퍼티를 의미합니다.
- `descriptor`: 적용하고자 하는 프로퍼티 설명자(속성 서술자) 객체를 의미합니다. 속성 서술자 객체 내부에는 값(value)과 데이터 서술자(플래그 정보)와 접근자 서술자(getter, setter)가 위치하고 있습니다. 서술자에 대해 밑에서 다룹니다.

1. 속성 서술자 (property descriptor)
- 속성 서술자 객체(= description 혹은 descriptor)는 데이터 서술자와 접근자 서술자로 구성됩니다.
- Object.defineProperty에 사용하는 서술자는 두 유형 중 하나여야 합니다.
- 이 둘은 모두 객체로 다음의 두 플래그를 선택적으로 공유할 수 있습니다.
- `configurable`: 객체 속성의 값 변경 및 삭제 가능 여부(Boolean)
- `enumerable`: 객체 속성 열거 시 노출 여부(Boolean)
단, 데이터 서술자의 writable 또는 value는 getter 또는 setter와 함께 쓰일 수 없습니다. 만약, 같이 쓴다면,
`Cannot both specify accessors and a value or writable attribute, #<Object>` 이러한 구문을 보게 될 것입니다.
let user = {
name: 'John',
surname: 'Smith',
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
/**
* @param {string} value
*/
set(value) {
[this.name, this.surname] = value.split(' ');
},
value: 'kyle',
});
// TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
위에서 언급한 선택적으로 공유할 수 있는 데이터 서술자는 접근자 서술자와 함께 쓸 수 있습니다.(지금 이해가 안된다면 밑의 카테고리들을 먼저 보고와도 좋습니다. 근데 보통 같이 사용하는 경우는 없습니다.)
// 접근자 서술자만
let user = {
name: 'John',
surname: 'Smith',
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
/**
* @param {string} value
*/
set(value) {
[this.name, this.surname] = value.split(' ');
},
});
console.log(user); // { name: 'John', surname: 'Smith' }
console.log(user.fullName); // John Smith
user.fullName = 'John Hopkins';
console.log(user.fullName); // John Hopkins
console.log(user); // { name: 'John', surname: 'Hopkins' }
// 데이터 서술자와 접근자 서술자 함께
let user = {
name: 'John',
surname: 'Smith',
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
/**
* @param {string} value
*/
set(value) {
[this.name, this.surname] = value.split(' ');
},
configurable: false,
enumerable: true,
});
console.log(user); // { name: 'John', surname: 'Smith', fullName: [Getter/Setter] }
console.log(user.fullName); // John Smith
user.fullName = 'John Hopkins';
console.log(user.fullName); // John Hopkins
console.log(user); // { name: 'John', surname: 'Hopkins', fullName: [Getter/Setter] }
- description 자리는 빈 객체(`{ }`)라도 들어가야 `value`프로퍼티의 기본값인 `undefined`를 얻을 수 있습니다. 그렇지 않으면 에러를 냅니다.
// description 자리에 {} 빈 객체 전달 시 자동완성 지원
const ob2 = {};
Object.defineProperty(ob2, 'a', {});
console.log(ob2.a); // undefined
// description 전달인자 없을 시 자동완성X, description은 객체여야 한다는 에러 발생.
const ob2 = {};
Object.defineProperty(ob2, 'a');
console.log(ob2.a); // TypeError: Property description must be an object: undefined

- 닷(`.`) 노테이션을 통한 프로퍼티 생성은 자동으로 모든 플래그가 `true`가 되지만, Object.defineProperty메서드를 사용해서 프로퍼티를 만든 경우 따로 플래그 값을 명시하지 않으면 플래그 값이 자동으로 `false`가 됩니다.
데이터 서술자(=데이터 프로퍼티)
데이터 서술자(=데이터 프로퍼티)
데이터 서술자는 값을 가지는 속성을 정의할 때 사용합니다.
이는 다음 프로퍼티들을 선택적으로 포함합니다. 데이터 서술자에 속해있는 프로퍼티들을 '프로퍼티 플래그(=플래그)'라고도 부릅니다. 플래그라고 부르는 것이 더 일반적인 것 같습니다. 이미 defineProperty 메서드 사용 전에 객체에 정의된 프로퍼티들의 플래그들도 수정이 가능합니다.
- value: 속성에 연관된 값 ( any )
🚩프로퍼티 플래그(플래그)
- writable: 할당 연산자 (=)로 값을 바꿀 수 있는지 여부(Boolean)
- configurable: 플래그 값 변경 및 값(value) 삭제 가능 여부(Boolean)
- enumerable: 객체 속성 열거 시 노출 여부(Boolean)
접근자 서술자(=접근자 프로퍼티)
접근자 서술자(=접근자 프로퍼티)
접근자 서술자는 getter, setter를 정의할 때 사용합니다.
이는 다음 키를 선택적으로 포함합니다.
- get: 접근자로 사용할 함수 (속성에 접근할 때 사용한 객체를 this로 지정해서 매개변수 없이 호출. 암시적 바인딩)
- set: 설정자로 사용할 함 (속성에 값을 할당할 때 사용한 객체를 this로 지정해서 한 개의 매개변수와 함께 호출. 암시적 바인딩)
2. 데이터 서술자
데이터 서술자에는 value, writable, enumerable, configurable이 있습니다.
이 중 writable, enumerable, configurable에 대해 알아보도록 합시다.
2-1. writable(값 수정 불가 처리)
`writable: false;`면 해당 프로퍼티는 read only property가 됩니다.
`writable: true;`면 새로운 값으로 갱신이 가능합니다.
주의할 점은 생성한 프로퍼티가 객체 내부에 열거되지는 않는다는 점입니다.
const ob = {};
Object.defineProperty(ob, 'a', {
value: 30,
writable: true,
});
console.log(ob['a']); // 30
ob['a'] = 40;
console.log(ob['a']); // 40
console.log(ob); // {} ---> 🚨주의 : 객체 내부에 열거되지는 않습니다.
ob.a = 50;
console.log(ob.a); //50 ---> 닷(.)노테이션을 통해 값을 얻을 수는 있음.
만약 `writable`이 false이면 `obj.key = value` 방식으로는 값을 변경할 수 없습니다. `writable`이 false일 때 `obj.key = value`로 값을 변경하려고 시도하면, strict mode(`"use strict"`)가 아닐 경우에는 에러가 발생하지 않고 무시되며, strict mode일 경우에는 (심지어) 동일한 값을 할당하려 해도 에러가 발생합니다.
단, `writable`이 false여도 `Object.defineProperty`를 사용해서 `value`를 변경할 수는 있습니다. 대신 이때의 플래그가 또 다음 번 프로퍼티 생성 때 영향을 미칠 것입니다.
const obj = {
name: 'mike',
};
obj.name = 'white';
console.log(obj.name); // white
Object.defineProperty(obj, 'name', {
writable: false,
});
obj.name = 'kyle';
console.log(obj.name); // white
Object.defineProperty(obj, 'name', {
value: 'kyle',
});
// ✅ ---> writable이 생략되고, 게다가 이전에 false였는데도 프로퍼티 값을 바꿀 수 있음
console.log(obj.name); // kyle
obj.name = 'ban';
console.log(obj.name); // kyle
(() => {
'use strict';
obj.name = 'kyle';
})(); // 에러 발생 TypeError: Cannot assign to read only property 'name' of object '#<Object>'
2-2. enumerable( 반복문에서 속성 반환 설정, 정의한 프로퍼티가 열거된다. )
enumerable은 Object.assign과 전개 연산자가 해당 속성을 열거할 수 있는지 결정합니다.(반복문에서 속성 반환 설정)
또한 Symbol이 아닌 속성들에 대해서 `for in`과 `Object.keys`에서의 추출 가능여부를 결정합니다.
`enumerable: false`여도 객체 내에서 사라진 것은 아니기 때문에 직접 닷(.)노테이션으로 프로퍼티에 접근한다면 값을 얻을 수 있습니다.
/**
* enumerable
*/
const ob2 = {
name: 'mike',
};
Object.defineProperty(ob2, 'a', {
value: 30,
enumerable: false,
});
Object.defineProperty(ob2, 'b', {
value: 50,
enumerable: true,
});
console.log(Object.keys(ob2)); // [ 'name', 'b' ]
console.log({ ...ob2 }); // { name: 'mike', b: 50 }
for (const key in ob2) {
console.log(key); // name b
}
console.log(ob2); // { name: 'mike', b: 50 } --> ✅enumerable: true로 지정한 속성이 그대로 객체 내부에 살아있음.
/**
* 참고로 기존 프로퍼티의 플래그도 바꿔버릴 수 있다.
*/
Object.defineProperty(ob2, 'name', {
enumerable: false,
});
console.log(Object.keys(ob2)); // [ 'b' ]
console.log({ ...ob2 }); // { b: 50 }
for (const key in ob2) {
console.log(key); // b
}
console.log(ob2); // { b: 50 } ---> name 프로퍼티 enumerable:false로 바뀌어서 안 찍힘
/**
* for ...in 문, 전개문법, Object.keys에서 뽑을 수 없을 뿐,
* 🚨 직접 닷(.) 노테이션으로 접근하면 값을 얻을 수 있다.
*/
console.log(ob2.name); // mike
2-3. configurable
해당 특성을 사용하면 객체에서 해당 속성을 삭제 혹은 그 속성의 지정자(플래그와 접근자)를 변경할 수 있는지도 결정합니다.
(🚨 값 수정과는 연관없습니다. 값 수정은 `writable` 옵션과 관련있습니다.)
/**
* configurable
*/
const obj3 = {};
Object.defineProperty(obj3, "a", {
value: 30,
configurable: false,
});
console.log(obj3); // {}
delete obj3["a"];
console.log(obj3["a"]); // 30
console.log(obj3); // {} ---> enumerable이 아니라서 열거 불가능.
`configurable:false`가 만들어내는 구체적인 제약사항은 다음과 같습니다.
- 객체의 해당 속성 삭제(delete) 불가능.
- `configurable` 플래그를 수정할 수 없음(같은 플래그인데도 수정 불가능 -> 돌이킬 수 없는 설정)
- `enumerable`플래그를 수정할 수 없음
- `writable: false`의 값을 `true`로 변경할 수 없음(`true`를 `false`로 변경하는 것은 가능)
- 접근자 프로퍼티(접근자 서술자) `get/set`을 변경할 수 없음(새롭게 만드는 것은 가능)
아까 위쪽에서 `writable: false`일 때 defineProperty 메서드로 값(value)을 변경할 수는 있다고 하였는데, `writable: false, configurable: flase`이면, `defineProperty`메서드로도 값(value) 변경이 불가능합니다. 이런 특징을 이용하면 아래와 같이 “영원히 변경할 수 없는” 프로퍼티(`user.name`)를 만들 수 있습니다.
let user = { };
Object.defineProperty(user, "name", {
value: "John",
writable: false,
configurable: false
});
// user.name 프로퍼티의 값이나 플래그를 변경할 수 없습니다.
// 아래와 같이 변경하려고 하면 에러가 발생합니다.
// user.name = "Pete"
// delete user.name
// Object.defineProperty(user, "name", { value: "Pete" })
Object.defineProperty(user, "name", {writable: true}); // Error
물론 `writable`이 true이면 프로퍼티 값 닷(`.`) 노테이션으로 변경, defineProperty로 변경 둘 다 가능합니다.(`non-configurable`과 `non-writable`은 다르다는 점을 인지해주세요.)
한편, 위에서 언급한 "영원히 변경할 수 없는 프로퍼티"를 만들어야 하는 상황이 어떤 게 있을까요?
유용한 사용 예시를 보자면, Math 내장 객체의 PI 프로퍼티의 값을 강제로 변환시키려는 행위를 막을 수 있습니다. 원의 지름에 대한 원둘레(원주)의 비율을 뜻하는 PI(π) 프로퍼티는 3.14...를 나타냅니다. 이는 다른 이상한 값으로 변경 되어서는 안 되는데요? 이 때 실험을 해보면 `configurable: false`가 이미 세팅되어 있음을 알 수 있습니다.
console.log(Math.PI); // 3.141592653589793
// writable이 false 임을 알 수 있다. -> read only property
Math.PI = 'asfdasdf'; // ! TypeError: Cannot assign to read only property 'PI' of object '#<Object>'
const fullDataFlag = {
value: undefined,
configurable: true,
writable: true,
enumerable: true,
};
/**
* Object.defineProperty(Math, 'PI', {
* ^
* ! TypeError: Cannot redefine property: PI
* ---> configurable이 false임을 알 수 있다. ---> 속성 설명자 설정을 영원히 바꾸지 못한다.
*/
Object.defineProperty(Math, 'PI', { // ! 🚨이 줄에서 에러 발생
...fullDataFlag,
value: 'lululala',
});
console.log(Math.PI);
3.접근자 서술자
접근자 서술자에는 get과 set이 있습니다.
이 둘은 흔히 getter와 setter 메서드라고도 불립니다.
3-1. getter(get) / setter(set) ( 값의 할당과 호출 설정 )
`get` 메서드는 인수가 없는 함수로, 프로퍼티를 읽을 때 동작합니다.
`set` 메서드는 인수가 단 하나인 함수로, 프로퍼티에 값을 쓸 때 호출됩니다.
객체의 속성에 getter / setter 를 지정하면 각각 속성에서 값을 읽을 때와 쓸 때 호출시킬 함수를 정의할 수 있습니다.
get과 set은 객체의 내부 상태를 숨기고 외부에서 직접 접근하지 못하게 하는데 도움을 줍니다. 또한 set은 데이터에 대한 유효성 검사를 수행할 수도 있습니다. get과 set을 사용하면 더 직관적이고 사용하기 쉬운 API를 디자인할 수 있습니다.(주관적)
/* 예시 1 */
function MrProminent(name: string, birthday: Date) {
this.name = name;
this.birthday = birthday;
Object.defineProperty(this, "age", {
configurable: false,
// writable: false,
get() {
const todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
},
});
}
const mary = new MrProminent("John", new Date(1997, 4, 13));
console.log(mary.age);
/* 예시 2 */
let user = {
get name() {
return this._name;
},
set name(value: string) {
if (value.length < 4) {
console.error("too short value. Please more than 4 characters");
return;
}
this._name = value;
},
};
user.name = "Pete";
console.log(user.name); // Pete
user.name = ""; // too short value. Please more than 4 characters
console.log(user.name); // Pete
console.log(user); // { name: [Getter/Setter], _name: 'Pete' }
4. 닷노테이션(.)을 통한 정의
보통 자바스크립트에서 속성을 부여할 때는 다음과 같이 닷노테이션(.)을 통해 생성합니다.
const person = {};
person.name = 'example';
이렇게 닷노테이션을 통해 속성을 생성하게 되면 descriptor 객체의 모든 플래그의 값이 자동으로 true로 설정되어 생성됩니다.
반면 맨 위에서 얘기했듯이 Object.defineProperty로 프로퍼티를 만들었을 때 descriptor 객체 내부의 플래그를 따로 명시하지 않으면 자동으로 전부 false가 됩니다.
5. Object.getOwnPropertyDescriptor, Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptor의 특징
메서드를 사용하면 특정 프로퍼티에 대한 정보를 모두 얻을 수 있습니다.
Object.getOwnPropertyDescriptor의 문법
`let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);`
- `obj` : 정보를 얻고자 하는 객체
- `propertyName` : 정보를 얻고자 하는 객체 내 프로퍼티
메서드를 호출하면 프로퍼티 설명자(descriptor) 객체가 반환되는데, 여기에 프로퍼티 값과 세 플래그에 대한 정보가 모두 담겨있습니다.
예시:
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
`Object.getOwnPropertyDescriptor` 메서드를 통해 서술자는 기존 프로퍼티에 서술되어 있는 서술자를 그대로 상속한다는 점을 확인해 볼 수도 있습니다.
const ob = {};
Object.defineProperty(ob, 'a', {
value: 30,
writable: true,
enumerable: true,
});
console.log(ob.a); // 30
Object.defineProperty(ob, 'a', {
value: 50,
});
console.log(ob.a); // 50
ob.a = 60;
console.log(ob.a); // 60
console.log(Object.keys(ob)); // [ 'a' ]
console.log(Object.getOwnPropertyDescriptor(ob, 'a')); // { value: 60, writable: true, enumerable: true, configurable: false }
Object.defineProperty(ob, 'a', {
value: 70,
});
console.log(Object.keys(ob)); // [ 'a' ]
console.log(Object.getOwnPropertyDescriptor(ob, 'a')); // { value: 70, writable: true, enumerable: true, configurable: false }
유용한 사용법 : 속성 설명자까지 복사하기
`Object.getOwnPropertyDescriptors`
위의 'getOwnPropertyDescriptor'에서 's'가 붙었네요. getOwnPropertyDescriptors 메서드를 사용하면 가지고 있는 모든 속성의 모든 프로퍼티 설명자를 얻을 수 있습니다.
for...in문, 전개 문법을 사용하여 객체를 복사했을 때는 속성 설명자 설정을 잃어버리게 됩니다.
getOwnPropertyDescriptor와 getOwnPropertyDescriptors는 속성 설명자 설정 정보까지 그대로 받고 싶다면, 유용하게 사용할 수 있습니다. 아래 예시와 주석을 참고해주세요.
const fullDataFlag = {
value: undefined,
configurable: true,
writable: true,
enumerable: true,
};
const 기존객체 = {};
Object.defineProperties(기존객체, {
welcome: { ...fullDataFlag, value: 'hi', writable: false },
name: { ...fullDataFlag, value: 'brooth', writable: false },
});
console.log(기존객체); // { welcome: 'hi', name: 'brooth' }
/**
* * 속성 설명자 설정을 잃어 버린다.
* 닷(.)노테이션 초기 설정과 동일하게 전부 true가 됨.
* spread 문법, for...in문도 마찬가지이다.
*/
let 다른객체;
다른객체 = { ...기존객체 };
console.log(다른객체); // { welcome: 'hi', name: 'brooth' }
const result = Object.getOwnPropertyDescriptors(다른객체);
console.log(result);
// {
// welcome: { value: 'hi', writable: true, enumerable: true, configurable: true },
// name: {
// value: 'brooth',
// writable: true,
// enumerable: true,
// configurable: true
// }
// }
/**
* * 이전 속성 설명자 설정을 그대로 받는다.
*/
let 또다른객체;
또다른객체 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(기존객체));
console.log(또다른객체); // { welcome: 'hi', name: 'brooth' }
const result2 = Object.getOwnPropertyDescriptors(또다른객체);
console.log(result2);
// {
// welcome: {
// value: 'hi',
// writable: false,
// enumerable: true,
// configurable: true
// },
// name: {
// value: 'brooth',
// writable: false,
// enumerable: true,
// configurable: true
// }
// }
6. 상속과 관련해서 주의해야 할 점
만약 생성자 함수의 프로토타입에 값과 플래그를 설정한다면 어떻게 될까요?
프토토타입은 쉽게 말해서 '유전자'라고 생각하면 좋습니다. 그렇기 때문에 해당 생성자 함수로 만들어진 인스턴스들도 해당 값에 대한 플래그 설정을 따를 수밖에 없습니다.
예를 들어 다음과 같은 상황이 생길 수 있겠습니다. 생성자 함수의 `writable: false` 설정으로 인해서 `y` 프로퍼티에 대한 수정을 하지 못하게 되는 경우입니다.
function MyClass() {}
MyClass.prototype.x = 1;
Object.defineProperty(MyClass.prototype, "y", {
writable: false,
value: 1,
});
const a = new MyClass();
a.x = 2;
console.log(a.x); // 2
console.log(MyClass.prototype.x); // 1
a.y = 2; // 무시, strict mode일 경우 에러 발생
console.log(a.y); // 1
console.log(MyClass.prototype.y); // 1
7. 기타: Object Constructor와 관련된 다양한 메서드
프로퍼티 설명자는 객체 내 특정 프로퍼티 하나를 대상으로 합니다.
아래 메서드를 사용하면 한 객체 내 프로퍼티 전체를 대상으로 하는 제약사항을 만들 수 있습니다.
`Object.preventExtensions(obj)`
객체에 새로운 프로퍼티를 추가할 수 없게 합니다.
`Object.seal(obj)`
새로운 프로퍼티 추가나 기존 프로퍼티 삭제를 막아줍니다. 그러나 밀봉된 객체의 기존 속성은 여전히 변경할 수 있습니다. 프로퍼티 전체에 configurable: false를 설정하는 것과 동일한 효과입니다.
const sealedObj = Object.seal({ prop: 42 });
sealedObj.prop = 33; // 변경은 허용됩니다.
console.log(sealedObj.prop); // 33
`Object.freeze(obj)`
새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정을 막아줍니다. 프로퍼티 전체에 configurable: false, writable: false를 설정하는 것과 동일한 효과입니다. 즉, 객체는 읽기 전용이 됩니다.
const frozenObj = Object.freeze({ prop: 42 });
frozenObj.prop = 33; // 변경이 무시됩니다.
console.log(frozenObj.prop); // 42
아래 메서드들은 위 세 가지 메서드를 사용해서 설정한 제약사항을 확인할 때 사용할 수 있습니다.
`Object.isExtensible(obj)`
새로운 프로퍼티를 추가하는 게 불가능한 경우 false를, 그렇지 않은 경우 true를 반환합니다.
`Object.isSealed(obj)`
프로퍼티 추가, 삭제가 불가능하고 모든 프로퍼티가 configurable: false이면 true를 반환합니다.
`Object.isFrozen(obj)`
프로퍼티 추가, 삭제, 변경이 불가능하고 모든 프로퍼티가 configurable: false, writable: false이면 true를 반환합니다.
위 메서드들은 실무에선 잘 사용되지 않습니다.
중첩 객체까지 동결하기
위에서 언급한 변경 방지 메서드들은 얕은 변경 방지(shallow only)로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지는 못합니다. 따라서 Object.freeze 메서드로 객체를 동결하여도 중첩 객체까지 동결할 수는 없습니다.
그렇기 때문에 중첩 객체까지 동결하려면 내부에 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 합니다.
function deepFreeze(target) { if (target !== null && typeof target === 'object' && !Object.isFrozen(target)) { Object.freeze(target); Object.keys(target).forEach(key => deepFreeze(target[key])); } return target; }