코드노트

자바스크립트 프로퍼티 어트리뷰트(내부 슬롯, 내부 메서드) 본문

Code note/자바스크립트

자바스크립트 프로퍼티 어트리뷰트(내부 슬롯, 내부 메서드)

코드노트 2022. 12. 11. 21:44

 

 

- 내부슬롯, 내부 메서드는 ECMAScript 사양에 정의된 대로 구현되고 JS 엔진에서 동작한다. 그러나 개발자가 직접 접근할 수 없다.

 

- 내부 슬롯, 내부 메서드는 JS 엔진의 구현 알고리즘을 설명하기 위해 사용하는

의사 프로퍼티( pseudo property )의사 메서드( pseudo method ) 이다.  *JS 엔진의 내부 로직

 

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

 

* [[...]] 로 감싼 이름들이 내부 슬롯과 내부 메서드

 

ex) 모든 객체는 [[ Prototype ]]이라는 내부 슬롯을 갖는다.

 

- 내부 슬롯은 JS 엔진의 내부 로직이기 때문에 직접 접근할 수 없지만 [[ Prototype ]] 내부 슬롯의 경우,

__proto__를 통해 간접적으로 접근할 수 있다.


프로퍼티 어트리뷰트 ( 내부 슬롯 )

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

* 프로퍼티의 상태: 값( value ), 값의 갱신 가능 여부( writable ), 열거 가능 여부( enumerable ), 재정의 가능 여부( configurable )

[[ value ]], [[ writable ]], [[ enumerable ]], [[ configurable ]]

 

- 프로퍼티 어트리뷰트는 JS 엔진이 관리하는 내부 상태 값인 내부 슬롯이다. 직접 접근할 수 없다. Object.getOwnPropertyDescriptor 메소드를 사용하여 간접적으로 확인 가능

const traveler = {
  name : "곽튜브"
};

// 프로퍼티 디스크립터 객체를 반환
console.log(Object.getOwnPropertyDescriptor(traveler, "name"));
//{ value: '곽튜브', writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptors(traveler));
/*
{
  name: {
    value: '곽튜브',
    writable: true,
    enumerable: true,
    configurable: true
  }
}
*/

- 첫번째 매개변수에는 객체의 참조를 전달하고, 두번째 매개변수에는 프로퍼티 키를 문자열로 전달

- Object.getOwnPropertyDescriptor 메서드는 프로퍼티 디스크립터 객체를 반환

- 존재하지 않는 프로퍼티일 경우 undefined가 반환

 

- Object.getOwnPropertyDescriptors는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공


◆ 데이터 프로퍼티, 접근자 프로퍼티

 

- 프로퍼티는 데이터 프로퍼티와 접근자 프로퍼티로 구분 가능

▶ 데이터 프로퍼티(Data property)

- key, value로 구성된 프로퍼티

프로퍼티 어트리뷰트 프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Value]] value - 프로퍼티 키로 프로퍼티 값에 접근하면 반환되는 값
- 프로퍼티 키로 프로퍼티 값을 저장하면 [[Value]]값에 저장
- 프로퍼티가 없으면 생성, 생성된 프로퍼티의 [[value]]에 값을 저장
[[Writable]] Writable - 프로퍼티 값의 변경 가능 여부를 boolean값으로 가진다.
- [[Writable]]의 값이 false인 경우,
해당 프로퍼티의 [[Value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티
[[Enumerable]] Enumerable - 프로퍼티의 열거 가능 여부를 boolean값으로 가진다.
- [[Enumerable]]의 값이 false인 경우
해당 프로퍼티는 for...in, Object.keys 메서드 등으로 열거할 수 없다.
[[Configurable]] Configurable - 프로퍼티의 재정의 가능 여부를 나타내며, boolean값을 가진다.
- [[Configurable]]의 값이 false인 경우
해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지
- [[Writable]]이 true인 경우, [[value]], [[Writable]] 값을 false 변경하는것이 허용

const traveler = {
  name: "곽튜브",
};

console.log(Object.getOwnPropertyDescriptor(traveler, "name"));
// { value: '곽튜브', writable: true, enumerable: true, configurable: true }

const traveler = {
  name: "곽튜브",
};

traveler.newName = "빠니보틀";

console.log(Object.getOwnPropertyDescriptors(traveler));
/*
{
  name: {
    value: '곽튜브',
    writable: true,
    enumerable: true,
    configurable: true
  },
  newName: {
    value: '빠니보틀',
    writable: true,
    enumerable: true,
    configurable: true
  }
}
*/

- 메서드가 반환된 프로퍼티 디스크립터 객체를 보게 되면 알 수 있다.

- value프로퍼티의 값은 "곽튜브"

- writable, enumerable, configurable의 값은 모두 true이다.

- 값을 변경할 수 있고, for...in 및 Object.keys로 열거할 수 있다, 그리고 재정의도 가능하다.

* 프로퍼티가 생성이 되면 value값은 프로퍼티 값으로 초기화되고 그 외 프로퍼티 어트리뷰트 또한 true로 초기화 된다.

   -> 동적으로 추가 되어도 true로 초기화되는건 같다.

 


▶ 접근자 프로퍼티(Accessor property)

- 자체적으로 값을 가지고 있지 않다.

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

프로퍼티 어트리뷰트 프로퍼티 디스크립터
객체의 프로퍼티
설명
[[Get]] get - 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수
- [[Get]] 함수가 호출되고 그 결과를 프로퍼티 값으로 반환
[[Set]] set - 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 지정할 때 호출되는 접근자 함수
- [[Set]] 함수가 호출되고 그 결과를 프로퍼티 값으로 지정
[[Enumerable]] Enumerable - 프로퍼티의 열거 가능 여부를 boolean값으로 가진다.
- [[Enumerable]]의 값이 false인 경우
해당 프로퍼티는 for...in, Object.keys 메서드 등으로 열거할 수 없다.
[[Configurable]] Configurable - 프로퍼티의 재정의 가능 여부를 나타내며, boolean값을 가진다.
- [[Configurable]]의 값이 false인 경우
해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지
- [[Writable]]이 true인 경우, [[value]], [[Writable]] 값을 false 변경하는것이 허용

 

ex)

const traveler = {
  first: "곽튜브", // 데이터 프로퍼티
  second: "빠니보틀", // 데이터 프로퍼티

  // together은 접근자 함수로 구성된 접근자 프로퍼티
  get together() { // getter 함수
    return `${this.first} ${this.second}`;
  },
  set together(name) { // setter 함수
    [this.first, this.second] = name.split(" ");
  },
};

// 데이터 프로퍼티를 통해서 프로퍼티 값을 참조할 수 있다.
console.log(traveler.first + " " + traveler.second);

console.log(traveler.together); // 곽튜브 빠니보틀
console.log(traveler); // { first: '곽튜브', second: '빠니보틀', together: [Getter/Setter] }

// 접근자 프로퍼티를 통함 프로퍼티 값의 저장
traveler.together = "채코제 원지의하루"; // 접근자 프로퍼티 together()에 값을 저장하면 setter 함수가 호출

// 접근자 프로퍼티를 통함 프로퍼티 값의 참조 : 접근자 프로퍼티 together()에 접근하면 getter 함수가 호출
console.log(traveler.together); // 채코제 원지의하루

console.log(traveler); // { first: '채코제', second: '원지의하루', together: [Getter/Setter] }

// first는 데이터 프로퍼티로 프로퍼티 어트리뷰트 확인 가능
let descriptor = Object.getOwnPropertyDescriptor(traveler, "first");
console.log(descriptor);
// { value: '채코제', writable: true, enumerable: true, configurable: true }

// together는 접근자 프로퍼티로 프로퍼티 어트리뷰트 확인 가능
descriptor = Object.getOwnPropertyDescriptor(traveler, "together");
// { get: [Function: get together], set: [Function: set together], enumerable: true, configurable: true }
console.log(descriptor);

- get : 참조할때, set: 값을 할당할 때 호출이 된다.

- 데이터 프로퍼티, 접근자 프로퍼티를 구별할 수 있는 방법 Object.getOwnPropertyDescriptor

- 반환된 프로퍼티 디스크립터 객체를 보면 알 수 있다.


여기서 프로퍼티 정의에 대해서 알고 넘어가자!

프로퍼티 정의

- 객체의 프로퍼티가 어떻게 동작할지 정의하는 것

- 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하는 것

- 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의 하는 것

* 프로퍼티 값을 갱신 가능하도록? 열거 가능하도록? 재정의가 가능하도록? 정의할 수 있다.

 

Object.defineProperty

프로퍼티의 어트리뷰트를 정의 가능, 인수로는 객체의 참조와 데이터 프로퍼티의 키인 문자열 그리고 프로퍼티 디스크립터 객체를 전달

const traveler = {};

Object.defineProperty(traveler, "first", {
  value: "곽튜브",
  writable: true,
  enumerable: true,
  configurable: true,
});

Object.defineProperty(traveler, "second", {
  value: "빠니보틀",
});

let descriptor = Object.getOwnPropertyDescriptor(traveler, "first");
console.log(descriptor);
// { value: '곽튜브', writable: true, enumerable: true, configurable: true }
descriptor = Object.getOwnPropertyDescriptor(traveler, "second");
console.log(descriptor);
// { value: '빠니보틀', writable: false, enumerable: false, configurable: false }

- 위 코드를 보게 되면 알 수 있듯이

defineProperty 메서드를 통해서 프로퍼티를 추가할때 디스크립터 객체의 프로퍼티를 누락시키게 되면 undefined false가 기본값으로 설정된다.

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

 

여러개의 프로퍼티를 정의하려면 Object.defineProperties 를 사용할 수 있다.

const traveler = {};

Object.defineProperties(traveler, {
  first: {
    value: "곽튜브",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  second: {
    value: "빠니보틀",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  together: {
    get() {},
    set() {},
    enumerable: true,
    configurable: true,
  },
});

그럼 프로퍼티 어트리뷰트, 디스크립터를 왜 알아야할까?

객체는 변경이 가능하기 때문에 당연하게 변경을 하면서 사용한다.

프로퍼티를 추가하고 삭제하고 갱신하게 되는데 여기서 프로퍼티 어트리뷰트를 재정의하면서 값을 변경하게? 변경하지 못하게? 가능하다.

 

객체 변경 방지

그렇기 때문에 JS는 객체의 변경을 방지하기 위해 다양한 메서드를 제공하고 있다.

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

 

- 프로퍼티 추가 방법 / 두가지 모두 금지

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

   > Object.defineProperty메서드로 추가 또는 갱신

 

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

 

▶ 객체 확장 금지

* 프로퍼티 추가 금지를 의미

Object.preventExtensions 메서드는 객체의 확장을 금지, 확장이 금지된 객체는 프로퍼티 추가가 금지

const traveler = { first: "곽튜브" };
// 추가는 금지 되지만 삭제는 가능하다.

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

Object.preventExtensions(traveler); // 객체 확장 금지

console.log(Object.getOwnPropertyDescriptors(traveler));
// { first: { value: '곽튜브', writable: true, enumerable: true, configurable: true } }

 

객체 밀봉

* 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미

Object.seal 메서드는 읽기와 쓰기만 가능, 갱신도 가능

const traveler = { first: "곽튜브" };

console.log(Object.isSealed(traveler)); // false 밀봉 객체가 아니다.

Object.seal(traveler); // 객체 밀봉

console.log(Object.isSealed(traveler)); // true 밀봉 객체다.

console.log(Object.getOwnPropertyDescriptors(traveler));
// { first: { value: '곽튜브', writable: true, enumerable: true, configurable: false } }

 

▶ 객체 동결

* 동결된 객체는 읽기만 가능

Object.freeze 메서드는 객체를 동결 , 읽기만 가능

const traveler = { first: "곽튜브" };

console.log(Object.isFrozen(traveler)); // false 동결 객체가 아니다.

Object.freeze(traveler); // 객체 동결

console.log(Object.isFrozen(traveler)); // true 동결 객체다.

console.log(Object.getOwnPropertyDescriptors(traveler));
// { first: { value: '곽튜브', writable: false, enumerable: true, configurable: false } }

불변 객체

- 객체 확장금지, 밀봉, 동결은 얕은 변경 방지를 한다. 즉 직속 프로퍼티만 변경이 방지되기 때문에

중첩 객체까지는 영향을 주지 못한다. freeze메서드로 객체를 동결하게 되더라도 중첩 객체까지 동결할수는 없다.

 

* 만약 중첩된 객체까지 동결하려면?  객체 값으로 갖는 모든 프로퍼티를 재귀적으로 실행해주어야한다.

function deep(obj) {
  // 들어오는 매개변수가 undefined, null이 아니고, 객체 타입 그리고 현재 상태가 동결이 아니면!
  if (obj && typeof obj == "object" && !Object.isFrozen(obj)) {
    Object.freeze(obj);
    
    Object.keys(obj).forEach((key) => deep(obj[key]));
  }
  return obj;
}

- 이렇게 조건문을 통해서 현재 상태과 타입을 확인하고 동결을 시켜주게 되면 깊은 객체 동결을 할 수 있다.