[JS] 프로퍼티 어트리뷰트

_sqrlkoo·2023년 8월 27일
1
post-thumbnail

클래스를 공부하다 보니 명확하지 않은 개념들이 있어서 잠시 멈추고 부족한 부분으로 다시 돌아가려한다.

클래스를 공부하다보면 prototype, proto.. 등 생소한 용어들의 등장으로 JS 교재를 읽어내려가는데 어려움을 주었다ㅠ

그래서 prototype을 이해하려고 챕터를 돌아가니 이번에는 내부슬롯([[...]] 대충 이런모양..), 프로퍼티 어트리뷰트.. 이번에도 도저히 교재를 읽어내려갈 수 없었다.

그래서 오늘은 프로퍼티와 어트리뷰트에 대해 공부하려고 한다!

내부 슬롯과 내부 메서드

프로퍼티 어트리뷰트를 이해하기 위해 먼저 내부 슬롯과 내부 메서드의 개념을 알아야 된다.

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

이해하기 어려운 내용이지만 일단 ECMAScript 사양에서 등장하는 이중 대괄호 ([[...]] 이런모양..)로 감싼 이름들이 모두 내부 슬롯과 내부 메서드다.

내부 슬롯과 내부 메서드는 JS에서 실제로 동작하지만 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아니라고 한다.
즉, 내부 메서드와 내부 슬롯은 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않습니다.

이말을 쉽게 표현하자면! 다음과 같다.

const jangwoo = {};

jangwoo.[[Prototype]] // 이렇게 직접 접근이 불가능하다!

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

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

모든 객체는 [[Prototype]]을 갖고 있다고??? 무슨 말인지 이해가 어렵다면..(알고 계시다면 아래 나눠진 영역을 넘어가 읽어주시면 됩니다.)


이 부분은 필자가 개인적으로 알고 있는 지식을 기반으로 설명하는 내용입니다. 정확하지 않을 수 있지만 독자들의 이해를 돕기위해 작성된 내용이므로 너무 맹신하지 않고 가볍게 읽어주시길 바랍니다.

일단 F12를 눌러 객체를 확인해 봅시다!
콘솔에 window 라고 입력하면? -> 전역 객체를 보여줍니다.

그럼 전역 객체는 무엇인가? -> 전역 객체는 말 그대로 전역 객체이다. 객체 전체를 감싸고 있는 객체라고 생각하시면 됩니다.

이해가 어려워도 계속 읽어보세요! 이해가 될 겁니다!

JS는 모두 객체로 이루어져 있습니다.
예를들어 콘솔에 window를 입력하면 다음과 같은 메서드, 객체, 함수, 변수 등등이 보여집니다.

정말 수많은 내용들이 보이네요..
잘보면 이중 우리가 흔히 알 수 있는 메서드도 보입니다!

예를들어 알파벳 순서대로 내려가다 보면 alert라는 메서드가 보이나요?

맞습니다 alert는 전역객체의 window.alert() 혹은 alert() 와 같습니다.

더 설명을 하고 싶지만.. 이 부분에 대해서는 나중에 따로 정리해서 올리겠습니다.

어쨌든 우리가 확인해야 될 내용은
사진안에 보면 각 매서드 혹은 객체 등 모든 객체는 내부에 [[Prototype]]이라는 녀석이 존재합니다.

어쨌든 그래서 모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖습니다!


본론으로 돌아와서 내부 슬롯은 JS엔진의 내부 로직이므로 원칙적으로 직접 접근할 수 없지만, [[Prototype]] 의 경우 __proto를 통해 간접적으로 접근할 수 있습니다.

const jangwoo = {};

jangwoo.[[Prototype]] // error

jangwoo.__proto__ // Object.prototype

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

JS 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의합니다.

프로퍼티의 상태란 프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)를 말합니다.

프로퍼티의 어트리뷰트는 JS 엔진이 관리하는 내부 상태 값인 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]입니다.

따라서 프로퍼티 어트리뷰트에 직접 접근할 수 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수는 있습니다.

const person = {
	name : "jangwoo"
};

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

디스크립터 메소드

  • 메서드를 호출할 때 첫 번째 매개변수는 객체의 참조를 전달한다.
  • 두 번째 매개변수에는 프로퍼티 키를 문자열로 전달한다.
  • 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면 undefined를 반환한다.

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

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

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

1. 데이터 프로퍼티

데이터 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다. 이 프로퍼티 어트리뷰트는 자바스크립트 엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의된다.

  1. [[Value]]
    • 프로퍼티 어트리뷰트 : [[Value]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : value
    • 설명
      • 프로퍼티 키를 통해 프로퍼티값에 접근하면 반환되는 값이다.
      • 프로퍼티 키를 통해 프로퍼티의 값을 변경하면 [[Value]]에 값을 재할당한다. 이때 프로퍼티가 없으면 프로퍼티를 동적으로 생겅하고 생성된 프로퍼티에 [[Value]]에 값을 저장한다.
  2. [[Writable]]
    • 프로퍼티 어트리뷰트 : [[Writable]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : writable
    • 설명
      • 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다.
      • [[Writable]]값이 false인 경우 해당 프로퍼티의 [[Value]] 의 값을 변경할 수 없는 읽기 전용 프로퍼티가 된다.
  3. [[Enumerable]]
    • 프로퍼티 어트리뷰트 : [[Enumerable]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : enumerable
    • 설명
      • 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다.
      • [[Enumerable]]값이 false인 경우 해당 프로퍼티는 for...in문이나 Object.key 메서드 등으로 열거할 수 없다.
  4. [[Configurable]]
    • 프로퍼티 어트리뷰트 : [[Configurable]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : configurable
    • 설명
      • 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값을 갖는다.
      • [[Configurable]]값이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트의 값의 변경이 금지된다.
      • 단, [[Writable]]이 true인 경우 [[Value]]의 변경과,[[Writable]] false로 변경하는 것은 허용된다.

예제

const person = {
	name : "jangwoo"
};

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

Object.getOwnPropertyDescriptor 메서드가 반환한 프로퍼티 디스크립터 객체를 살펴보면 value 프로퍼티의 값은 "jangwoo"이다.

이것은 프로퍼티의 어트리뷰트 [[Value]]의 값이 "jangwoo"인 것을 의미합니다. 그리고 [[Configurable]], [[Writable]],[[Enumerable]] 이 모두 true인 것을 의미합니다.
이처럼 프로퍼티가 생성되면 [[Value]] 값은 프로퍼티 값으로 초기화되며
[[Configurable]], [[Writable]],[[Enumerable]] 이 모두 true 로 초기화 됩니다.

2. 접근자 프로퍼티

접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티입니다.

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

  1. [[Get]]

    • 프로퍼티 어트리뷰트 : `[[Get]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : get
    • 설명
      • 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수입니다. 즉, 접근자 프로퍼티의 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값 즉, getter함수가 호출되고 그 결과 프로퍼티 값으로 반환됩니다.
  2. [[Set]]

    • 프로퍼티 어트리뷰트 : [[Set]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : set
    • 설명
      • 접근자 프로퍼티를 통해 데이터의 프로퍼티의 값을 지정할 때는 호출되는 접근자 함수입니다. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과 프로퍼티 값으로 저장됩니다.
  3. [[Enumerable]]

    • 프로퍼티 어트리뷰트 : [[Enumerable]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : enumerable
    • 설명
      • 데이터 프로퍼티의 [[Enumerable]] 과 같다.
  4. [[Configurable]]

    • 프로퍼티 어트리뷰트 : [[Configurable]]
    • 프로퍼티 디스크립터 객체의 프로퍼티 : configurable
    • 설명
      • 데이터 프로퍼티의 [[Configurable]] 과 같다.

예시

const person = {
	firstName : "jangwoo",
  	lastName : "Koo",
  
  	get fullName() {
		return `${this.firstName} ${this.lastName}`;
    },
  	set fullName() {
    	[this.firstName, this.lastName] = name.split("")
    }
};


console.log(person.firstName) // jangwoo
person.fullName = "justin koo"
console.log(person) // {fistName : "justin", lastName : "koo"}
console.log(person.fullName); // justin Koo

// firstName은 데이터 프로퍼티이다.
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
// 프로퍼티 어트리뷰트를 갖는다.

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


// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]]
// 프로퍼티 어트리뷰트는 갖는다.
descriptor= Object.getOwnPropertyDescriptor(person, "fullName");

//{get:f, set:f, 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.getOwnPropertyDescriptor 메서드가 반환하는 프로퍼티 디스크립터 객체의 get 프로퍼티 값과 같다.

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

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

Object.getOwnPropertyDescriptor(function() {}, "prototype")
// {value : {...}, writable: true, enumerable: true, configurable: true}

그럼 각 프로퍼티의 어트리뷰트를 변경하거나 정의할 수 있을까?

프로퍼티 정의

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

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

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

const person = {};

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

Object.defineProperty(person, "lastName", {
	value : "Koo",
});

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

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

// enumerable 값이 false인 경우
// 해당 프로퍼티는 반복문이나 Object.key로 열거할 수 없다.
//lastName 프로퍼티는 enumerable 값이 false이므로 열거할 수 없다.
console.log(Object.key(person)); // ["firstName"]

// writable 값의 false인 경우 프로퍼티의 value 값을 변경할 수 없다.
// lastName 프로퍼티는 false이므로 값을 변경할 수 없다.
// 이떄 값을 변경하면 에러는 발생하지 않고 무시된다.
person.lastName = "Kim"

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

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

let descriptor = Object.getOwnPropertyDescriptor(person, "lastName")
console.log("lastName", descriptor);
// {value : "Koo", writable: false, enumerable: false, configurable: false}

// 접근자 프로퍼티 정의
Object.defineProperty(person, "fullName", {
  	get() {
		return `${this.firstName} ${this.lastName}`;
    },
  	set(name) {
    	[this.firstName, this.lastName] = name.split("")
    },
  	enumerable : true,
  	configurable : true
});

descriptor = Object.getOwnProperty(person, "fullName");

console.log("fullName", descriptor);
// fullName {get:f, set:f, enumerable : true, configurable : true }

Object.getOwnProperty메서드는 한번에 하나의 프로퍼티만 정의 할 수 있지만 Object.getOwnProperties메서드를 사용하면 여러개의 프로퍼티를 한 번에 정의할 수 있다.

객체 변경 방지

객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있다. 즉, 프로퍼티를 추가하거나 삭제할 수 있고, 프로퍼티 값을 갱신할 수 있으며, 앞서 언급한 두 메서드를 이용해서 프로퍼티 어트리뷰트를 재정의 할 수 있다.

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

구분메서드프로퍼티 추가프로퍼티 삭제프로퍼티 값 읽기프로퍼티 값 쓰기프로퍼티 어트리뷰트 재정의
객체 확장 금지Object.preventExtensionsXOOOO
객체 밀봉Object.sealXXOOX
객체 동결Object.freezeXXOXX
// 각 매서드는 다음와 같이 확인하고 사용할 수 있다.

const person = {name : "jangwoo"}

console.log(Object.isExtensible(person)) // true or false
Object.preventExtensions(person) // 객체의 확장을 금지하여 프로퍼티 추가를 금지한다.


console.log(Object.isSealed(person)); // true or false
Object.seal(person) // 객체를 밀봉해 프로퍼티 추가, 삭제, 재정의를 금지한다.


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

0개의 댓글