> For the complete documentation index, see [llms.txt](https://onemorebottlees-organization.gitbook.io/onemorebottlees-til/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onemorebottlees-organization.gitbook.io/onemorebottlees-til/book/deep-dive/16.md).

# 16장 프로퍼티 어트리뷰트

### 16.1 내부 슬롯과 내부 메서드

내부 슬롯과 내부 메서드는 JS 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드다. (\[\[…]])로 감싼 이름…

내부 슬롯과 내부 메서드는 실제 동작하지만 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아니다. 즉, 내부 슬롯과 내부 메서드는 JS 엔진의 내부 로직이므로 원칙적으로 접근, 호출 방법을 제공하지 않는다. (일부 제외)

모든 객체는 \[\[Prototype]]이라는 내부 슬롯을 갖는다. \_\_proto\_\_를 통해 간접적으로 접근 가능하다.

***

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

JS 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 **프로퍼티 어트리뷰트**를 기본값으로 자동 정의한다. 프로퍼티의 상태란 프로퍼티의 값 value, 값의 갱신 가능 여부 writable, 열거 가능 여부 enumerable, 정의 가능 여부 configurable를 말한다.

프로퍼티 어트리뷰트는 JS 엔진이 관리하는 내부 상태 값인 내부 슬롯 \[\[Value]], \[\[Writable]], \[\[Enumerable]], \[\[Configurable]]이다. 따라서 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용해 간접적으로 확인 가능하다.

```jsx
const person = {
	name: 'Lee'
}

console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

// Object.getOwnPropertyDescriptor(객체참조, 프로퍼티 키)
```

Object.getOwnPropertyDescriptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크럽터 객체를 반환한다. (존재하지 않으면 undefined)

ES8의 Object.getOwnPropertyDescriptors 는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크럽터 객체들을 반환한다.

***

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

프로퍼티는 **데이터 프로퍼티**와 **접근자 프로퍼티**로 구분할 수 있다.

데이터 프로퍼티 - 키와 값으로 구성된 일반적인 프로퍼티

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

#### 16.3.1 데이터 프로퍼티

데이터 프로퍼티는 다음의 프로퍼티 어트리뷰트를 JS 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의된다.

| 프로퍼티 어트리뷰트         | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명                             |
| ------------------ | ------------------- | ------------------------------ |
| \[\[Value]]        | value               | 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값 |
| \[\[Writable]]     | writable            | 값의 변경 가능 여부                    |
| 초깃값 true           |                     |                                |
| \[\[Enumerable]]   | enumerable          | 열거 가능 여부                       |
| 초깃값 true           |                     |                                |
| \[\[Configurable]] | configurable        | 재정의 가능 여부                      |
| 초깃값 true           |                     |                                |

#### 16.3.2 접근자 프로퍼티

| 프로퍼티 어트리뷰트         | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명                                          |
| ------------------ | ------------------- | ------------------------------------------- |
| \[\[Get]]          | get                 | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수  |
| \[\[Set]]          | set                 | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수 |
| \[\[Enumerable]]   | enumerable          | 데이터 프로퍼티의 \[\[Enumerable]]과 같다              |
| \[\[Configurable]] | configurable        | 데이터 프로퍼티의 \[\[Configurable]]과 같다            |

접근자 함수는 getter / setter 함수로도 부른다.

```jsx
const person = {
	// 데이터 프로퍼티
	firstName: 'ByoungJu',
	lastName: 'Han',

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

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

// 데이터 프로퍼티를 통한 프로퍼티 값 참조
console.log(person.firstName + ' ' + person.lastName) // ByoungJu Han

// 접근자 프로퍼티를 통한 프로퍼티 값 저장
person.fullName = 'Heungmin Son';
console.log(person) // {firstName: "Heungmin", lastName: "Son"}

// 접근자 프로퍼티를 통한 프로퍼티 값 참조
console.log(person.fullName) // Heungmin Son

//
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log(descriptor)
// {value: "Heungmin", writable: true, enumberable: true, configurable: true}

descriptor = Object.getOwnPropertyDescriptor(person, 'fullName')
console.log(descriptor)
// {get: f, set: f, enumberable: true, configurable: true}
```

> **프로토타입 Prototype**\
> \
> **어떤 객체의 상위 객체의 역할을 하는 객체. 하위 객체에 자신의 프로퍼티와 메서드를 상속한다. 프로토타입 체인은 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조를 말한다. 객체의 프로퍼티나 메서드에 접근하려 할 때 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 프로토타입 체인을 따라 프로토타입의 프로퍼티나 메서드를 차례대로 검색한다.**

***

### 16.4 프로퍼티 정의

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

Object.defineProperty 메서드를 사용하면 프로퍼티의 어트리뷰트를 정의할 수 있다. 인수로는 객체의 참조와 데이터 프로퍼티의 키인 문자열, 프로퍼티 디스크립터 객체를 전달한다.

```jsx
const person = {}

// 데이터 프로퍼티 정의
Object.defineProperty(person, 'firstName', {
	value: 'ByungJu',
	writable: true,
	enumerable: true,
	configurable: true
})

Object.defineProperty(person, 'lastName', {
	value: 'Han'
})

let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log('firstName', descriptor)
// firstName {value: "ByungJu", writable: true, enumerable: true, configurable: true}

// 디스크립터 객체의 프로퍼티를 누락하면 undefined, false가 기본값이다.
descriptor = Object.getOwnPropertyDescriptor(person, 'lastName')
console.log('lastName', descriptor)
// lastName {value: "Han", writable: false, enumerable: false}

// [[Enumerable]]의 값이 false인 경우
// 해당 프로퍼티는 for ... in 문이나 Object.keys 등으로 열거할 수 없다.
// lastName 프로퍼티는 false이므로 열거되지 않는다.
console.log(Object.keys(person)) // [firstName]

// [[Writable]]의 값이 false인 경우
// 해당 프로퍼티의 [[value]] 값을 변경할 수 없다.
// 이때 값을 변경하면 에러 없이 무시된다.
person.lastName = 'Kim'; // 무시됨

// [[Configurable]]의 값이 false인 경우
// 해당 프로퍼티를 삭제할 수 없다.
// 이때 값을 삭제하면 에러 없이 무시된다.
delete person.lastName; // 무시됨

// [[Enumerable]]의 값이 false인 경우
// 해당 프로퍼티를 재정의할 수 없다.

descriptor = Object.getOwnPropertyDiscriptor(person, 'lastName')
console.log('lastName', descriptor)
// lastName {value: "Han", writable: false, enumerable: false, configurable: false}

// 접근자 프로퍼티 정의
Object.defineProperty(person, 'fullName', {
	// getter 함수
	get() {
		return `${this.firstName} ${this.lastName}`
	}

	// setter 함수
	set(name) {
		[this.firstName, this.lastName] = name.split(' ')
	},
	enumerable: true,
	configurable: true
})

descriptor = Object.getOwnPropertyDescriptor(person, 'fullName')
console.log('fullName', descriptor)
// fullName {get: f, set: f, enumerable: true, configurable: true}

person.fullName = 'ByoungJu Han'
console.log(person) // {firstName: "ByoungJu", lastName: "Han"}
```

Object.defineProperty 메서드로 프로퍼티 정의할 때 프로퍼티 디스크립터 객체의 프로퍼티를 일부 생략할 수 있다. 다음과 같이 기본값이 적용된다.

| 프로퍼티 디스크립터 객체의 프로퍼티 | 대응하는 프로퍼티 어트리뷰트    | 생략했을 때의 기본값 |
| ------------------- | ------------------ | ----------- |
| value               | \[\[Value]]        | undefined   |
| get                 | \[\[Get]]          | undefined   |
| set                 | \[\[Set]]          | undefined   |
| writable            | \[\[Writable]]     | false       |
| enumerable          | \[\[Enumerable]]   | false       |
| configurable        | \[\[Configurable]] | false       |

Object.defineProperty 메서드는 한번에 하나의 프로퍼티만 정의할 수 있고,

Object.defineProperties 메서드는 한 번에 여러 프로퍼티를 정의할 수 있다.

***

### 16.5 객체 변경 방지

객체는 변경 가능한 값이므로 재할당 없이 변경할 수 있다. 즉, 프로퍼티를 추가, 삭제할 수 있고, 프로퍼티 값을 갱신할 수 있으며, Object.defineProperty 또는 Object.defineProperties 메서드를 사용하여 프로퍼티 어트리뷰트를 재정의할 수도 있다.

JS는 객체의 변경을 방지하는 다양한 메서드를 제공한다. 객체 변경 방지 메서드들은 객체의 변경을 금지하는 강도가 다르다.

#### 16.5.1 객체 확장 금지

**Object.preventExtensions 메서드**는 객체의 확장을 금지한다.

**객체 확장 금지**란 프로퍼티 추가 금지를 의미한다.

즉, 확장이 금지된 객체는 프로퍼티 추가가 금지된다. **Object.isExtensible 메서드**로 확인할 수 있다.

```jsx
const person = {name: 'Han'}

console.log(Object.isExtensible(person)) // true => 확장 가능

// 확장 금지
Object.perventExtensions(person)
console.log(Object.isExtensible(person)) // false => 확장 불가

person.age = 27 // 무시 strict mode에서는 에러
console.log(person) // {name: 'Han'}

// 프로퍼티 추가는 금지되지만 삭제는 가능하다.
delete person.name
console.log(person) // {}

// 프로퍼티 정의에 의한 프로퍼티 추가도 금지된다.
Object.defineProperty(person, 'age', {value: 29})
// TypeError ~~~~
```

#### 16.5.2 객체 밀봉

**Object.seal 메서드는 객체를 밀봉한다.**

**객체 밀봉**이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미한다.

즉, 밀봉된 객체는 읽기와 쓰기만 가능하다. **Object.isSealed 메서드**로 확인할 수 있다.

```jsx
const person = {name: "Han"}

console.log(Object.isSealed(person)) // false => 밀봉된 객체가 아님

Object.seal(person)
console.log(Object.isSealed(person)) // true => 밀봉된 객체

console.log(Object.getOwnPropertyDescriptors(person))
// {name: {value: "Han", writable: true, enumerable: true, configurable: false}}

// 프로퍼티 추가가 금지된다.
person.age = 20 // 무시 strict mode에서는 에러
console.log(person) // {name: 'Han'}

// 프로퍼티 삭제도 금지된다.
delete person.name
console.log(person) // {name: 'Han'}

// 값 갱신은 가능
person.name = "Son"
console.log(person) // {name: 'Son'}

// 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, 'name', {configurable: true})
// TypeError ~~~
```

#### 16.5.3 객체 동결

**Object.freeze 메서드는 객체를 동결한다.**

**객체 동결**이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지를 의미한다.

즉, 동결된 객체는 읽기만 가능하다. **Object.isFrozen 메서드**로 확인할 수 있다.

```jsx
const person = {name: 'Han'}

console.log(Object.isFrozen(person)) // false => 동결된 객체가 아님

Object.freeze(person)
console.log(Object.isFrozen(person)) // true => 동결된 객체

console.log(Object.getOwnPropertyDescriptors(person))
// {name: {value: "Han", writable: false, enumerable: true, configurable: false}}

// 프로퍼티 추가가 금지된다.
person.age = 20 // 무시 strict mode에서는 에러
console.log(person) // {name: 'Han'}

// 프로퍼티 삭제도 금지된다.
delete person.name
console.log(person) // {name: 'Han'}

// 값 갱신도 금지
person.name = "Son"
console.log(person) // {name: 'Han'}

// 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, 'name', {configurable: true})
// TypeError ~~~
```

#### 16.5.4 불변 객체

위의 세 메서드는 얕은 변경 방지로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못한다.

```jsx
const person = {
	name: 'Han',
	address: {city: 'Seoul'}
}

// 얕은 객체 동결
Object.freeze(person)

// 직속 프로퍼티만 동결한다.
console.log(Object.isFrozen(person)) // true
// 중첩 객체까지 동결하지 못함
console.log(Object.isFrozen(person.address)) // false

person.address.city = 'London'
console.log(person) // {name: "Han", address: {city: "London"}}
```

객체의 중첩 객체까지 동결해 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 재귀적으로 **Object.freeze 메서드**를 호출해야 한다.

```jsx
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: 'Han',
	address: {city: 'Seoul'}
}

deepFreeze(person)

console.log(Object.isFrozen(person)) // true
console.log(Object.isFrozen(person.address)) // false

person.address.city = 'London'
console.log(person) // {name: "Han", address: {city: "Seoul"}}
```
