개발자 연쨘

[딥 다이브] 프로퍼티 어트리뷰트 본문

스터디/모던 자바스크립트 Deep Dive

[딥 다이브] 프로퍼티 어트리뷰트

연쨘 2024. 3. 21. 18:23
 

 


 

내부 슬롯과 내부 메서드 🐰

 

프로퍼티 어트리뷰트를 이해하기 위해 먼저 내부 슬롯과

내부 메서드의 개념에 대해 알아보자!

 

내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을

설명하기 위해 ECMAScript 사양에서 사용하는

의사 프로퍼티와 의사 메서드이다.

 

ECMAScript 사양에 등장하는 이중 대괄호 ([[...]])로 감싼 이름들이

내부 슬롯과 내부 메서드이다.

내부 슬롯과 내부 메서드

 

내부 슬롯과 내부 메서드는 ECAScript 사양에 정의된 대로 구현되어

자바스크립트 엔진에서 실제로 동작 하지만 개발자가 접근할 수 있도록

외부로 공개된 객체의 프로퍼티는 ❌!

 

즉, 내부 슬롯과 내부 메서드는 자바스크립트 엔진의 내부 로직이므로

원칙적으로 자바스크립트는 내부 슬롯과 내부 메서드에

직접적으로 접근하거나 호출할 수 있는 방법 제공

 

단, 일부 내부 슬롯과 내부 메서드에 한하여

간접적으로 접근할 수 있는 수단을 제공하기는 한다.

 

예를 들어, 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다.

내부 슬롯은 자바스크립트 엔진의 내부로직이므로

원칙적으로 직접 접근할 수 없지만

[[Prototype]] 내부 슬롯의 경우,

  __proto__를 통해 간접적으로 접근 가능!

 

const o = {};

// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근할 수 없다.
o.[[Prototype]] // -> Uncaught SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기는 한다.
o.__proto__ // -> Object.prototype

 

 

 

 

참고로 Object.prototype.__proto__는 피하자!

mdn 사이트에 나온 내용을 보면 ▼

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

 

Object.prototype.__proto__ - JavaScript | MDN

주의: 객체의 [[Prototype]]을 변경하는 것은 최신 JavaScript 엔진이 속성 접근을 최적화하는 방식의 특성상 모든 브라우저 및 JavaScript 엔진에서 매우 느린 작업입니다. 상속 구조를 변경하는 것이 성

developer.mozilla.org

 

이기능을 없애려고 한다고 한다!
사용을 피하는게 좋을것 같다.

 

🐯💬 : 간단히 이야기를 내방식으로 풀어보자면,

ES6에서 도입되기 이전에 browser들이 맘대로 인스턴스에 접근하려는

이 수단 즉, o. __proto__를 지원하고 있었다. (원래는 비표준)

 

browser들이 이미 맘대로 사용하고 있어서 ES6에서

이미 다 사용하고 있으면 어쩔 수 없지 표준으로 해줄게 하면서 해줌.

현재 표준이긴 하지만 없앨 예정이다라고 알아두면 될 것 같다.

 

 

Object.getPrototypeOf(o) 사용하자.

 

그래서 이거 말고

Object.getPrototypeOf(o)를 사용하면 똑같이 접근되니까 이걸 사용하자!


 

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

 

자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는

프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

 

프로퍼티의 상태란 프로퍼티의 값, 값의 갱신 가능 여부, 열거 가능 여부,

재정의 가능 여부를 말한다.

 

프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값인

내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]이다.

따라서 프로퍼티 어트리뷰트에 직접 접근할 수 없지만

Object. gerOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인

const person = {
  name: 'Lee'
};

// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

 

Object. gerOwnPropertyDescriptor 메서드를 호출할 때 첫 번째

매개변수에는 객체의 참조를 전달하고,

두 번째 매개변수에는 프로퍼티 키를 문자열로 전달한다.

 

이때 Object. gerOwnPropertyDescriptor  메서드는 프로퍼티 어트리뷰트

정보를 제공하는 프로퍼티 디스크립터객체를 반환한다.

 

만약 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한

프로퍼티 디스크립터를 요구하면 undefined가 반환된다.

 

Object. gerOwnPropertyDescriptor 메서드는 하나의 프로퍼티에 대해

프로퍼티 디스크립터 객체를 반환하지만 ES8에서 도입된

Object. gerOwnPropertyDescriptor 메서드는 모든 프로퍼티의

프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환!

const person = {
  name: 'Lee'
};

// 프로퍼티 동적 생성
person.age = 20;

// 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
  name: {value: "Lee", writable: true, enumerable: true, configurable: true},
  age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/

 


 

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

 

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

  • 데이터 프로퍼티 :
    키와 값으로 구성된 일반적인 프로퍼티
    지금까지 살펴본 모든 프로퍼티는 데이터 프로퍼티!
  • 접근자 프로퍼티 :
    자체적으로 값을 갖진 않고 다른 데이터 프로퍼티의 값을
    읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티!

 

 

데이터 프로퍼티

 

데이터 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다.

이 프로퍼티 어트리뷰트는 자바스크립트 엔진이

프로퍼티를 생성할 때 기본값으로 자동 정의된다.

 

프로퍼티 
어트리뷰트
프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Value]] value 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값

● 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]]에 값을 재할당한다.
이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고
생성된 프로퍼티 [[Value]]에 값을 저장한다.

[[Writable]] writable  프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다.

[[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을
변경할 수 없는 읽기 전용 프로퍼티가 된다.
[[Enumerable]] enumerable  프로퍼티의 열거 기능 여부를 나타내며 불리언 값을 갖는다.

 [[Enumerable]]의 값이 false인 경우 해당 프로퍼티의 for...in문이나
Object.keys 메서드 등으로 열거 할 수 ❌
[[Configurable]] configurable  프로퍼티 재정의 가능 여부를 나타내며 불리언 값을 갖는다.

 [[Configurable]의 값이 false인 경우 해당 프로퍼티의 삭제,
프로퍼티 어트리뷰트 값이 변경이 금지된다.
단, [[Writable]]을 false로 변경하는 것은 허용된다.

 

 

다음 예제를 살펴보자.

const person = {
  name: 'Lee'
};

// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 취득한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

 

Object. gerOwnPropertyDescriptor 메서드가 반환한

프로퍼티 디스크립터 객체를 살펴보면 value 프로퍼티의 값은 'Lee' 다.

이것은 프로퍼티 어트리뷰트 [[Value]]의 값이 'Lee'인 것을 의미한다.

 

그리고 writable, enumerable, configurable 프로퍼티의 값은 모두 true이다.

이것은 프로퍼티 어트리뷰트 [[Writable]], [[Enumerable]], [[Configurable]]의 

값이 모두 true인 것을 의미한다.

 

이처럼 프로퍼티가 생성될 때 [[Value]]의 값은 프로퍼티 값으로 초기화되며

[[Writable]], [[Enumerable]], [[Configurable]]의 값은 true로 초기화된다.

이것은 프로퍼티를 동적 추가해도 마찬가지이다.

 

const person = {
  name: 'Lee'
};

// 프로퍼티 동적 생성
person.age = 20;

console.log(Object.getOwnPropertyDescriptors(person));
/*
{
  name: {value: "Lee", writable: true, enumerable: true, configurable: true},
  age: {value: 20, writable: true, enumerable: true, configurable: true}
}
*/

 

 

 

접근자 프로퍼티

 

접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을

읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티이다.

접근자 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다.

 

프로퍼티 
어트리뷰트
프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Get]] get 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때
호출되는 접근자 함수이다.

즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면
프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고
그 결과가 프로퍼티 값으로 반환된다.

[[Set]] set 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때
호출되는 접근자 함수이다.

즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면
프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고
그 결과가 프로퍼티 값으로 저장된다.
[[Enumerable]] enumerable 데이터 프로퍼티의 [[Enumerable]]과 같다.



 프로퍼티의 열거 기능 여부를 나타내며 불리언 값을 갖는다.

 [[Enumerable]]의 값이 false인 경우 해당 프로퍼티의 for...in문이나
Object.keys 메서드 등으로 열거 할 수 ❌ )

[[Configurable]] configurable   데이터 프로퍼티의 [[Configurable]]과 같다.



 프로퍼티 재정의 가능 여부를 나타내며 불리언 값을 갖는다.

 [[Configurable]의 값이 false인 경우 해당 프로퍼티의 삭제,
프로퍼티 어트리뷰트 값이 변경이 금지된다.
단, [[Writable]]을 false로 변경하는 것은 허용된다.

 

 

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

접근자 프로퍼티는 getter와 setter 함수를 모두 정의할 수도 있고

하나만 정의할 수도 있다.

 

다음 예제를 살펴보자!

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

  // fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set fullName(name) {
    // 배열 디스트럭처링 할당: "31.1 배열 디스트럭처링 할당" 참고
    [this.firstName, this.lastName] = name.split(' ');
  }
};

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

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

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

// firstName은 데이터 프로퍼티다.
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
// {value: "Heegun", writable: true, enumerable: true, configurable: true}

// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}

 

person 객체의 firstName과 lastName 프로퍼티는 일반적인 데이터 프로퍼티다.

메소드 앞에 get, set이 붙은 메서드가 있는데 이것들이 바로 getter와 setter 함수이고,

getter/setter 함수의 이름 fullName이 접근자 프로퍼티이다.

 

접근자 프로퍼티는 자체적으로 값(프로퍼티 어트리뷰트 [[Value]])을 가지지 않으며

다만 데이터 프로퍼티의 값을 읽거나 저장할 때 관여할 뿐이다.

 

이를 내부 슬롯/메서드 관점에서 설명하면 다음과 같다.

접근자 프로퍼티 fullName으로 프로퍼티 값에 접근 하면

내부적으로 [[Get]] 내부 메서드가 호출되어 다음과 같이 동작한다.

  1. 프로퍼티 키가 유효한지 확인한다.
    프로퍼티 키는 문자열 또는 심벌이어야 한다.
    프로퍼티 키 "fullName"은 문자열이므로 유효한 프로퍼티 키다.
  2. 프로토타입 체인에서 프로퍼티를 검색한다.
    person 객체에 fullName 프로퍼티가 존재한다.
  3. 검색된 fullName 프로퍼티가 데이터 프로퍼티인지
    접근자 프로퍼인지 확인한다. fullName 프로퍼티는 접근자 프로퍼티이다.
  4. 접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]의 값,
    즉 getter 함수를 호출하여 그 결과를 반환한다.
    프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]의 값은
    Object. gerOwnPropertyDescriptor 메서드가 반환하는
    프로퍼티 디스크립터 객체의 get 프로퍼티 값과 같다.

 

 

실제로는 person.fullName이라는 프로퍼티는 없음!

 

저걸( fullName ) 클릭하는 순간  getter가 발동이됌!

그래서 그때 'yeon jin'이 나오게됌!

 

 

프로토타입

프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체이다.
프로토타입은 하위(자식) 객체에게 자신의 프로퍼티와 메서드를 상속한다.
프로토타입 객체의 프로퍼티나 메서드를 상속받은 하위 객체는
자신의 프로퍼티 또는 메서드인 것처럼 자유롭게 사용할 수 있다!


프로토타입 체인은 프로토타입이 단방향

링크드 리스트 형태로 연결되어 있는 상속 구조를 말한다.

객체의 프로퍼티나 메서드에 접근하려고 할 때 
해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면
프로토타입 체인에 따라 프로토타입의 프로퍼티나 메서드를 차례대로 검색한다.
프로토타입과 프로토타입 체인에 대해서는 "프로토타입"파트에서 자세히 살펴볼 예정!

 

 

 

접근자 프로퍼티와 데이터 프로퍼티를 구별하는 방법은 다음과 같다.

// 일반 객체의 __proto__는 접근자 프로퍼티다.
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
//getter/setter로 접근

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

 

Object.getOwnPropertyDescriptor 메서드가 반환한 프로퍼티 어트리뷰트를

객체로 표현한 프로퍼티 디스크립터 객체를 유심히 살펴보자.

접근자 프로퍼티와 데이터 프로퍼티의 프로퍼티 디스크립터 객체의
프로퍼티가 다른 것을 알 수 있다.

 


 

프로퍼티 정의 🐰

 

프로퍼티 정의란 새로운 프로퍼티를 추가하면서

  • 프로퍼티 어트리뷰트를 명시적으로 정의하거나,
  • 기존 프로퍼티의 프로퍼티 어트리뷰트를
    재정의하는 것을 말한다. 예를 들어,
    👉🏻 프로퍼티 값을 갱신 가능하도록 할 것인지,
    👉🏻 프로퍼티를 열거 가능하도록 할 것인지,
    👉🏻 프로퍼티를 재정의 가능하도록 할 것인지 정의할 수 있다.

이를 통해 객체의 프로퍼티가 어떻게 동작해야 하는지 명확히 정의 할 수 있다.

 

Object.defineProperty 메서드를 사용하면 프로퍼티의 어트리뷰트를 정의할 수 있다.

인수로는 객체의 참조와 데이터 프로퍼티의 키인 문자열,

프로퍼티 디스크립터 객체를 전달한다.

const person = {};

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

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

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

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

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

// [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없다.
// lastName 프로퍼티는 [[Writable]]의 값이 false이므로 값을 변경할 수 없다.
// 이때 값을 변경하면 에러는 발생하지 않고 무시된다.
person.lastName = 'Kim';

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

// [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 재정의할 수 없다.
// Object.defineProperty(person, 'lastName', { enumerable: true });
// Uncaught TypeError: Cannot redefine property: lastName

descriptor = Object.getOwnPropertyDescriptor(person, 'lastName');
console.log('lastName', descriptor);
// lastName {value: "Lee", 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: ƒ, set: ƒ, enumerable: true, configurable: true}

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

 

 

정의할 때 value값 주고, 나머지들 다 true로 줬을 때

 

 

value값도 있고 다 true로 줘서 다 값이 나온다! 다 가능!

 

 

 

정의할 때 value값만 줬을 때

 

🐯💬: 그렇다면 value값만 줬을땐 어떻게 될까?

 

value값만 있고 다 false로 되있다.
그 상태에서 이름을 바꿀려하면 에러는 안뜨지만 값이 바뀌지 않는다.

 

 

 

삭제도 되지않고, 확인해보면 원래 있는 value값이 나온다.

 

 

 

for in문도 아무것도 안나온다!
value값만 주고 enumerable이 false이기 때문이다.

 

그렇다면 for in문이 enumerable: true인건 무슨 값이 나오는걸까? 🐰

 

위에서 정의한 firstName

 

 

firstName을 abc로 바꾸고! (이것도 true라서 변경 가능한것)

enumerable이 true이기 때문에 for in 써보면 값이 나온다. 

 

 

 

정의할 때 프로퍼티 일부 생략 가능

 

Object.defineProperty 메서드로 프로퍼티를 정의할 때

프로퍼티 디스크립터 객체의 프로퍼티를 일부 생략 할 수 있다.

 

프로퍼티 디스크립터 객체에서 생략된

어트리뷰트는 다음과 같이 기본값이 적용된다.

 

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

 

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

 

✔️ Object.defineProperties 메서드를 사용하면
여러 개의 프로퍼티를 한 번에 정의할 수 있다.

👉🏻 Object.defineProperties는 객체에 여러 속성을

추가하거나 수정하는 데 사용

즉, 객체를 조작하는데 사용

const person = {};

Object.defineProperties(person, {
  // 데이터 프로퍼티 정의
  firstName: {
    value: 'Ungmo',
    writable: true,
    enumerable: true,
    configurable: true
  },
  lastName: {
    value: 'Lee',
    writable: true,
    enumerable: true,
    configurable: true
  },
  // 접근자 프로퍼티 정의
  fullName: {
    // getter 함수
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    // setter 함수
    set(name) {
      [this.firstName, this.lastName] = name.split(' ');
    },
    enumerable: true,
    configurable: true
  }
});

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

 

 

전체를 다가져온다!

👉🏻 Object.getOwnPropertyDescriptors는 객체의 모든 자신의 속성에 대한

상세 정보를 포함하는 객체를 반환하는 데 사용된다.
즉, 객체의 현재 상태를 분석하는데 사용

 

 

이걸로 객체의 완전 깊은 복사는 아니고

그 전 단계인 깊은 복사 할 수 있다!

 

a라는 변수에 값 넣고 결과 보면

복사된걸 확인할 수 있다.

 

 

하지만 a를 바꿔도 person은 바뀌지 않는다!
만약 얕은 복사였다면 바뀌었을것이다.

 

그리고 이 방법이getter, setter도 복사가 되니까 좋은것 같긴하다.

 

 

복사하는 위 코드에 대한 설명이 궁금하다면 GPT가 알려준 내용 참고!

▼ 더보기 클릭 ▼

더보기

 

이 코드는 Object.defineProperties와 Object.getOwnPropertyDescriptors를 함께 사용하여

person 객체의 모든 속성(값, 쓰기 가능 여부, 열거 가능 여부, 설정 가능 여부 등을 포함)을

새로운 객체에 복사하는 과정을 나타냅니다.

여기서 사용된 구문과 기호들을 하나씩 살펴보겠습니다.

 

 

 

코드 분석

 

const a = Object.defineProperties({}, Object.getOwnPropertyDescriptors(person));
  • Object.defineProperties(target, props):
    이 메서드는 첫 번째 인자로 주어진 target 객체에
    두 번째 인자 props에 지정된 속성들을 추가하거나
    수정합니다. 여기서 props는 속성명을 키로,
    속성 설명자(descriptor)를 값으로 하는 객체입니다.
  • Object.getOwnPropertyDescriptors(obj):
    이 메서드는 인자로 주어진 객체 obj의
    모든 자신의 속성(즉, 해당 객체에 직접 정의된 속성)에 대한 속성 설명자를
    포함하는 새로운 객체를 반환합니다.

 

사용된 기호와 문구

  • {}:
    빈 객체 리터럴입니다. 이 코드에서는
    Object.defineProperties의 첫 번째 인자로 전달되어,
    새로운 속성이 이 빈 객체에 추가됩니다.
  • ():
    함수 호출을 나타냅니다.
    Object.defineProperties와 Object.getOwnPropertyDescriptors 모두 함수이며,
    각각의 괄호 안에 그 함수의 인자들이 위치합니다.
  • ,:
    콤마는 Object.defineProperties 함수의 인자들을 구분짓습니다.
    첫 번째 인자는 새 속성을 추가할 대상 객체이고,
    두 번째 인자는 추가할 속성들의 설명이 담긴 객체입니다.

 

작동 원리

  1. Object.getOwnPropertyDescriptors(person):
    person 객체의 모든 속성 설명자들을 가져옵니다.
    이 설명자들에는 해당 속성의 값, 열거 가능 여부, 쓰기 가능 여부,
    설정 가능 여부 등의 정보가 포함됩니다.
  2. Object.defineProperties({}, ...):
    첫 번째 인자로 주어진 빈 객체 {}에,
    두 번째 인자로 주어진 속성 설명자들(person 객체의 속성들)을 사용하여
    새 속성들을 추가하거나 수정합니다.
  3. 결과적으로, a 변수는 person 객체의 모든 속성을 깊은 복사한 새로운 객체를 참조하게 됩니다.
    여기서 "깊은 복사"라는 표현은 속성의 값 뿐만 아니라,
    그 속성의 특성(열거 가능 여부 등)까지도 복사된다는 의미입니다.

이 방식을 사용함으로써, 객체의 모든 속성과 그 특성들을 정확하게 복사하여
새 객체를 생성할 수 있습니다.
이는 단순히 속성의 값만 복사하는 얕은 복사(shallow copy)와는 다른 개념입니다.

 


 

객체 변경 방지 🐰 -발표

 

객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다.

즉, 프로퍼티를 추가하거나 삭제할 수 있고,

프로퍼티 값을 갱신할 수 있으며,

 

Object.defineProperty 또는 Object.defineProperties 메서드를 사용하여

프로퍼티 어트리뷰트를 재정의할 수도 있다.

 

자바스크립트는 객체의 변경을 방지하는 다양한 메서드를 제공한다.

객체 변경 방지 메서드들은 객체의 변경을 금지하는 강도가 다르다.

 

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

 

 

 

객체 확장 금지

 

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

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

 

즉, 확장이 금지된 객체는 프로퍼티 추가가 금지된다.

프로퍼티는 프로퍼티 동적 추가와

Object.defineProperty 메서드로 추가할 수 있다.

하지만 여기선 이 두 가지 추가 방법 모두 ❌ !!

 

 

확장이 가능한 객체인지 여부는 Object.isExtensible 메서드로 확인 가능!

const person = { name: 'Lee' };

// person 객체는 확장이 금지된 객체가 아니다.
console.log(Object.isExtensible(person)); // true

// person 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.
Object.preventExtensions(person);

// person 객체는 확장이 금지된 객체다.
console.log(Object.isExtensible(person)); // false

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

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

// 프로퍼티 정의에 의한 프로퍼티 추가도 금지된다.
Object.defineProperty(person, 'age', { value: 20 });
// TypeError: Cannot define property age, object is not extensible

 

 

👉🏻 person을 Object.isExtensible로 확인해보면

처음에 확장이 가능한 객체인걸 확인할 수 있다.

 

 

 

 

👉🏻 확장 가능했던 person을 Object.preventExtensions

확장을 금지한다. 현재 person에 있는 값은 { name: 'Lee' } 하나이다.

 

 

 

👉🏻 키 age  값 20 추가해도 확장을 금지시켰기 때문에

console.log해보면 추가한 내용이 아닌

원래 갖고 있던 { name: 'Lee' }만 갖고 있다!

엄격한 모드(strict mode)에서는 에러를 내지만

엄격한 모드가 아니라서 여기서는 에러는 내지 않는다.

 

 

 

 

👉🏻  프로퍼티 값 갱신도 가능!

name값이 Lee에서 Yeon으로 변경됌.

 

 

 

 

👉🏻  프로퍼티 추가는 금지 되지만 삭제는 가능하다.

person.name을 삭제하니 위에 갱신해서 바뀐 { name: 'Yeon' } 이게

없어지고 빈 객체만 남아 있다.

 

 

 

 

👉🏻  프로퍼티 정의에 의한 프로퍼티 추가도 금지된다.

오류 내용을 확인해 보면

"객체에 새로운 속성을 추가하려고 시도했으나,

해당 객체가 확장 불가능한 상태(extensible state)" 라고 써져있다.

 

확장 불가능한 상태에서 Object.defineProperty() 함수를

사용하여 객체에 새로운 속성을 정의하거나 기존 속성을 수정하려고

시도했을 때 오류가 발생한다.

 

즉, 객체가 확장 불가능한 상태이기 때문에

Object.defineProperty() 함수를 통해 속성을 추가하거나

변경할 수 없다.

 

 

 

 

객체 밀봉

 

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

객체 밀봉이란 프로퍼티 추가 및 삭제와

프로퍼티 어트리뷰트 재정의 금지를 의미한다.

즉, 밀봉된 객체는 읽기와 쓰기만 가능하다.

 

밀봉된 객체인지 여부는 Object.isSealed 메서드로 확인할 수 있다.

 

const person = { name: 'Lee' };

// person 객체는 밀봉(seal)된 객체가 아니다.
console.log(Object.isSealed(person)); // false

// person 객체를 밀봉(seal)하여 프로퍼티 추가, 삭제, 재정의를 금지한다.
Object.seal(person);

// person 객체는 밀봉(seal)된 객체다.
console.log(Object.isSealed(person)); // true

// 밀봉(seal)된 객체는 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
  name: {value: "Lee", writable: true, enumerable: true, configurable: false},
}
*/

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

// 프로퍼티 삭제가 금지된다.
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

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

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

 

 

👉🏻  person을 Object.isSealed로 확인해보면

처음에 밀봉된 객체가 아닌걸 확인할 수 있다.

 

 

 

👉🏻  person을 밀봉 시키고, 밀봉된 객체냐고 물어보면 true

 

 

 

 

👉🏻  밀봉된 객체는 재정의부분이 false로 되어있다.

 

 

 

 

👉🏻   밀봉한 상태라서 프로퍼티 추가가 금지 되었기 때문에

추가된 내용이 무시되지만, strict mode가 아니라서 에러가 따로 뜨지 않는다.

추가가 금지 되어서 원래 있던 {name : 'Lee'}만 있다.

 

 

 

 

👉🏻  밀봉한 상태에서 프로퍼티 삭제가 금지된다.

strict mode가 아니라서

이 내용은 에러는 안뜨고 무시가 된다.

해당 삭제 내용이 무시 되었기 때문에

원래 있던 {name : 'Lee'}가 있다.

 

 

 

👉🏻  프로퍼티 값 갱신 가능!

name 값이 Lee에서 Kim으로  변경됌.

 

 

 

 

👉🏻 밀봉된 객체는 프로퍼티 속성을 재정의하는게 금지되어 에러가 뜬다!  

 

오류내용을 확인 해보면

'name' 속성을 재정의하려고 했으나,

해당 속성이 재정의 불가능한 상태(configurable: false)

설정되어 있어서 발생한 오류

JavaScript에서 Object.defineProperty() 함수는 객체의 속성에 대한

메타데이터를 정의하거나 수정할 때 사용된다.

이 함수를 사용하여 속성의 값(value), 값의 쓰기 가능 여부(writable),

열거 가능 여부(enumerable), 그리고 재정의 가능 여부(configurable)를 설정할 수 있다.

속성의 'configurable' 속성이 false로 설정되어 있다면, 

그 속성은 재정의할 수 없다.

 

즉, 해당 속성을 삭제하거나, 'configurable'을 true로 변경하거나,

'enumerable'을 변경하거나, 'writable'이 false에서 true로 변경하는 것이 불가능하다.

단, 'writable'이 true인 경우에는 'value'를 변경할 수 있다.

 

 

 

 

객체 동결

 

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

객체 동결이란 프로퍼티 추가 및 삭제와

프로퍼티 어트리뷰트 재정의 금지,

프로퍼티 값 갱신 금지를 의미한다.

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

 

동결된 객체인지 여부는 Object.isFrozen 메서드로 확인할 수 있다.

 

const person = { name: 'Lee' };

// person 객체는 동결(freeze)된 객체가 아니다.
console.log(Object.isFrozen(person)); // false

// person 객체를 동결(freeze)하여 프로퍼티 추가, 삭제, 재정의, 쓰기를 금지한다.
Object.freeze(person);

// person 객체는 동결(freeze)된 객체다.
console.log(Object.isFrozen(person)); // true

// 동결(freeze)된 객체는 writable과 configurable이 false다.
console.log(Object.getOwnPropertyDescriptors(person));
/*
{
  name: {value: "Lee", writable: false, enumerable: true, configurable: false},
}
*/

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

// 프로퍼티 삭제가 금지된다.
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

// 프로퍼티 값 갱신이 금지된다.
person.name = 'Kim'; // 무시. strict mode에서는 에러
console.log(person); // {name: "Lee"}

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

 

 

 

👉🏻 person을 Object.isFrozen로 확인해보면

처음에 동결 객체가 아닌걸 확인할 수 있다.

 

 

 

 

👉🏻 person을 동결 객체로 만들고, 동결 객체냐고 물어보니 true라고 대답!

 

 

 

 

👉🏻 동결된 객제는 값의 쓰기 기능과 재정의 기능이 false로 되어있다. 

 

 

 

 

👉🏻 동결된 객체는 프로퍼티 추가가 금지된다!

그래서 추가하면 그 내용이 무시된다.

strict mode가 아니라서 에러는 뜨지 않는다.

해당 추가내용이 무시되었기 때문에

원래 있던 {name : 'Lee'}만 있다.

 

 

 

 

👉🏻 동결된 객체는 프로퍼티 삭제가 금지된다!

그래서 삭제를 하면 그 내용이 무시된다.

strict mode가 아니라서 에러는 뜨지 않는다.

해당 삭제내용이 무시되었기 때문에

원래 있던 {name : 'Lee'}가 삭제되지 않고 그대로 있다.

 

 

 

👉🏻 동결된 객체는 프로퍼티 값 갱신이 금지된다!

그래서 갱신을 하면 그 내용이 무시된다.

strict mode가 아니라서 에러는 뜨지 않는다.

해당 갱신내용이 무시되었기 때문에

원래 있던 {name : 'Lee'}가 Kim으로

변경되지 않고 그대로 있다.

 

 

 

 

👉🏻  동결된 객체는 프로퍼티 속성 재정의가 금지된다!

 

에러 내용을 자세히 확인해보면,

위에서 봤듯이 이미 정의된 'name' 속성이 재정의 불가능한 상태

(configurable 속성이 false로 설정된 상태)임을 나타낸다.

이것도 위에서 말했듯이 다시 말해보자면,

JavaScript에서 Object.defineProperty() 함수는 객체의 속성을 정의하거나

기존 속성을 수정할 때 사용된다.

 

이 함수를 호출할 때, 객체의 특정 속성에 대해 설명자(descriptor) 객체를

제공하게 되는데, 이 설명자 객체 내에서 다양한 속성 플래그를

설정할 수 있다.

 

이 플래그들 중 'configurable' 플래그는 속성이 재정의 가능한지 여부를 결정하는데,

만약 'configurable'이 false로 설정되어 있다면, 그 속성은 재정의할 수 없으며,

이는 속성을 삭제하거나, 'configurable', 'enumerable', 'writable' 플래그를

변경할 수 없음을 의미한다.

 

(내용이 익숙해지기 위해 위해서 했던말 다시 하기...😀)

 

 

 

불변 객체

 

지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지로 직속 프로퍼티만

변경이 방지되고 중첩 객체까지는 영향을 주지는 못한다.

 

따라서 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"}}

 

즉, Object.freeze() 메소드는 객체를

얕게 동결하여 직속 프로퍼티만 변경 불가능하게 만든다.

중첩된 객체는 그대로 변경 가능하며, 이를 동결하려면

중첩된 객체에 대해서도 명시적으로 Object.freeze()를 호출해야한다.

(이러한 특성은 Javascript를 사용하여 불변성을 관리할 때 매우 중요!)

 

위에서 말했다 시피 객체의 중첩 객체까지 동결하여 변경이 불가능한

읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는게

Javascript를 사용하여 불변성을 관리할 때 매우 중요하다.

이렇게 중첩객체까지 동결하려면 모든 프로퍼티에 대해

재귀적으로 Object.freeze 메서드를 호출해야 한다!

 

 

function deepFreeze(target) {
  // 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않은 객체만 동결한다.
  if (target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
    /*
      모든 프로퍼티를 순회하며 재귀적으로 동결한다.
      Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환한다.
      ("19.15.2. Object.keys/values/entries 메서드" 참고)
      forEach 메서드는 배열을 순회하며 배열의 각 요소에 대하여 콜백 함수를 실행한다.
      ("27.9.2. Array.prototype.forEach" 참고)
    */
    Object.keys(target).forEach(key => deepFreeze(target[key]));
  }
  return target;
}

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

 


 

🐯💬: 위 예시 코드 나눠서 실행 방식 설명

 

function deepFreeze(target) {
  if (target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
    Object.keys(target).forEach(key => deepFreeze(target[key]));
  }
  return target;
}

 

인자로 받은 target이 객체이고

아직 동결되지 않았는지 확인하고 만약 이 조건들이 참이라면,

Object.freeze(target)을 호출하여 현개 객체를 동결한다.

 

그 다음 단계에서는 Object.keys(target)을 사용하여 객체의

모든 열거 가능한 프로퍼티 키를 배열로 가져온다.

 

이 배열을 forEach메서드로 순회하면서, 각 키에 해당하는 속성 값에 대해

deepFreeze 함수를 재귀적으로 호출한다.

 

 

이제 이 함수를 사용하여 앞서 정의한 person 객체를 깊게 동결!

const person = {
  name: 'Lee',
  address: { city: 'Seoul' }
};

deepFreeze(person);

 

deepFreeze 함수를 호출한 후, person 객체와 그 중첩된 address 객체 모두 동결되었는지

확인해보면 동결된걸 확인 할 수 있다!

 

 

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

 

person 객체와 그 내부의 address 객체 모두 동결되었기 때문에,

어떠한 속성도 변경할 수 없다!

 

실제로 person.address.city 를 'Busan'으로

변경하려고 시도해도 변경되지 않는다.

 

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

 

이것은 어플리케이션에서 불변성을 유지할 때 유용하다.

또, 객체의 상태를 예측 가능하게 유지하고 객체가 예상치 못하게

변경되지 않도록 보장하는것이 중요한데 이 깊은 동결 함수를 통해

이러한것을 하는데 도움을 준다!

728x90