JavaScript

[모던 자바스크립트]프로퍼티 어트리뷰트

jjin502 2022. 8. 17. 20:02

내부 슬롯과 내부 메서드

  • 내부 슬롯과 내부 메서드 : 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript사양에서 사용하는 의사 프로퍼티와 의사 메서드.
  • 형태 : [[...]] (이중 대괄호로 감싸져있음)
  • 개발자가 직접 접근할 수 없음.
  • 간접적으로 접근이 가능함
// 16-1
    const o = {};

    // 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근 불가능
    o.[[Prototype]]
    // SyntaxError: Unexpected token '['
    // 단, 일부 내부 슬롯과 내부 메서드에 한해 간접적으로 접근할 수 있는 수단을 제공하긴 함.
    o.__proto__; // Object.prototype

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

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

  • 프로퍼티의 상태 : 프로퍼티의 값, 값의 갱신 가능 여부, 열거 가능 여부, 재정의 가능 여부를 말함.
  • 프로퍼티 어트리뷰트 : JS엔진이 관리하는 내부 상태 값인 내부 슬롯
  • [[Writeable]], [[Value]], [[Enumerable]], [[Configurable]],..
  • 직접 접근 불가능, but Object.getOwnPropertyDescriptor 메서드로 간접적으로 확인 가능
// 16-2
    const person = {
      name: "Sim"
    };

    // 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환함.
    console.log(Object.getOwnPropertyDescriptor(person, "name"));
    // 첫 번째 매개변수 : 객체의 참조를 전달. 두 번째 매개변수 : 프로퍼티 키를 문자열로 전달.
    // {value: "Sim", writable: true, enumerable: true, configurable: true}
    // 프로퍼티 디스크립터 객체를 반환
// 16-3
    const person = {
      name: "Sim"
    };
    // 프로퍼티 동적 생성
    person.age = 18;
    // 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환함.
    console.log(Object.getOwnPropertyDescriptor(person));
    // {value: "Sim", writable: true, enumerable: true, configurable: true}

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

어떻게 구분하는 가 ?

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

데이터 프로퍼티

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

 

// 16-4
    const person = {
      name: "Sim"
    };
    // 프로퍼티 동적 생성
    person.age = 18;
    // 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 취득함.
    console.log(Object.getOwnPropertyDescriptor(person, "name"));
    // {value: "Sim", writable: true, enumerable: true, configurable: true}
// 16-5
    const person = {
      name: "Sim"
    };
    // 프로퍼티 동적 생성
    person.age = 18;
    // 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 취득함.
    console.log(Object.getOwnPropertyDescriptor(person));
    // {value: "Sim", writable: true, enumerable: true, configurable: true}
  • 프로퍼티가 생성될 때 [[Value]]의 값은 프로퍼티 값으로 초기화,[[Writeable]], [[Enumerable]], [[Configurable]] 의 값은 true로 초기화
  • 프로퍼티를 동적으로 추가해도 마찬가지

접근자 프로퍼티

프로퍼티 어트리뷰트 프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Get]] get - 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면, 프로퍼티 어트리 뷰트 [[Get]]의 값 즉, getter함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
[[Set]] set - 접근자 프로퍼티를 통해 데이터 프로퍼티 값을 저장할 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면, 프로퍼티 어트리뷰트 [[Set]] 의 값, 즉 setter함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
[[Enumerable]] enumerable - 데이터 프로퍼티와 같음.
[[Configurable]] configurable - 데이터 프로퍼티와 같음.
  • 접근자 함수는 getter/setter 함수라고도 함
  • 접근자 프로퍼티는 둘 다 정의 할 수도 있고, 하나만 정의할 수도 있음.
// 16-6
    const person = {
      // 데이터 프로퍼티
      firstName: "Mijin",
      lastName: "Sim",

      // 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); // Mijin Sim

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

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

    // firtName은 데이터 프로퍼티
    // 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]]
    // 프로퍼티 어트리뷰트를 가짐.

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

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

내부 슬롯/메서드 관점에서 보자면 ....

프로퍼티 키가 유효한지 확인.

  • 프로퍼티 키는 문자열 또는 심벌이어야 함.
  • 프로퍼티 키 "fullName" 은 문자열 이므로 유효한 프로퍼티 키다.

프로토타입 체인에서 프로퍼티를 검색.

  • person객체에서 fullName 프로퍼티가 존재함.

검색된 fullName 프로퍼티가 데이터 프로퍼티인지 전근자 프로퍼티인지 확인

  • fullName 프로퍼티는 접근자 프로퍼티이다.

접근자 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]]  의 값, 즉 getter함수를 호출해 그 결과를 반환.

  • 프로퍼티 fullName의 프로퍼티 어트리뷰트 [[Get]] 의 값은 Object.getOwnPropertyDescriptor 메서드가 반환하는 프로퍼티 디스크립터 객체의 get프로퍼티 값과 같다.

 

흠 .. 프로토타입이 뭐죠 .. ?

  • 어떤 상위(부모) 객체의 역할을 하는 객체
  • 하위(자식)객체에 프로퍼티와 메서드를 상속함.
  • 상속받은 하위객체는 자신의 프로퍼티 또는 메서드 처럼 사용할 수 있음.

그런데 접근자 프로퍼티와 데이터 프로퍼티를 어떻게 구분해야하나요 .. ?

  • 이렇게 !!
// 16-7

    // 일반 객체의 __proto__는 접근자 프로퍼티이다.
    Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");

    // 함수 객제의 prototype은 데이터 프로퍼티다.
    Object.getOwnPropertyDescriptor(function () {}, "prototype");

프로퍼티 정의

  • 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것.
  • Object.defineProperty 메서드 사용하면 가능함
// 16-8
    const person = {};
    // 데이터 프로퍼티 정의
    Object.defineProperty(person, "firstName", {
      value: "Mijin",
      writable: true,
      enumerable: true,
      configurable: true
    });

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

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

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

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

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

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

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

    descripter = Object.getOwnPropertyDescriptor(person, "lastName");
    console.log("lastName", descripter);
    // lastName {value: "Sim", 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
    });

    descripter = Object.getOwnPropertyDescriptor(person, "fullName");
    console.log("fullName", descripter);
    // fullName {get: ƒ get(), set: ƒ set(), enumerable: true, configurable: true}

    person.fullName = "Hyunjin Sim";
    console.log(person);
    // {firstName: "Hyunjin", fullName: "Hyunjin Sim"}
  • Object.defineProperty 메서드는 한 번에 한 번만 정의가 가능함.
  • Object.defineProperties를 쓰면 한 번에 사용 가능
// 16-9
    const person = {};
    Object.defineProperty(person, {
      // 데이터 프로퍼티 정의
      firstName: {
        value: "Mijin",
        writable: true,
        enumerable: true,
        configurable: true
      },
      lastName: {
        value: "Sim",
        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 = "Hyunjin Sim";
    console.log(person);
    // {firstName: "Hyunjin", fullName: "Hyunjin Sim"}

객체 변경 방지

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

객체 확장 금지

// 16-10
    const person = { name: "Sim" };

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

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

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

    // 프로퍼티 추가가 금지됨
    person.age = 18;
    console.log(person);

    // 삭제는 가능
    delete person.name;
    console.log(person);

    // 프로퍼티 정의에 의한 프로퍼티 추가도 금지됨
    Object.defineProperty(person, "age", { value: 18 });
    // TypeError: Cannot define property afe, object is not extensiable

객체 밀봉

  • 읽기, 쓰기만 가능
// 16-11
    const person = { name: "Sim" };

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

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

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

    // 밀봉된 객체는 configurable이 false다.
    console.log(Object.getOwnPropertyDescriptors(person));

    // 프로퍼티 추가 금지됨
    person.age = 20;
    console.log(person);

    // 프로퍼티 삭제 금지됨
    delete person.name;
    console.log(person);

    // 갱신 가능
    person.name = "Lee";
    console.log(person);

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

객체 동결

  • 읽기만 가능
// 16-12
    const person = { name: "Sim" };

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

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

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

    // 밀봉된 객체는 configurable이 false다.
    console.log(Object.getOwnPropertyDescriptors(person));

    // 동결된 객체는 writable과 configurable이 false다.
    console.log(Object.getOwnPropertyDescriptors(person));

    // 프로퍼티 추가 금지됨
    person.age = 20;
    console.log(person);

    // 프로퍼티 삭제 금지됨
    delete person.name;
    console.log(person);

    // 갱신 금지
    person.name = "Lee";
    console.log(person);

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

불변 객체

  • 위 메서드들은 얕은 변경 방지로 직속 프로퍼티만 변경이 방지되고, 중첩에는 영향을 미치지 않음.
// 16-14

    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: "Sim",
      address: { city: "Ulsan" }
    };

    // 얕은 객체 동결
    deepFreeze(person);

    // 직속 프로퍼티만 동결
    console.log(Object.isFrozen(person)); // false
    // 중첩 객체까지 동결.
    console.log(Object.isFrozen(person.address));

    person.address.city = "Busan";
    console.log(person);