모던 자바스크립트 Deep Dive (5)

📖 정리

  • 이 글은 '모던 자바스크립트 Deep Dive'를 읽으면서 새롭게 알게된 내용을 정리하기 위해 쓴 글입니다.

23장 실행 컨텍스트

24장 클로저

  • 클로저에 대한 내용은 따로 블로그 글로 적을 예정

  • 클로저는 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.

  • 클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.

  • 자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프라 한다.

    • 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다.
    • 이 렉시컬 환경은 자신의 외부 렉시컬 환경에 대한 참조를 통해 상위 렉시컬 환경과 연결된다. 이것이 스코프 체인이다.
  • 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.

  • 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.

    • 이때 [[Environment]]에 저장된 상위 스코프의 참조는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리킨다.
  • 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

  • 자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저이다. 하지만 일반적으로 모든 함수를 클로저라고 부르지는 않는다.

    • 상위 스코프의 어떠한 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않는다.
    • 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.
    • 대부분의 모던 브라우저는 최적화를 통해 상위 스코프의 식별자 중에서 클로저가 참조하고 있는 식별자만 기억한다.
  • 클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부른다. 클로저란 함수가 자유 변수에 대해 닫혀있다는 의미이다. 다르게 말하면 자유 변수에 묶여있는 함수라고 할 수 있다.

  • 클로저는 상태를 안전하게 변경하고 유지하기 위해서 사용된다. 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용된다.

  • 캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.

  • 자바스크립트는 정보 은닉을 완전하게 지원하지 않는다.

    • 인스턴스 메서드를 사용한다면 자유 변수를 통해 private을 흉내 낼 수는 있다.
    • 하지만 프로토타입 메서드를 사용한다면 이마저도 불가능하다.

25장 클래스

  • 클래스는 함수이며 기존 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 문법적 설탕이다.

    • 단 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스를 생성하지만 정확히 동일하게 동작하지는 않는다.
    • 클래스는 생성자자 함수보다 엄격하며 생성자 함수에서는 제공하지 않는 기능도 제공한다.
  • 클래스와 생성자 함수의 차이점

  • 클래스를 new 연산자 없이 호출하면 에러가 발생한다.

  • 클래스는 상속을 지원하는 extends와 super 키워드를 제공한다.

  • 클래스는 호이스팅이 발생하지 않는 것처럼 동작한다.

  • 클래스 내의 모든 코드에는 암묵적으로 strict 모드가 지정되어 실행된다.

  • 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]] 값이 false 이다.

  • 클래스와 생성자 함수의 정의 방식 비교

const Person = (function () {
  // 생성자 함수
  function Person(name) {
    this.name = name;
  }
  // 프로토타입 메서드
  Person.prototype.sayHi = function () {
    console.log(`안녕하세요 저는 ${this.name} 입니다.`);
  };
  // 정적 메서드
  Person.sayHello = function () {
    console.log('안녕하세요');
  };

  return Person;
})();
class Person {
  // 생성자
  constructor(name) {
    this.name = name;
  }
  // 프로토타입 메서드
  sayHi() {
    console.log(`안녕하세요 저는 ${this.name} 입니다.`);
  }
  // 정적 메서드
  static sayHello() {
    console.log('안녕하세요');
  }
}
  • 클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정에 함수 객체를 생성한다.

    • 이때 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 함수, 즉 constructor다.
    • 클래스는 클래스 정의 이전에 참조할 수 없다.
  • 클래스 몸체에는 0개 이상의 메서드만 선언할 수 있다. 클래스 몸체에서 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드 세 가지가 있다.

    • constructor

      • 인스턴스를 생성하고 초기화하기 위한 특수한 메서드다.
      • 모든 함수 객체가 가지고 있는 prototype 프로퍼티가 가리키는 프로토타입 객체의 constuctor 프로퍼티는 클래스 자신을 가리키고 있다.
      • 이는 클래스가 인스턴스를 생성하는 생성자 함수인 것을 의미한다.
      • 클래스가 평가되어 생성된 함수 객체나 클래스가 생성한 인스턴스 어디에도 constructor 메서드가 보이지 않는다. 이는 클래스 몸체에 정의한 constructor가 단순한 메서드가 아니라는 것을 의미한다.
      • constructor는 메서드로 해석되는 것이 아니라 클래스가 평가되어 생성한 함수 객체 코드의 일부가 된다.
      • 클래스가 평가되면 constructor의 기술된 동작을 하는 함수 객체가 생성된다.
      • 생략하면 빈 constructor가 암묵적으로 정의된다.
    • 프로토타입 메서드

      • 클래스 몸체에서 정의한 메서드는 생성자 함수에 의한 객체 생성 방식과는 다르게 클래스의 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 된다.
      • 클래스는 생성자 함수와 같이 인스턴스를 생성하는 생성자 함수라고 볼 수 있다. 다시 말해 클래스는 생성자 함수와 마찬가지로 프로토타입 기반의 객체 생성 매커니즘인다.
    • 정적 메서드

      • 인스턴스를 생성하지 않아도 호출할 수 있는 메서드를 말한다.
      • 클래스에서는 메서드에 static 키워드를 붙이면 정적 메서드가 된다.
  • 정적 메서드와 프로토타입 메서드의 차이

    • 정적 메서드와 프로토타입 메서드는 자신이 속해 있는 프로토타입 체인이 다르다.
    • 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.
    • 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 메서드를 참조할 수 있다.
    • 정적 메서드는 클래스로 호출해야 하므로 정적 메서드 내부의 this는 인스턴스가 아닌 클래스를 가리킨다. 즉 프로토타입 메서드와 정적 메서드 내부의 this 바인딩이 다르다.
  • 클래스에서 정의한 메서드는 다음과 같은 특징을 갖는다.

    • function 키워드를 생략한 메서드 축약 표현을 사용한다.
    • 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없다.
    • 암묵적으로 strict mode로 실행된다.
    • for in 문이나 Object.keys 메서드 등으로 열거할 수 없다.
    • 내부 메서드 [[Construct]]를 갖지 않는 non-construct다.
  • 접근자 프로퍼티는 getter 에는 get, setter 에는 set 키워드를 앞에 붙여서 정의한다.

    • 클래스의 메서드는 기본적으로 프로토타입 메서드가 된다. 따라서 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.
  • 자바스크립트의 클래스 몸체에는 메서드만 선언할 수 있다.

    • 하지만 최신 브라우저 또는 최신 Node.js에서 실행하면 정상 동작한다.
    • 자바스크립트에서도 인스턴스 프로퍼티를 마치 클래스 기반 객체지향 언어의 클래스 필드처럼 정의할 수 있는 새로운 표준 사양이 제안되어 있다.(2021.01 기준)
    • 클래스 몸체에서 클래스 필드를 정의할 경우 this에 클래스 필드를 바인딩해서는 안 된다.
    • 인스턴스를 생성할 때 외부의 초기값으로 클래스 필드를 초기화해야 할 필요가 있다면 constructor에서 클래스 필드를 초기화해야 한다.
    • 함수는 일급 객체이므로 함수를 클래스 필드에 할당할 수 있다. 따라서 클래스 필드를 통해 메서드를 정의할 수 있다.
    class Person {
      name = 'Yoo';
      getName = () => this.name;
    }
    const me = new Person();
    console.log(me); // Person {name: "Yoo", getName: f}
    console.log(me.getName()); // Yoo
    • 클래스 필드에 함수를 할당하는 경우, 이 함수는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다.
    • 모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문이다. 따라서 클래스 필드에 함수를 할당하는 것은 권장하지 않는다.
  • private 필드의 선두에는 #을 붙여준다. private 필드를 참조할 때도 #을 붙여주어야 한다.

    • 클래스 외부에서 private 필드에 직접 접근할 수 있는 방법은 없다. 다만 접근자 프로퍼티를 통해 간접적으로 접근하는 방법은 유효하다.
  • 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.

    • 클래스는 상속을 통해 다른 클래스를 확장할 수 있는 문법인 extends 키워드가 기본적으로 제공된다.
    • extends 키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수도 있다. 단 extends 키워드 앞에는 반드시 클래스가 와야 한다.
    • extends 키워드 다음에는 클래스뿐만이 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다. 이를 통해서 동적으로 상속받을 대상을 결정할 수 있다.
  • super() 는 수퍼클래스의 constructor를 호출하여 인스턴스를 생성한다.

    • super 키워드는 함수처럼 호출하면 수퍼클래스의 constructor를 호출한다.
      • 서브클래스에서 constructor를 생략하지 않는 경우 서브 클래스의 constructor에서는 반드시 super를 호출해야 한다.
      • 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다.
      • super는 반드시 서브클래스의 constructor에서만 호출한다.
    • super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
      • super 참조가 동작하기 위해서는 super를 참조하고 있는 메서드가 바인딩되어 있는 객체의 프로토타입을 찾을 수 있어야 한다. 이를 위해 메서드는 내부 슬롯 [[HomeObject]] 를 가지며, 자신을 바인딩하고 있는 객체를 가리킨다.
      • ES6 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]]를 갖는다.
  • 클래스 상속에 관한 글을 따로 작성해야 할 것 같다...

26장 ES6 함수의 추가 기능

  • ES6 이전의 모든 함수는 일반 함수로서 호출할 수 있는 것은 물론 생성자 함수로서 호출할 수 있다.

    • ES6 이전에 일반적으로 메서드라고 부르던 객체에 바인딩된 함수도 callable이며 constructor이다.
    • 함수에 전달되어 보조 함수의 역할을 수행하는 콜백 함수도 마찬가지다.
  • ES6 에서는 함수를 사용 목적에 따라 세 가지 종류로 구분한다.

  • ES6 사양에서 메서드는 메서드 축약 표현으로 정의된 함수만을 의미한다.

    • ES6 사양에서 정의한 메서드는 인스턴스를 생성할 수 없는 non-constructor다.
    • ES6 메서드는 인스턴스를 생성할 수 없으므로 prototype 프로퍼티가 없고 프로토타입 메서드도 생성하지 않는다.
    • 표준 빌트인 객체가 제공하는 프로토타입 메서드와 정적 메서든느 모두 non-constructor다.
    • ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다.
  • 화살표 함수는 표현만 간략한 것이 아니라 내부 동작도 기존의 함수보다 간략하다. 특히 화살표 함수는 콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위한 대안으로 유용하다.

    • 화살표 함수는 인스턴스를 생성할 수 없는 non-constructor다.
    • 중복된 매개변수 이름을 선언할 수 없다.
    • 화살표 함수는 함수 자체의 this, arguments, super, new, target 바인딩을 갖지 않는다.
    • this
      • 콜백 함수 내부의 this가 외부 함수의 this와 다르기 때문에 발생하는 문제를 해결하기 위해 의도적으로 설계된 것이다.
      • 화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다. 이를 lexical this라고 한다.
      • 이는 마치 렉시컬 스코프와 같이 화살표 함수의 this가 함수가 정의된 위치에 의해 결정된다는 것이다.
      • 화살표 함수 내부에서 this를 참조하면 일반적인 식별자 처럼 스코프 체인을 통해 상위 스코프에서 this를 탐색한다.
      • call, bind, apply를 사용해도 화살표 함수 내부의 this를 교체할 수 없다.
  • Rest 파라미터

    • 매개변수 이름 앞에 세개의 점 ...을 붙여서 정의한 매개변수를 의미한다.
    • 함수에 전달된 인수들의 목록은 배열로 전달받는다.
    • 반드시 마지막 파라미터여야 한다.
    • arguments 객체는 배열이 아닌 유사 배열 객체이므로 배열 메서드를 사용하려면 call이나 apply 메서드를 사용해 배열로 변환해야 하는 번거로움이 있었다.
  • 매개변수 기본값을 지정할 수 있다.