제로베이스 네카라쿠배 온라인스쿨 1기 과정(2021. 11 ~ 2021. 12)에서 공부했던 Javascript Deepdive 교재의 내용 요약 및 필자의 생각을 정리한 내용입니다. 원서의 내용 챕터의 일부를 요약한 내용으로 보다 자세한 내용은 원서를 참조 바랍니다.
16장 프로퍼티 어트리뷰트
16.1 내부 슬롯과 내부 메서드
- 내부 슬롯과 내부 메서드란, 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티(pseudo property)와 의사 메서드(psuedo method)다.
- ECMAScript 사양에 등장하는 이중 대괄호([[…]])로 감싼 이름들이 내부 슬롯과 내부 메서드다.
- 내부 슬롯과 내부 메서드는 ECMAScript 사양에 정의된 대로 구현되어 엔진 내에 실제로 동작하지만 개발자가 직접 접근할 수는 없다.
- 일반적으로 직접 접근하거나 호출할 수 있는 방법을 제공하지 않는다.
- 단, 일부 내부 슬롯과 메서드에 한해 간접적으로 접근할 수 있는 수단을 제공한다.
- 모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, proto를 통해 간접적으로 접근할 수 있다.
const o = {};
// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근할 수 없다.
o.[[Prototype]]; // Uncauhgt SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공한다.
o.__proto__
16.2 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체
- 자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
- 프로퍼티 상태란, 프로퍼티 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), **재정의 가능 여부(configurable)**를 말한다.
- 각각 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]]. [[Configurable]]을 간접적으로 확인할 수 있다.
const person = {
name: "Seo",
}
// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, "name"))
// {value: 'Seo', writable: true, enumerable: true, configurable: true};
- 첫 번째 매개변수에는 객체의 참조를 전달하고, 두 번째 매개변수에는 프로퍼티 키를 문자열로 전달한다. (존재하지 않으면 undefined)
- Object.getOwnPropertyDescriptor 메서드는 하나의 프로퍼티에 대해 프로퍼티 디스크립터 객체를 반환하지만, ES8에서 도입된 Object.getOwnPropertyDescriptors 메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
const person = {
name: "Seo",
}
// 프로퍼티 동적 생성
person.age = 20
// 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
console.log(Object.getOwnPropertyDescriptors(person))
/*
{
name: {value: 'Seo', writable: true, enumerable: true, configurable: true},
age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/
16.3 데이터 프로퍼티와 접근자 프로퍼티
프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분할 수 있다.
- 데이터 프로퍼티(Data property)
- 키와 값으로 구성된 일반적인 프로퍼티다.
- 접근자 프로퍼티(accessor property)
- 자체적으로 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수(accessor function)로 구성된 프로퍼티다.
16.3.1 데이터 프로퍼티
- 데이터 프로퍼티가 같은 어트리뷰트는 다음과 같다.
- 이 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 정의된다.
16.3.2 접근자 프로퍼티
- 접근자 프로퍼티(Accessor property)는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.
const person = {
firstName: "Daewon",
lastName: "Seo",
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`
},
// setter 함수
set fullName(name) {
;[this.firstName, this.lastName] = name.split(" ")
},
}
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(person.firstName + " " + person.lastName)
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장.
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
person.fullName = "Sangwon Seo"
console.log(person) // {firstName: "Sangwon", "Seo"};
// 접근자 프로퍼티를 통한 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(person.fullName)
// firstName은 데이터 프로퍼티다.
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
// 프로퍼티 어트리뷰트를 갖는다.
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName")
console.log(descriptor)
// {value: "Sangwon", writable: true, enumerable: true, configurable: ture}
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]]
// 프로퍼티 어트리뷰트를 갖는다.
descriptor = Object.getOwnPropertyDescriptor(person, "fullName")
console.log(descriptor)
// {get: f, set:f, enumerable: true, configurable: true}
- 메서드 앞에 get, set이 붙은 메서드가 getter, setter 함수이고, 함수 이름이 접근자 프로퍼티가 된다.
- 접근자 프로퍼티는 자체적으로 값을 가지지 않으며, 데이터 프로퍼티의 값을 읽거나 저장할 때 관여할 뿐이다.
내부/슬롯 메서드 관점에서 접근자 프로퍼티 fullName으로 프로퍼티 값에 접근하면 내부적으로[[Get]] 내부 메서드가 호출되어 다음과 같이 동작한다. ([[Get]] 내부 메서드의 사양을 만족시키는 구현체가 자바스크립트 엔진에 존재한다는 것이 중요하다.)
- 프로퍼티 키가 유효한지 확인한다.(문자열, 심벌)
- 프로토타입 체인에서 프로퍼티를 검색한다.
- 검색된 fullName 프로퍼티가 데이터 프로퍼티인지 접근자 프로퍼티인지 확인한다.
- 접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트의 값 getter 함수를 호출하여 그 결과를 반환한다.
프로토타입(prototype)
-
프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체다.
-
프로토타입은 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속한다.
-
프로토타입 객체의 프로퍼티나 메서드를 상속받은 하위 객체는 자신의 프로퍼티 또는 메서드인 것처럼 자유롭게 사용할 수 있다.
-
프로토타입 체인은 프로토타입이 단반향 링크드 리스트 형태로 연결되어 있는 상속 구조를 말한다.
-
객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 프로토타입 체인을 따라 프로토타입의 프로퍼티나 메서드를 차례대로 검색한다.
-
접근자 프로퍼티와 데이터 프로퍼티를 구별하는 방법은 다음과 같다.
// 일반 객체의 __proto__는 접근자 프로퍼티다.
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__")
// {get: f, set: f, enumerable: false, configurable: true};
Object.getOwnPropertyDescriptor(function () {}, "prototype")
// {value: {...}, writable: true, enumerable: false, configurable: false}
- Object.getOwnPropertyDescriptor를 통해 반환된 어트리뷰트 객체를 보면 다르다는 것을 알 수 있다.
16.4 프로퍼티 정의
- 프로퍼티 정의란 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.
- Object.definedProperty 메서드를 사용하면 프로퍼티의 어트리뷰트를 정의할 수 있다.
const person = {}
// 데이터 프로퍼티 정의
Object.defineProperty(person, "firstName", {
value: "Daewon",
writable: true,
enumerable: true,
configurable: true,
})
Object.defineProperty(person, "lastName", {
value: "Seo",
})
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName")
console.log("firstName", descriptor)
// firstName {value: 'Daewon', writable: true, enumerable: true, configurable: true}
descriptor = Object.getOwnPropertyDescriptor(person, "lastName")
console.log("lastName", descriptor)
// [[Enumerable]]의 값이 false 인 경우
// 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다.
console.log(Object.keys(person))
// [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없다.
// 이때 값을 변경하면 에러는 발생하지 않고 무시된다.
person.lastName = "Kim"
// [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 삭제할 수 없다.
// 이때 프로퍼티를 삭제하면 에러는 발생하지 않고 무시된다.
delete person.lastName
// [[Configurable]] 값이 false인 경우 해당 프로퍼티를 재정의할 수 없다.
// Object.defineProperty(person, 'lastName', {enumerable: true});
// Uncaught TypeError: Cannot redefined property: lastName;
descriptor = Object.getOwnPropertyDescriptor(person, "lastName")
console.log("lastName", descriptor)
// 접근자 프로퍼티 정의
Object.defineProperty(person, "fullName", {
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)
person.fullName = "Sangwon Seo"
console.log(person)
- Object.defineProperty 메서드로 프로퍼티를 정의할 때 프로퍼티 디스크립터 객체의 프로퍼티를 일부 생략할 수 있으며, 생략된 어트리뷰트는 다음과 같이 기본값이 적용된다.
- value : undefined
- get : undefined
- set : undefined
- writable : false
- enumerable : false
- configurable : false
- Object.definedProperty 메서드는 한번에 하나의 프로퍼티만 적용 가능하며, Object.definedProperties 메서드를 사용하면 여러 개 프로퍼티를 한 번에 정의할 수 있다.
16.5 객체 변경 방지
- 객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다.
- Object.defineProperty 또는 Object.definedProperties 메서드를 사용해 프로퍼티 어트리뷰트를 재정의할 수도 있다.
16.5.1 객체 확장 금지
- Object.preventExtensions 메서드는 객체의 확장을 금지(프로퍼티 추가 금지)한다.
- 확장이 금지된 객체는 프로퍼티 추가가 금지된다.
- 프로퍼티는 프로퍼티 동적 추가와 Object.defineProperty 메서드로 추가할 수 있는데 두 가지 방법이 모두 금지된다.
- 확장이 가능한 객체인지 여부는 Object.isExtensible 메서드로 확인할 수 있다.
const person = { name: "Seo" }
// person 객체는 확장이 금지된 객체가 아니다.
console.log(Object.isExtensible(person)) // true;
// person 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person)
// person 객체는 확장이 금지된 객체다.
Object.isExtensible(person) // false;
// 프로퍼티 추가가 금지된다.
person.age = 20 // 무시 strict 모드에서는 에러
console.log(person) // {name: 'Seo'};
// 프로퍼티 추가는 금지되지만 삭제는 가능하다.
delete person.name
console.log(person) // {}
// 프로퍼티 정의에 의한 프로퍼티 추가도 금지된다.
// Object.defineProperty(person, 'age', { value: 20 });
// TypeError: Cannot defined property age, object is not extensible
Comment : 프로퍼티 삭제는 가능하기 때문에 주의할 것
16.5.2 객체 밀봉
- Object.seal 메서드는 객체를 밀봉한다. 밀봉이란 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미한다.
- 밀봉된 객체는 읽기와 쓰기만 가능하다.
- 밀봉 객체인지 여부는 Object.iseSealed 메서드로 확인할 수 있다.
16.5.3 객체 동결
- Object.freeze 메서드는 객체를 동결한다. 동결이란, 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지, 프로퍼티 값 갱신 금지를 의미한다.
- 동결된 객체는 읽기만 가능하다.
- 동결된 객체인지 여부는 Object.isFrozen 메서드로 확인할 수 있다.
16.5.4 불변 객체
- 위의 변경 방지 메서드들은 얕은 변경 방지(shallow only)로 직속 프로퍼티 변경만 방지되고 중첩 객체까지는 영향을 주지 못한다.
- Object.freeze 메서드로 객체를 동결하여도 중첩 객체까지 동결할 수는 없다.
const person = {
name: "Seo",
address: { city: "Seoul" },
}
// 얕은 객체 동결
Object.freeze(person)
// 직속 프로퍼티만 동결한다.
console.log(Object.isFrozen(person)) // true;
// 중첩 객체까지 동결하진 못한다.
console.log(Object.isFrozen(person.address)) // false;
person.address.city = "Busan"
console.log(person) // {name: 'Seo', address: {city: 'Busan'}};
- 객체의 중첩 객체까지 동결하여 변경 불가능한 읽기 전용 불변 객체를 구현하려면, 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 한다.
function deepFreeze(target) {
// 객체가 아니거나 동결된 객체는 무시하고 객체이고 동겨되지 않은 객체만 동결한다.
if (target && typeof target === "object" && Object.isFrozen(target)) {
Object.freeze(target)
/*
모든 프로퍼티를 순회하면서 재귀적으로 동결한다.
Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환한다.
forEach 메서드는 배열을 순회하며 배열의 각 요소에 대하여 콜백 함수를 실행한다.
*/
Object.keys(target).forEach(key => deepFreeze(target[key]))
}
return target
}
const person = {
name: "Seo",
address: { city: "Seoul" },
}
// 깊은 객체 동결
deepFreeze(person)
console.log(Object.isFrozen(person)) // true
console.log(Object.isFrozen(person.address)) // true
person.address.city = "Busan"
console.log(person) // {name: 'Seo', address: {city: 'Seoul'}}