본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
자바스크립트의 객체는 프로퍼티를 저장하고, 이 프로퍼티는 키-값
쌍 구조를 가진다. 그러나 사실 객체의 프로퍼티는 그 자체로 더 유연하고 강력한 자료구조이다.
통상적으로 선언한 객체의 프로퍼티는 값(value)
을 가지지만, 이 외에 플래그(flag)
라고 불리는 특별한 속성 3가지를 더 가질 수 있다.
writable
: true
라면 값을 수정 가능. 그렇지 않다면 읽기만 가능.enumerable
: true
이면 반복문을 사용해 나열 가능. 그렇지 않다면 반복문을 사용해 나열 불가configurable
: true
이면 프로퍼티 삭제나 플래그 수정 가능. 그렇지 않다면 프로퍼티 삭제와 플래그 수정이 불가앞서 객체를 다루었던 일반적인 방식으로 객체를 선언하게 되는 경우 프로퍼티 플래그는 모두 true
가 된다. 프로퍼티 플래그는 객체 내장 메서드를 통해 접근 및 수정이 가능하다.
Object.getOwnPropertyDescriptor
메서드를 사용하면 특정 프로퍼티에 대한 정보를 모두 얻을 수 있다.
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
// obj는 정보를 얻고자 하는 객체
// propertyName은 정보를 얻고자 하는 객체 내 프로퍼티
해당 메서드를 호출하면 프로퍼티 설명자(descriptor)
라고 불리는 객체가 반환되고, 해당 객체는 프로퍼티의 값과 세가지 플래그에 대한 정보를 모두 담고 있다.
let user = {
name: "KG"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log(JSON.stringify(descriptor, null, 2));
/*
{
"value": "KG",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
Object.defineProperty
메서드를 사용해서 특정 프로퍼티에 대한 플래그를 변경할 수 있다.
Object.defineProperty(obj, propertyName, descriptor);
// obj는 설명자를 적용하고 싶은 객체
// propertyName은 설명자를 적용하고 싶은 객체 프로퍼티
// descriptor는 적용하고자 하는 프로퍼티 설명자
defineProperty
는 일반 객체 선언과 달리 넘겨받는 정보(descriptor
)가 없다면 모든 플래그 값을 false
로 선언한다. 특정 플래그 값을 true
로 설정하기 위해서는 descriptor
안에 원하는 플래그 값을 true
로 명시해주어야 한다.
let user = {};
// 일반 객체 선언 방식이 아닌
// defineProperty를 사용해 선언 (value만 설정)
Object.defineProperty(user, "name", {
value: "KG"
});
let descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(JSON.stringify(descriptor, null, 2));
/*
{
"value: "KG",
"writable": false,
"enumerable": false,
"configurable": false,
}
*/
writable
플래그를 통해 객체의 특정 프로퍼티를 읽기 전용으로 만들 수 있다.
let user = {
name: "KG"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "SJ"; // Error: ... read only ...
// 또는 처음부터 defineProperty를 통해 선언할 수 있다.
// 아래의 결과는 위와 동일하다.
let user = {};
Object.defineProperty(user, "name", {
value: "KG",
// writable은 명시하지 않음으로 false로 초기화
enumerable: true,
configurable: true
});
user.name = "SJ"; // Error: ... read only ...
에러는 엄격모드(
use strict
)에서만 발생한다. 비엄격모드에서는 에러는 발생하지 않지만 값을 변경하려는 시도가 무시된다.
모든 객체에는 객체가 텍스트 값으로 표시되거나 객체가 문자열이 예상되는 방식으로 참조 될 때 자동으로 호출되는 toString()
메서드가 있다. 그러나 이 메서드는 기본적으로 열거 불가(non-enumerable)하기 때문에 for...in
사용 시 나타나지 않는다. 그렇지만 일반 방식으로 커스텀 toString()
메서드 선언 시에는 enumerable
플래그 값이 true
로 설정되기 때문에 for...in
사용 시 해당 값을 출력할 수 있다.
let userA = {
name: "KG"
};
let userB = {
name: "SJ",
toString() {
return this.name;
}
};
for(let key in userA)
console.log(key); // name만 출력
for(let key in userB)
console.log(key); // name, toString 출력
만일 커스텀 toString
메서드 역시 열거 불가능 속성으로 지정하고 싶다면 defineProperty
메서드를 이용하여 enumerable
값을 false
로 지정해주면 된다.
let userB = { ... };
Object.defineProperty(user, "toString", {
enumerable: false
});
for(let key in userB)
console.log(key); // name만 출력
열거 불가능으로 지정된 프로퍼티는
Object.keys()
메서드에서도 배제된다.
configurable
플래그는 false
로 지정되면 해당 프로퍼티를 객체에서 지울 수 없는 것은 무론, 쓰기와 열거 모두 불가하다. 즉 재구성(non-configurable)이 불가능하다. 자바스크립트에서는 Math.PI
와 같은 프로퍼티가 대표적으로 기본값이 configurable: false
로 설정되어 있다.
configurable
값을 false
로 설정하면 다시 돌이킬 수 있는 방법이 없다. 선언 시점에서부터 재구성이 불가능하도록 설정되기 때문이다. 해당 플래그가 만들어내는 구체적인 제약사항은 다음과 같다.
configurable
플래그 수정 불가enumerable
플래그 수정 불가writable: false
의 값을 true
로 변경 불가 (그러나 반대는 가능: true
-> false
)get/set
변경 불가 (새로 생성은 가능)non-configurable 과 non-writable은 다르다.
configurable
이false
라도writable
이true
라면 프로퍼티 값 변경이 가능하다.
configurable: false
는 플래그 값 변경 또는 프로퍼티 삭제를 위해 만들어진 플래그이다.
그러나 두 플래그가 모두false
라면 프로퍼티 값 변경 역시 불가하고 다시true
로 돌아갈 수 없다.
Object.defineProperties
메서드를 사용하면 프로퍼티 여러개를 한번에 정의할 수 있다.
Object.defineProperties(user, {
name: { value: "KG", writable: false },
surname: { value: "LEE", writable: false }
});
Object.getOwnPropertyDescriptors(obj)
메서드를 사용하면 해당 객체의 모든 프로퍼티 설명자를 전부 가져올 수 있다. 해당 메서드를 사용해서 객체를 복사하면 객체의 값 뿐만이 아니라 플래그까지 모두 복사가 가능하다.
또한 기본적으로 숨김 처리된 심볼(Symbol)형으로 선언된 프로퍼티까지 포함한 프로퍼티 설명자 전체를 반환한다.
const clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
// for...in을 통한 일반적인 방식
for (const key of obj) {
clone[key] = obj[key];
}
프로퍼티 설명자는 특정 프로퍼티 하나를 대상으로 한다. 아래의 메서드는 한 객체 내 전체 프로퍼티를 대상으로 하는 제약사항을 적용할 수 있다.
Object.preventExtensions(obj)
Object.seal(obj)
configuralbe: false
와 같은 효과Object.freeze(obj)
configurable: false, writable: false
와 같은 효과객체의 프로퍼티는 두 가지로 구분할 수 있다.
데이터 프로퍼티(data property)
키-값
의 구조로 저장되는 프로퍼티접근자 프로퍼티(accessor property)
접근자 프로퍼티는 객체 지향형 프로그래밍 언어에서 보이는
getter
및setter
와 매우 유사하다.
접근자 프로퍼티는 getter
와 setter
메서드로 표현되고, 이는 객체 리터럴 안에서 각각 get
과 set
으로 나타낼 수 있다.
let user = {
get propName() {
// getter, obj.propName 반환
},
set propName(value) {
// setter, obj.propName = value로 설정
}
}
let user = {
name: "KG",
surname: "LEE",
// getter
get fullName() {
return `${this.name} ${this.surname}`;
},
// setter
set fullName() {
[this.name, this.surname] = value.split(" ");
}
}
console.log(user.fullName); // KG LEE
user.fullName ="SJ WOO"; // setter(value) 사용
console.log(user.fullName); // SJ WOO
위와 같이 getter
와 setter
메서드를 구현하면 객체엔 fullName
이라는 가상의 프로퍼티가 생긴다. 가상의 프로퍼티는 분명 읽고 쓸 수는 있지만 가상이기 때문에 실제로 존재하지는 않는다.
데이터 프로퍼티와 설명자 프로퍼티의 설명자는 살짝 다르다. 접근자 프로퍼티는 그 값을 가상으로 읽고 쓰기 때문에 value
와 writable
플래그가 존재하지 않는다. 대신 get
과 set
이라는 함수를 가진다. 따라서 Object.defineProperty
를 통해서도 getter
와 setter
를 설정할 수 있다.
get
: 인수가 없는 함수로 프로퍼티를 읽을 때 동작set
: 인수가 하나인 함수로 프로퍼티를 쓸 때 동작enumerable
: 데이터 프로퍼티와 동일configurable
: 데이터 프로퍼티와 동일let user = {
name: 'KG',
surname: 'LEE',
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
데이터 프로퍼티와 접근자 프로퍼티는 반드시 한 종류에만 속해야 한다. 따라서 get/set
플래그와 value/writable
플래그를 동시에 설정하면 에러가 발생한다.
getter
와 setter
를 실제 프로퍼티 값을 감싸는 래퍼(wrapper)처럼 활용한다면, 프로퍼티 값을 원하는 대로 통제할 수 있다.
let user = {
get name() {
return this._name;
},
// 4글자 미만으로 입력한 이름은 처리하지 않도록 설정
set name(value) {
if (value.length < 4) {
alert("입력한 값이 너무 짧습니다. 네 글자 이상으로 입력해주세요.");
return;
}
this._name = value;
}
};
user
의 이름은 실제 프로퍼티 _name
에 저장되고, 이에 접근하고 수정하는 것은 오직 getter
와 setter
에 의해 이루어진다. 문법적으로는 user._name
으로 바로 이름에 접근할 수는 있지만, 관습적으로 언더바(_
)로 시작하는 프로퍼티는 오직 객체 내부에서만 활용하고 외부에서는 건드리지 않는다.
접근자 프로퍼티는 언제 어느 때나 getter
와 setter
를 사용해 데이터 프로퍼티의 행동과 값을 원하는 대로 조정할 수 있게 한다는 점에서 유용하다.
만약 어떤 객체의 데이터 프로퍼티가 다음과 같이 선언되어 있었다고 가정하자.
function User(name, age) {
this.name = name;
this.age = age;
}
let KG = new User("KG", 20);
이때 만약 해당 객체의 age
프로퍼티 대신 birthday
프로퍼티가 저장되어야 하는 상황이 발생하면, 기존 코드의 age
프로퍼티를 모두 수정해주어야 한다. 즉 age
프로퍼티를 사용하고 있는 이미 기존에 선언된 객체 역시 일일이 찾아 수정해야 하는데 이는 매우 번거로울 뿐더러 시간이 오래 소요되는 작업이다.
만약 age
프로퍼티 처럼 굳이 제거될 필요가 없는 값이라면 굳이 이를 제거하지 말고 그대로 두되, getter
를 이용하여 age
에 대해 계속 접근할 수 있도록 설정할 수 있다. 이처럼 설정하면 기존에 생성된 객체 역시 age
프로퍼티를 계속 유지할 수 있다.
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
// 객체 리터럴 방식의 객체 생성 방식이 아니기 때문에
// Object.defineProperty 메세드를 이용해 getter 선언
// 이때 전달하는 obj는 함수 성질에 맞춰 this를 전달한다.
Object.defineProperty(this, "age", {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let kg = new User('KG', new Date(1995, 04, 27));
// birthday와 age 모두 사용 가능
console.log(kg.birthday);
console.log(kg.age);