프로퍼티 어트리뷰트

오늘은 프로퍼티 어트리뷰트에 대해서 알아보자. 한국말로 번역하면 속성 속성이다. 하지만 자바스크립트에서 속성은 두가지로 구분하여 사용하여야 한다. 이번 시간에는 속성 중 하나인 프로퍼티에 대해 알아보자

내부슬롯 internal slot

내부슬롯과 내부메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 프로퍼티와 메서드이다.

내부슬롯은 개발자가 직접 접근할 수는 없다.

하지만, [[Prototype]] 내부슬롯은 proto를 통해 간접적으로 접근할 수 있다.

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

자바스크립트 엔진은 프로퍼티 생성 시 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

1
2
3
4
let a = { name: "kim", age: 28 };
Object.getOwnPropertyDescriptors(a).age.value = 3000;

console.log(a); // {name: 'kim', age: 28}

프로퍼티 어트리뷰트란 내부슬롯이다. 따라서 직접 접근할 순 없지만, Object.getOwnPropertyDescriptor 메서드로 간접적으로 접근할 수 있다.

다만 간접적으로 접근만 가능한 것이므로 위 메서드로 값을 변경하거나 할 수는 없다.

⇒ 프로퍼티 생성될 때 [[value]]는 프로퍼티 값으로 초기화 되고 나머지는 true로 초기화된다.

1
2
3
4
5
const person = {
name : 'Lee'
}

console.log(Object.getOwnPropertyDescriptor(person, 'name'); // {value: 'Lee', writable:true,... configurable: true}
  • 프로퍼티 디스크립터 객체를 반환한다. 존재하지 않거나 상속받은 프로퍼티라면 undefined 반환

데이터 프로퍼티와 접근자 프로퍼티

1. 데이터 프로퍼티

키와 값으로 구성된 일반적인 프로퍼티

  • 프로퍼티의 값 [[value]]
  • 값의 갱신 가능여부 [[writable]] : false이면 프로퍼티의 [[value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티
  • 열거 가능여부 [[enumerable]] : for…in 혹은 Object.keys 메서드로 열거 가능 여부
  • 재정의 가능여부 [[configurable]] : false이면 해당 프로퍼티 삭제, 프로퍼티 어트리뷰트 값의 변경 금지

2. 접근자 프로퍼티

자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티

  • [[get]] : 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[get]]의 값 즉, getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
  • [[set]] : 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[set]]의 값 즉, setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
  • [[enumerate]]
  • [[configurable]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const person = {
// 데이터 프로퍼티
firstName: "Donald",
lastName: "Duck",

// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},

// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
},
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + " " + person.lastName); // Donald Duck

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName값을 저장하면 setter 함수가 호출
person.fullName = "Daisy Duck";
console.log(person); // {firstName: "Daisy", lastName: "Duck"}

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출
console.log(person.fullName); // Daisy Duck

// firstName은 데이터 프로퍼티
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value:"Daisy", writable: true, enumerable: true, configurable: true}

// fullName은 접근자 프로퍼티
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor); // {get: f, set: f, enumerable: true, configurable: true}

접근자 프로퍼티와 데이터 프로퍼티 구분 방법

1
2
3
4
5
6
7
// 일반 객체의 __proto__는 접근자 프로퍼티
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
// {get: f, set: f, enumerable: true, configurable: true}

// 함수 객체의 prototype은 데이터 프로퍼티
Object.getOwnPropertyDescriptor(function () {}, "prototype");
// {value: {...}, writable: true, enumerable: false, configurable: false}

위 예시는 접근자 프로퍼티의 경우 다음과 같은 프로퍼티 어트리뷰트가 나오고 데이터 프로퍼티의 경우 출력되는 프로퍼티 어트리뷰트가 다른 것을 확인할 수 있다.

프로퍼티 정의

프로퍼티를 정의한다. 라는 것은 새로운 프로퍼티 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.

데이터 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
const person = {};

Object.defineProperty(person, "firstName", {
value: "yiju",
writable: true,
enumerable: true,
configurable: true,
});

Object.defineProperty(person, "lastName", {
value: "Kim",
});
  • Object.defineProperty 메서드로 프로퍼티 어트리뷰트 정의할 수 있다. 인수로는 객체의 참조, 데이터 프로퍼티의 키(문자열), 프로퍼티 디스크립터 객체를 전달한다.
  • 디스크립터 객체 누락시키면 false가 기본값

궁금증

왜 프로퍼티 동적으로 생성해줄 때, 프로퍼티 어트리뷰트 생략하는데 프로퍼티 어트리뷰트값이 true인데, define 메서드를 사용할 때 생략하면 기본값 false?

1
2
3
4
5
6
7
const obj = {};
obj['name'] = 'Kim';
Object.defineProperty(obj,'age',{
value:19,
});

왜 다른가?

동적으로 생성시에 편의를 위해서 true로 설정된다. 그렇지 않은 경우 Object.defineProperty() 메서드를 사용할 때, 생략하는 어트리뷰트는 false로 설정된다.

writable : false

1
2
3
// [[writable]] 값이 false 인 경우 해당 프로퍼티의 [[value]]의 값 변경할 수 없다.
// 에러 발생하지 않고 무시
person.lastName = "soondae";

enumerable : false

1
2
// [[enumerable]] 값이 false 인 경우 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다.
console.log(Object.keys(person)); // ['firstName']

configurable : false

1
2
3
4
5
6
7
// [[configurable]] 값이 false 인 경우 해당 프로퍼티를 삭제할 수 없다.
// 삭제해도 에러없이 무시된다.
delete person.lastName; // false

// 또한, 해당 프로퍼티를 재정의 할 수도 없다.
// Object.defineProperty(person, 'lastName', { enumerable : true });
// Uncaught TypeError: Cannot redefine property: firstName

접근자 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.defineProperty(person, "fullName", {
// getter 함수
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set(name) {
[this.firstName, this.lastName] = name.split(" ");
},
enumerable: true,
configurable: true,
});

person.fullName = "yoonju hong";
console.log(person); // {firstName: 'yoonju', lastName: 'Kim'}
  • lastName은 writable이 false여서 값이 안바뀌는 것을 볼 수 있다.

객체 변경 방지

객체 변경을 방지하는 메서드를 제공한다. 종류에 따라 금지하는 강도가 다르다.

직속 프로퍼티만 방지(얕은 방지)

구분 메서드 프로퍼티 추가 삭제 값 읽기 값 쓰기 프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X (configurable : false)
객체 동결 Object.freeze X X O X (writable : false) X (configurable : false)

객체 확장 금지 확인 메서드 - Object.isExtensible( ) false면 확장 금지된 객체

객체 밀봉 확인 메서드 - Object.isSealed( ) true면 밀봉된 객체

객체 동결 메서드 - Object.isFrozen( ) true면 동결된 객체

깊은 방지를 구현하려면 재귀적으로 객체 값을 갖는 모든 프로퍼티에 대해 Object.freeze 메서드를 호출해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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: "Lee",
address: { city: "Seoul" },
};

// 깊은 객체 동결
deepFreeze(person);

console.log(Object.isFrozen(person));
console.log(Object.isFrozen(person.address));

person.address.city = "Busan";
console.log(person);