Read/Morden Javascript Deep Dive

[Modern Javascript Deep Dive] 16~18장 인사이트 정리

helloyukyung 2024. 1. 16. 22:22

16~17장을 읽으며 얻은 인사이트를 추가로 공부하면서 정리해 보자.

  • 16장: 프로퍼티 어트리뷰트
  • 17장: 생성자 함수에 의한 객체 생성
  • 18장: 함수와 일급 객체

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

객체의 프로퍼티는 2종류로 나뉜다.

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

프로퍼티 어트리뷰트: JS엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본 값으로 자동 정의
프로퍼티 디스크립터: 프로퍼티 어트리뷰트 정보를 제공

데이터 프로퍼티

데이터 프로퍼티는 아래와 같은 프로퍼티 어트리뷰트를 갖는데 아래 값들은 JS엔진이 프로퍼티를 생성할 때 기본값으로 자동 정의된다.

  • Value: 우리가 흔히 알고 있는 값으로 프로퍼티 키를 통해 접근하면 반환되는 값이다. 프로퍼티 키를 통해 값을 변경하면 Value에 값을 재할당하며, 만약 프로퍼티가 없으면 동적 생성 후 생성된 프로퍼티의 Value에 값을 저장한다.
  • Writable: 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다. false일 경우 Value를 변경할 수 없다.(즉 읽기 전용 프로퍼티가 된다.)
  • Enumerable: 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다. Enumerable값이 false인 경우 for..in문이나 Object.keys()등으로 열거할 수 없다.
  • Configurable: 재정의 가능 여부를 나타내며 불리언 값을 갖는다. false인 경우 프로퍼티의 삭제, 변경이 금지된다. 단, Writable이 true인 경우 Value의 변경과 Writable을 false로 변경하는 것은 허용된다.
const person = {
  name : 'lee'
}

console.log(Object.getOwnPropertyDescriptor(person,'name'))
// {value: 'lee', writable: true, enumerable: true, configurable: true}

접근자 프로퍼티

접근자 프로퍼티는 자체적으로는 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.
접근자 함수는 getter/setter 함수라고도 불린다.

  • Get :접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수이다. 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 Get의 값(getter함수 호출 값)이 프로퍼티 값으로 반환된다.
  • Set :접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다. 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 Set의 값, setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
  • Enumerable: 데이터 프로퍼티와 동일
  • Configurable: 데이터 프로퍼티와 동일
const person = {
    // 데이터 프로퍼티
    firstName : 'sally',
    lastName : 'kim',

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


// firstName은 데이터 프로퍼티
let discriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(discriptor); // {value: 'sally', writable: true, enumerable: true, configurable: true}

// fullName은 접근자 프로퍼티 
let descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor); // {enumerable: true, configurable: true, get: ƒ, set: ƒ}

접근자 프로퍼티는 자체적으로 값을 가지지 않으며 다만 데이터 프로퍼티의 값을 읽거나 저장할 때 관여할 뿐이다.

getter와 setter를 ‘실제’ 프로퍼티 값을 감싸는 래퍼(wrapper)처럼 사용하면, 프로퍼티 값을 원하는 대로 통제할 수 있다.

아래 예시에선 name을 위한 setter를 만들어 user의 이름이 너무 짧아지는 걸 방지한다.
실제 값은 별도의 프로퍼티 _name에 저장된다.

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("입력하신 값이 너무 짧습니다. 네 글자 이상으로 구성된 이름을 입력하세요.");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // 너무 짧은 이름을 할당하려 함 
  1. user의 이름은 _name에 저장되고, 프로퍼티에 접근하는 것은 getter와 setter를 통해 이뤄진다.
  2. 기술적으론 외부 코드에서 user.name을 사용해 이름에 바로 접근할 수 있다.
    그러나 밑줄 _
    로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습이다.

객체 변경 방지

객체는 변경 가능한 값이므로 재할 당 없이 직접 변경할 수 있지만, 객체의 확장을 금지하거나, 객체를 밀봉, 동결할 수도 있다.

  1. 확장 금지 Object.preventExtensions()
const person = { name: 'Lee' };

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

// person 객체의 확장을 금지하여 프로퍼티 추가를 금지  
Object.preventExtensions(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
  1. 객체 밀봉 Object.freeze()

프로퍼티의 읽기와 갱신만 가능하며 프로퍼티의 추가, 삭제, 제정의가 금지된다.

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

객체의 생성 방법

1. 객체 리터럴 (Object Literal)

const obj = {};

2.new 연산자 사용

2.const obj = new Object();

객체 리터럴에 의한 객체 생성 방식의 문제점

단 하나의 객체만 생성하기 때문에, 객체 재사용이 불가능.
동일한 프로퍼티를 갖는 객체를 여러 개 생성할 경우 비효율적.

// 프로퍼티 값은 다를 수 있지만, 일반적으로 메서드는 동일
const circle1 = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  }
};

console.log(circle1.getDiameter()); // 10

const circle2 = {
  radius: 10,
  getDiameter() {
    return 2 * this.radius;
  }
};

console.log(circle2.getDiameter()); // 20

생성자 함수에 의한 객체 생성 방식의 장점

프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성 가능하다.

생성자 함수 : new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수
(new 연산자 없이 사용하면 일반 함수로 동작)

function Circle(radius) {
  // 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스의 생성
const circle1 = new Circle(5);  // 반지름이 5인 Circle 객체를 생성
const circle2 = new Circle(10); // 반지름이 10인 Circle 객체를 생성

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20
const circle3 = Circle(15);

// 일반 함수로서 호출된 Circle은 반환문이 없으므로 암묵적으로 undefined를 반환한다.
console.log(circle3); // undefined

// 일반 함수로서 호출된 Circle내의 this는 전역 객체를 가리킨다.
console.log(radius); // 15

생성자 함수의 인스턴스 생성 과정

생성자 함수의 역할은 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 탬플릿(클래스)으로 동작해서,

인스턴스를 생성하는 것과 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값할당)하는 것을 의미한다.

  • 생성자 함수의 내부 코드를 살펴보면 this에 프로퍼티를 추가하고 필요에 따라 전달된 인수를 프로퍼티의 초기값에 할당하여 인스턴스를 초기화한다.
  • 인스턴스를 생성하고 반환하는 코드는 자바스크립트 엔진에서 암묵적으로 일어난다.

1. 인스턴스 생성과 this 바인딩

암묵적으로 빈 객체가 생성되는데, 이 빈 객체가 생성자 함수가 생성한 인스턴스(아직 완성되지 않은)이다.

그리고 암묵적으로 생성된 빈 객체, 인스턴스는 this에 바인딩된다.
(바인딩: 식별자와 값을 연결하는 과정)

2. 인스턴스 초기화

생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되며, this에 바인딩되어 있는 인스턴스를 초기화한다.

즉, this에 바인딩되어있는 인스턴스에 프로퍼티, 혹은 메서드를 추가하고, 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당한다.

3. 인스턴스 반환

생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.

(만약 this가 아닌 다른 객체를 명시적으로 반환하면 this가 반환되지 않고 return 문에 명시한 객체가 반환된다)

function Circle(radius) {
  // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩 됨.

  // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
  this.radius = radius
  this.getDiameter = function () {
    return 2 * this.radious
  }
  // 3. 암묵적으로 this를 반환하지만, 명시적으로 객체를 반환하면 반환한 객체를 반환
  // return {} <-  명시적 객체 반환
}

생성자함수에서는 기본동작을 훼손하므로, 생성자 함수 내부에서는 return문을 반드시 생략해야 함

일반 함수와 생성자 함수의 동작 차이

함수는 객체와 다르게 호출 가능. 따라서 [[Call]],[[Constract]] 내부 메서드를 추가로 가지고 있음 

function foo() {}

foo()
// 일반적인 함수로서 [[Call]] 호출

new foo()
// 생성자 함수로서 [[Construct]] 호출

모든 함수 객체는 callbale이지만 모든 함수객체가 constructor인 것은 아니다

constructor와 non-constructor의 구분

  • constructor : 함수 선언문, 함수 표현식, 클래스(클래스도 함수)
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
function foo() {} //🙆‍♀️
const bar = function () {} //🙆‍♀️
const hihi = {
  x: function () {} //🙆‍♀️
}

new foo() // foo {}
new bar() // bar {}
new hihi.x() // x {}
const arrow = () => {} // 🙅🏿‍♀️
const obj = {
  x() {} // 🙅🏿‍♀️
} 

new arrow() 
new obj.x()

생성자 함수는 일반적으로 파스칼 케이스로 명명해 일반함수와 구별할 수 있도록 노력한다.