업데이트:

스코프와 변수 키워드를 공부하다가 갑자기 프로퍼티 어트리뷰트라는 새로운 개념이 나와 다소 어렵게 느껴졌다. 그래도 차근히 읽다보면 내용이 반복되서 오히려 이해가 잘되었다 !

[01] 내부 슬롯과 내부 메서드

프로퍼티 어트리뷰트를 이해하기 위해서는 내부 슬롯과 내부 메서드를 알아야 한다.

📢 내부 슬롯, 내부 메서드 : 이중 대괄호 [[ … ]]로 감싼 이름들

이 둘은 자바스크립트 엔진에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부적으로 공개된 객체의 프로퍼티는 아니다.

단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단이 제공된다.

예를 들어, 모든 객체는 [[ Prototype ]] 라는 내부 슬롯을 갖는다. __proto__를 통해 간접적으로 접근할 수 있다.

const o = {};

o.[[Prototype]] // Uncaught SyntaxError: Unexpected token '['
// 직접 접근 불가
o.__proto__ // Object.prototype
// 간접 접근 가능

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

📢 프로퍼티 디스크립터 객체 : 프로퍼티 어트리뷰트 정보를 제공하는 객체

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

프로퍼티 어트리뷰트는 내부 상태 값인 내부 슬롯 [[Value]] (값), [[Writable]](쓸 수 있는지), [[Enumerable]] (열거 가능한지),[[Configurable]](변경 가능한지) 이다. Object.getOwnPropertyDescriptor 메서드를 사용하면 간접적으로 확인할 수 있다.

🥕 ES8 이전

const person = {
	name: 'Lee'
};
console.log(Object.getOwnPropertyDescriptor(person, 'name')); // 프로퍼티 디스크립터 객체 반환
// {value: "Lee", writable: true, enumerable: true, configurable: true}

🥕 ES8 이후

const person = {
	name: 'Lee'
};
person.age = 20;
//모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공
console.log(Object.getOwnPropertyDescriptor(person));
//{name: {value: 'Lee', writable: true, enumerable: true, configurable: true}
//{age: {value: 20,  writable: true, enumerable: true, configurable: true}

[03] 데이터 프로퍼티와 접근자 프로퍼티

데이터 프로퍼티

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

위에서 다룬 코드에서 getOwnPropertyDescriptor 메서드를 사용하면 나오는 특징들이다.

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Value]] value 프로퍼티 값
[[Writable]] writable 값의 변경 가능 여부(false면 읽기 전용)
[[Enumeratable]] enumeratable 열거 가능 여부(for …in, Object.keys)
[[Configurable]] configurable 재정의 가능여부(false면 변경 금지)

프로퍼티가 생성되면 위 프로퍼티 어트리뷰트 중 Value만 할당된 프로퍼티 값으로 초기화 되고 나머지 속성들은 true 값으로 초기화된다.

접근자 프로퍼티

접근자 프로퍼티의 본질은 함수인데, 이 함수는 값을 획득(get)하고 설정(set)하는 역할을 담당한다. 그런데 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.

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

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Get]] get 인수가 없는 함수로, 프로퍼티를 읽을 때 동작함
[[Set]] set 인수가 하나인 함수로, 프로퍼티에 값을 쓸 때 호출됨
[[Enumerable]] enumerable 데이터 프로퍼티와 같음
[[Configurable]] configurable 데이터 프로퍼티와 같음

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


🔍 getter , setter 함수

밑의 코드를 해석하면 get fullName()를 통해 성과 이름을 합쳐 전체이름을 리턴하는 getter함수를 만들었다.

const person = {
	firstName: 'Ungmo', // 데이터 프로퍼티
	lastName: 'Lee', // 데이터 프로퍼티

	get fullName(){ //접근자 함수로 구성된 접근자 프로퍼티
		return `${this.fistName} ${this.lastName}`;
	},
	set fullName(){
		[this.firstName, this.lastName] = name.split(' ');
	}
};
consoloe.log(person.fullName); //Heegun Lee
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor); // {get: f, set: f, enumerable: true, configurable: true}

person 객체에서 데이터 프로퍼티와 접근자 프로퍼티를 구별해보자

  • firstName, lastName - 데이터 프로퍼티
  • fullName - 접근자 프로퍼티

코드를 통해 이 둘을 구별하는 방법도 있다.

Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
//{enumerable: false, configurable: true, get: ƒ, set: ƒ}
Object.getOwnPropertyDescriptor(function() {}, 'prototype');
//{value: {…}, writable: true, enumerable: false, configurable: false}

__가 붙으면 접근자 프로퍼티라고 알아두면 된다.


[04] 프로퍼티 정의

Object.defineProperty 메서드를 사용하면 프로퍼티 어트리뷰트를 정의할 수 있다. 인수는 객체의 참조(person)와 데이터 프로퍼티 키인 문자열(’firstName’프로퍼티 디스크립터 객체({value:… , writable: true, …})를 전달한다.

데이터 프로퍼티 정의

const person = {};

Object.defineProperty(person, 'firstName', {
	value: 'Ungmo',
	writable: true,
	enumerable: true,
	configurable: true
});
Object.defineProperty(person, 'lastName', {
	value: 'Lee'
});

console.log(Object.getOwnPropertyDescriptor(person,'firstName'))
//{value: 'Ungmo', writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(person,'lastName'))
//{value: 'Lee', writable: false, enumerable: false, configurable: false}

lastName에는 디스크립터 객체의 프로퍼티를 누락했기 때문에 false가 기본값으로 설정된다.

🥕 [[Enmerable]] false일 때 열거 불가능 - Object.keys()

console.log(Object.keys(person)); // ["firstName"]
// lastName은 enumerable이 누락되어 false이므로 열거되지 않는다.

🥕 [[Writable]] false일 때 값 변경 불가능

person.lastName = 'Kim' // 값 변경 불가, 오류는 안나고 무시된다.

🥕 [[Configurable]] 값 false일 때 재정의 불가능

delete person.lastName; // 무시된다.

접근자 프로퍼티 정의

Object.defineProperty(person, 'fullName',{
	get(){
		return `${this.firstName} ${this.lastName}`;
	},
	set(name){
		[this.firstName, this.lastName] = name.split(' ');
	},
	enumerable: true,
	configurable: true
});
console.log(Object.getOwnPropertyDescriptor(person,'fullName'))
//{enumerable: true, configurable: true, get: ƒ, set: ƒ}

console.log(person.fullName); // Ungmo Lee
person.fullName = 'Heegun Lee';
console.log(person); //{firstName: "Heegun", lastName: "Lee"}

person.fullName = 'Heegun Lee'; 로 값을 변경했더니 setter 함수를 통해 firstName, lastName의 이름이 바뀐 것을 확인할 수 있다.

[05] 객체 변경 방지

Object.defineProperty를 이용하여 프로퍼티 어트리뷰트를 재정의하는 것을 배웠다.

자바스크립트는 금지 강도에 따라 객체의 변경을 방지하는 다양한 메서드를 제공하기도 한다.

객체 확장 금지

const person = { name: 'Lee' };

// 객체의 확장을 금지시킴
Object.preventExtensions(person);

// isExtensible: 객체가 확장이 금지되었는지 확인
console.log(Object.isExtensible(person));   // false

// 프로퍼티 추가 금지 -> 확장이 무시된다
person.age = 25;
console.log(person); // {name: "Lee"}

//프로퍼티 삭제는 가능
delete person.name;
console.log(person); //{}

//프로퍼티 정의에 의한 프로퍼티 추가도 금지
Object.defineProperty(person, 'age', {value: 20});

위 코드에서 알 수 있듯이, Object.preventExtensions 메서드는 객체의 확장을 금지하고 확장이 금지된 객체는 프로퍼티 추가가 금지된다.

객체 밀봉

const person = { name: 'Lee' };

// 객체 밀봉시킴
Object.seal(person);

// isSealed: 객체가 밀봉되었는지 확인
console.log(Object.isSealed(person));   // true

// 추가와 삭제가 금지됨
person.age = 25;
delete person.name;
console.log(person);  // {name: "Lee"}

//프로퍼티 값 갱신은 가능
person.name = 'Kim';
console.log(person); // {name: "Kim"}

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

Object.seal 메서드는 프로퍼티 추가, 삭제, 재정의를 금지한다. 밀봉하면 읽기와 쓰기만 가능하다.

객체 동결

const person = { name: 'Lee' };

// 객체 동결시킴
Object.freeze(person);

// isFrozen: 동결된 객체인지 확인
console.log(Object.isFrozen(person));   // true

// 프로퍼티 추가, 삭제, 변경 금지
person.age = 25;
delete person.name;
person.name = 'Kim';
console.log(person);    //{name: 'Lee'}  
//프로퍼티 어트리뷰트 재정의도 금지
Object.defineProperty(person, 'name', {configurable: true});
//TypeError: Cannot redefine property: name

Object.freeze 메서드는 프로퍼티의 추가, 삭제, 변경을 금지한다.

동결된 객체는 읽기만 가능하다

불변 객체

위의 3가지 방법 모두 중첩된 객체까지 변경이 방지되지는 않는다. 불변된 객체를 만드는 방법은 재귀적으로 Object.freeze 메서드를 호출하는 것이다.

🥕 얕은 복사

const person = {
    name: 'Lee',
    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: "Lee", address: {city: "Busan"}]

🥕 깊은 복사(불변 객체 만들기)


//깊은 동결을 해보자.
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'}
};

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

console.log(Object.isFrozen(person)); // true.

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

person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Seoul"}}

댓글남기기