[모던 자바스크립트 Deep Dive] - 22~25장

Lee Jeong Min·2021년 9월 22일
1
post-thumbnail

이 글은 책 모던 자바스크립트 22장 ~25장을 읽고 정리한 글입니다.

22장 - this

this 키워드

메서드의 경우 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 하는데 객체 리터럴 방식으로 생성한 객체의 경우 식별자를 통해 그것이 가능하지만, 생성자 함수 방식으로 인스턴스를 생성하는 경우 인스턴스를 생성하기 전이므로 가리키는 식별자를 알 수 없다.

이러한 상황에서 this라는 특수한 식별자를 통해서 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수를 사용한다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.

this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.(바인딩: 식별자와 값을 연결하는 과정)

strict 모드에서는 일반함수의 this는 undefined로 설정된다. 그 이유는 this가 일반적으로 객체의 메서드 내부 또는 생성자 함수 내부에서만 의미가 있는데, 일반함수 내부의 this는 사용할 필요가 없기 때문에 의미가 없어서 undefined로 처리한다.

함수 호출 방식this 바인딩
일반 함수 호출전역 객체
메서드 호출메서드를 호출한 객체
생성자 함수 호출생성자 함수가 (미래에) 생성할 인스턴스
Function.prototype.apply/call/bind 메서드에 의한 간접호출Function.prototype.apply/call/bind 메서드에 첫 번째 인수로 전달할 객체

함수 호출 방식과 this 바인딩

this 바인딩은 함수 호출 방식에 따라 동적 결정된다.

  1. 일반 함수 호출

  2. 메서드 호출

  3. 생성자 함수 호출

  4. Function.prototype.apply/call/bind 메서드에 의한 간접 호출

렉시컬 스코프와 this 바인딩은 결정 시기가 다르다.
렉시컬 스코프: 함수 정의가 평가되어 함수 객체가 생성되는 시점에 상위 스코프 결정
this: 함수 호출 시점에 결정

일반 함수 호출

기본적으로 this에 전역 객체가 바인딩 된다.

일반함수로 호출된 모든 함수(중첩함수, 콜백 함수 포함) 내부의 this에는 전역 객체가 바인딩된다.

중첩함수나 콜백함수에 this를 바인딩하려면 Function.prototype.apply와 같은 메서드를 사용하거나, 화살표 함수를 이용하거나(화살표 함수 내부의 this는 상위 스코프 this를 가리킨다) 상위스코프에서 const that = this와 같이 변수를 정의하여 내부에서 사용하는 방법이 있다.

메서드 호출

해당 메서드를 호출한 객체에 바인딩 된다.

생성자 함수 호출

생성자 함수 내부의 this에는 생성자 함수가 생성할 인스턴스가 바인딩 된다.

Function.prototype.apply/call/bind 메서드에 의한 간접 호출

apply, call, bind 메서드는 Function.prototype의 메서드로 모든 함수가 상속받아 사용할 수 있다.

이 메서드의 첫 번째 인수로 오는 객체를 호출한 함수의 this에 바인딩한다.

apply와 call의 차이는 인수를 전달하는 방식에서 차이가 있는데, apply는 [1, 2, 3]로, call은 1, 2, 3으로 배열로 묶어서 전달하느냐, 리스트형식으로 전달하느냐에 차이가 있다.

apply와 call 메서드의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에 배열 메서드를 사용하는 경우 Array.prototype.slice와 같은 배열 메서드를 사용하여 배열의 복사본을 생성한다.

bind 메서드의 경우 함수를 호출하지 않고 this로 사용될 객체만 전달하는데 호출하고 싶다면, 명시적으로 호출 형식으로 작성 해주어야 한다. bind(arguments)();
단순히 바인딩만 시키는 경우 bind(arguments);
bind 메서드는 메서드의 this와 내부의 중첩, 콜백함수의 this가 불일치하는 문제를 해결하기위해 사용된다.

const person = {
  name: 'Lee',
  foo(callback) {
    setTimeout(callback, 100);
    // setTimeout(callback.bind(this), 100);
  }
};

person.foo(function() {
  console.log(`Hi! my name is ${this.name}.`);
  // bind함수 전 Hi! my name is . (브라우저 환경에서 window.name의 기본값은 '' 이기 때문에)
  // Node.js환경에서 this.name은 undefined이다.
  // bind 함수 적용시 Hi! my name is Lee.
});

위와 같이 foo의 callback함수의 this는 일반함수로 사용되어서 window객체를 가리키는데 이 문제를 해결하기 위해 setTimeout(callback.bind(this)과 같이 작성하여 문제를 해결한다.


23장 - 실행 컨텍스트

소스코드의 타입

ECMAScript 사양은 소스코드를 4가지 타입으로 구분하고, 4가지 타입의 소스코드는 실행 컨텍스트를 생성한다.

소스코드의 타입설명
전역 코드전역에 존재하는 소스코드를 말한다. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함하지 않는다.
함수 코드함수 내부에 존재하는 소스코드를 말한다. 함수 내부에 중첩된 함수, 클래스등의 내부 코드는 포함되지 않는다.
eval 코드빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드를 말한다.
모듈 코드모듈 내부에 존재하는 소스코드를 말한다. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않는다.
  1. 전역코드: 전역 변수를 관리하기 위해 최상위 스코프인 전역 스코프를 생성하여 var키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 객체와 연결되어야 한다. 이후 전역 코드가 평가되면 전역 실행 컨텍스트가 생성된다.

  2. 함수 코드: 함수 지역 스코프를 생성하며 지역변수, 매개변수, arguments 객체를 관리한다. 그 후, 전역스코프에서 시작하는 스코프 체인의 일원으로 연결하고, 함수 코드가 평가되면 함수 실행 컨텍스트가 생성된다.

  3. strict mode에서 자신만의 독자적인 스코프를 생성하고, 평가되면 eval 실행 컨텍스트가 생성된다.

  4. 모듈별로 독립적인 모듈 스코프를 생성한다. --> 평가 후 모듈 실행 컨텍스트가 생성

소스코드의 평가와 실행

소스코드의 평가와 소스코드의 실행의 과정을 나누어서 진행하며

소스코드 평가(선언문) 과정에서 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문을 먼저 실행하여 실행 컨텍스트가 관리하는 스코프에 등록한다.

이후 소스코드가 순차적으로 실행되며(런타임) 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색하여 취득 후 값의 변경 등의 결과는 실행 컨텍스트가 관리하는 스코프에 등록된다.

예를 들어 var x = 1; 이라는 선언문을 처음에 실행 컨텍스트를 생성하여 등록하고 있다가 런타임 시 x = 1 을 평가하여 x에 1을 할당한 결과를 실행 컨텍스트에 등록하여 관리한다.

실행 컨텍스트의 역할

const x = 1;

function foo(a) {
  const x = 10;
  const y = 20;
  console.log(a + x + y);
}

foo(100);

console.log(x);

다음과 같은 코드가 있을 때, JS엔진은 다음과 같은 순서대로 실행된다.

  1. 전역 코드 평가: 선언문이 먼저 실행되어 실행컨텍스트가 관리하는 전역 스코프에 전역변수, 전역함수가 등록됨.

  2. 전역 코드 실행: 런타임이 시작되어 전역 코드가 순차적으로 실행되면서 변수에 값이 할당되고 함수가 호출되어 함수 내부로 진입.

  3. 함수 코드 평가: 지역 변수가 실행컨텍스트가 관리하는 지역 스코프에 등록되고, arguments 객체가 생성되며 this바인딩이 결정된다.

  4. 함수 코드 실행: 함수 코드 평가 과정이 끝난 후, 코드가 순차적으로 실행되면서 console함수를 실행 시키는데, 이를 위해 지역 스코프는 상위 스코프인 전역 스코프와 연결되어야 함. 이후에 함수 코드 실행과정이 종료 된후 전역코드 실행을 계속함.

위와 같이 코드가 실행되려면 스코프, 식별자, 코드 실행 순서등의 관리가 필요한데 이를 실행 컨텍스트가 관리한다.
실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.
식별자와 스코프는 실행컨텍스트의 렉시컬환경으로, 코드 실행 순서는 실행 컨텍스트 스택으로 관리한다.

실행 컨텍스트 스택

소스코드를 평가할 때, 전역코드와 함수코드 같이 소스코드의 타입이 다른 경우, JS엔진은 먼저 전역 코드를 평가하여 전역 실행 컨텍스트를 생성한 후, 함수가 호출되면 함수 코드를 평가하여 함수 실행 컨텍스트를 생성한다.

생성된 실행 컨텍스트는 스택 자료구조로 관리되는데 이를 실행 컨텍스트 스택이라고 부른다.

실행 컨텍스트 스택은 코드의 실행 순서를 관리한다. 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트는 언제나 현재 실행중인 코드의 실행 컨텍스트이다. 이를 실행 중인 실행 컨텍스트라고 부른다.

렉시컬 환경

렉시컬 환경은 스코프와 식별자를 관리하는 공간이다.

실행 컨텍스트는 LexicalEnvironment 컴포넌트와 VariableEnvironment 컴포넌트로 구성되는데 이 책에서는 구분하지 않고 렉시컬 환경으로 통일해 설명한다.

렉시컬 환경

렉시컬 환경은 두 개의 컴포넌트로 구성된다.

  • 환경 레코드: 스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소

  • 외부 렉시컬 환경에 대한 참조: 상위 스코프를 가리키며 상위 스코프란 외부 렉시컬 환경, 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경을 말함. 이를 통해 단방향 링크드 리스트인 스코프 체인을 구현한다.

실행 컨텍스트의 생성과 식별자 검색 과정

var x = 1;
const y = 2;

function foo (a) {
  var x = 3;
  const y = 4;
  
  function bar (b) {
    const z = 5;
    console.log(a + b + x + y + z);
  }
  bar(10);
}

foo(20);

이 코드에서 어떻게 실행 컨텍스트가 생성되고 코드 실행 결과가 관리 되는지는 아래의 순서대로 진행된다.

1. 전역 객체 생성

전역 코드가 평가 되기 이전에 생성되어 빌트인 전역 프로퍼티와 빌트인 전역함수, 표준 빌트인 객체가 추가되며 특정 환경의 호스트 객체를 포함한다.

전역 객체 또한 Object.prototype을 상속받아 프로토타입 체인의 일원이다.

2. 전역 코드 평가

  1. 전역 실행 컨텍스트 생성

  2. 전역 렉시컬 환경 생성

  • 전역 환경 레코드 생성: 전역 환경 레코드는 객체 환경 레코드와 선언적 환경 레코드로 구성된다.

    • 객체환경 레코드 생성: BindingObject라고 부르는 객체와 연결된다. 전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 환경 레코드의 객체 환경 레코드에 연결된 BindingObject를 통해 전역 객체의 프로퍼티와 메서드가 된다.
    • 선언적 환경 레코드 생성: 전역 객체의 프로퍼티가 되지 않고, 개념적인 블록인 전역 환경 레코드의 선언적 환경 레코드에 let과 const 존재

      객체 환경 레코드 - 전역 객체과 관리하던 var 키워드로 선언한 전역 변수, 함수 선언문으로 정의한 전역함수, 빌트인 등을 관리한다.
      선언적 환경 레코드 - let, const 키워드로 선언한 전역 변수 관리.

  • this 바인딩: 일반적으로 전역 코드에서 this는 전역 객체를 가리킴. 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에서 전역 객체가 바인딩 된다. this바인딩은 전역 환경 레코드와 함수 환경 레코드에만 존재!

  • 외부 렉시컬 환경에 대한 참조 결정: 전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 null이다.(전역 렉시컬 환경이 스코프 체인의 종점에 존재함을 의미)

3. 전역 코드 실행

코드가 순차적으로 실행된다. 이 과정에서 동일한 이름의 식별자가 다른 스코프에 여러개 존재하는 경우가 있는데, 어느 스코프를 참조할 지 결정하는 것을 식별자 결정이라고 한다.

식별자 결정을 위해 식별자를 검색할 때는 실행중인 실행 컨텍스트에서 식별자를 검색하기 시작한다.

4. foo 함수 코드 평가

foo 함수가 호출되면 전역 코드의 실행을 중지하고 foo 함수 내부로 코드의 제어권이 이동한다. 그 이후에 함수 코드를 평가하기 시작하며 아래의 순서대로 진행이 된다.

  1. 함수 실행 컨텍스트 생성: 생성된 함수 실행 컨텍스트는 함수 렉시컬 환경이 완성된 다음 실행 컨텍스트에 푸시된다.

  2. 함수 렉시컬 환경 생성: 렉시컬 환경을 생성하고 foo 함수 실행 컨텍스트에 바인딩 한다.(환경 레코드와 외부 렉시컬 환경에 대한 참조로 구성)

  • 함수 환경 레코드 설정: 매개변수, arugments 객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리한다.

  • this 바인딩: 일반함수로 호출 되었으므로 전역 객체를 가리킨다.

  • 외부 렉시컬 환경에 대한 참조 결정: foo 함수 정의가 전역 코드 평가 시점에 평가되어 이 시점의 실행중인 실행 컨ㅌ켁스트는 전역 실행 컨텍스트이므로 외부 렉시컬 환경에 대한 참조에는 전역 렉시컬 환경의 참조가 할당된다. 함수의 상위 스코프를 함수 객체의 내부 슬롯 [[Environment]]에 저장하는데, 이것이 외부 렉시컬 환경에 대한 참조에 할당되는 것이며 렉시컬 스코프를 구현하는 메커니즘이다.

    렉시컬 스코프와 함수 객체의 내부 슬롯 [[Environment]]는 클로저를 이해할 수 있는 중요한 단서이다.

5. foo 함수 코드 실행

코드가 실행되면서 식별자 결정을 위해 실행 중인 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색하기 시작한다.

6. bar 함수 코드 평가

foo 함수 코드 평가와 동일하게 이루어짐

7. bar 함수 코드 실행

거의 foo 함수 코드와 비슷하게 이루어지며, 여기서 console.log(a + b + x + y+ z)의 경우 다음의 순서로 이루어진다.

  1. console식별자 검색: 식별자를 검색하다가 최종적으로 전역 렉시컬 환경의 객체 환경 레코드의 BindingObject에서 찾는다.

  2. log 메서드 검색: 프로토타입 체인을 통해 메서드 검색(console 객체가 직접 소유하는 프로퍼티)

  3. a + b + x + y + z 표현식의 평가

  4. console.log 메서드 호출

8. bar 함수 코드 실행 종료

bar 함수 실행 컨텍스트가 실행 컨텍스트 스택에서 pop되어 제거되고 foo 실행 컨텍스트가 실행 중인 실행 컨텍스트가 됨 --> 실행 컨텍스트가 제거 되었다고 해서 렉시컬 환경 까지 즉시 소멸하는 것이 아닌 가비지 컬렉터에 의해 메모리 공간의 확보가 해제 될때 까지 남아 있음. 누군가 참조하고 있다면 bar 렉시컬 환경은 소멸하지 않는다.

9. foo 함수 코드 실행 종료

foo 함수 실행 컨텍스트가 pop 되어 제거되고, 전역 실행 컨텍스트가 실행 중인 실행 컨텍스트가 된다.

10. 전역 코드 실행 종료

마찬가지로 전역 실행 컨텍스트가 pop되어 실행컨텍스트 스택에는 아무것도 남아있지 않게 된다.

실행 컨텍스트와 블록 레벨 스코프

전역 소스 코드에서 블록문의 경우 변수가 선언되었을 시, 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 교체하고, 이때 이 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 전역 렉시컬 환경을 가리킨다.

이는 if 문뿐만 아니라 블록 레벨 스코프를 생성하는 모든 블록문에 적용된다.

for 문의 변수 선언문의 경우 코드 블록이 반복되어 실행될 때마다 코드 블록을 위한 새로운 렉시컬 환경을 생성한다. 이때 for문안에 정의 된 함수가 있다면 이 함수의 상위스코프는 for문의 코드 블록이 생성한 렉시컬 환경이다. 또한, 함수의 상위 스코프는 for문의 코드 블록이 반복되어서 실행될 때마다 식별자의 값을 유지해야하는데 이를 위해 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다.


24장 - 클로저

MDN에서는 클로저에 대해 다음과 같이 정의하고 있다.

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

예제

const x = 1;

function outerFunc() {
  const x = 10;
  
  function innerFunc() {
    console.log(x); // 10
  }
  
  innerFunc();
}

outerFunc();

innerFunc함수는 outerFunc의 렉시컬 환경을 참조하여 x 변수의 값을 참조하고 있고, 이는 자바스크립트가 렉시컬 스코프를 따르고 있기 때문에 가능하다 --> 함수가 어디서 정의 되었는 지를 기반으로 상위 스코프 결정.

렉시컬 스코프

함수를 어디에 정의했는지에 따라 상위 스코프를 결정 --> 렉시컬 스코프(정적 스코프)

렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다. 이것이 바로 렉시컬 스코프다.

함수 객체의 내부 슬롯 [[Environment]]

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

함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프이다. 또한 자신이 호출되었을 때, 생성될 함수의 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장될 참조값이다. 함수 객체는 상위 스코프를 자신이 존재하는 한 기억한다.

클로저와 렉시컬 환경

클로저의 대표적인 예시

const x = 1;

function outer() {
  const x = 10;
  const inner = function () { console.log(x); };
  return inner;
}

const innerFunc = outer();
innerFunc(); // 10

outer함수를 호출하면 inner를 반환하고 생명주기를 다하여 실행 컨텍스트에서 제거되는데 innerFunc() 호출 시, 결과로 10이 나오게 된다. 이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.
outer함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer함수의 렉시컬 환경 까지 소멸하는 것은 아니다.

즉 클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.

클로저에 의해 차몾되는 상위 스코프의 변수를 자유 변수라고 부른다.

클로저의 활용

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

const increase = (function () {
  let num = 0;
  
  // 클로저
  return function () {
    return ++num;
  };
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

이 예제에서 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출되어서 즉시 실행 함수의 렉시컬 환경을 상위 스코프로 가지고 있기 때문에 num에 접근하여 참조하고 변경할 수 있다.

생성자 함수 예제

const Counter = (function () {
  let num = 0;
  
  function Counter() {
  }
  
  Counter.prototype.increase = function() {
    return ++num;
  };
  
  Counter.prototype.decrease = function() {
    return num > 0 ? --num : 0;
  };
  
  return Counter;
}());

const counter = new Counter();

인스턴스의 프로퍼티가 아닌 즉시 실행 함수 내부에 num을 정의해두고 프로토 타입을 통해 increase, decrease 메서드를 상속받는 인스턴스를 생성한다. 이 메서드는 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경을 기억하는 클로저이어서 자유 변수 num을 참조할 수 있다.

함수형 프로그래밍에서 클로저를 활용하기 위해서는 일반 함수로 만들어서 함수를 전달받고 함수를 반환하는 고차 함수로 만들게 되면 보조 함수에 따라 여러번 호출할 수 있어서 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다. --> 카운터 증감이 연동이 되지 않을 수 있다.

따라서 독립된 카운터가 아닌 연동하여 증감이 가능한 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 한다.

const counter = (function() {
  let counter = 0;
  
  // 함수를 인수로 전달받는 클로저 반환
  return function (predicate) {
    // 인수로 전달받은 보조 함수에 상태 변경 위임
    counter = predicate(counter);
    return counter;
  };
}());

function increase(n) {
  return ++n;
}

function decrease(n) {
  return --n;
}

console.log(counter(increase)); // 1
console.log(counter(increase)); // 2

console.log(counter(decrease)); // 1
console.log(counter(decrease)); // 0

캡슐화와 정보 은닉

캡슐화: 프로퍼티와 메서드를 하나로 묶는 것. 보통 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도하는데 이를 정보 은닉이라고 한다.

생성자 함수안에 지역변수로 선언한 변수는 외부에서 참조하거나 변경할 수 없는데 이 변수를 참조하는 메서드를 프로토타입으로 만든 경우 보통 방법으론 이 변수에 접근을 할 수 없다. --> 즉시 실행함수를 사용하여 생성자 함수와 prototype에 정의할 메서드를 한곳에 모아서 선언.
하지만 이 방법도 생성자 함수가 여러 개의 인스턴스를 생성할 경우 그 변수의 상태가 유지 되지 않는다.(프로토타입 메서드가 단 한번 생성되는 클로저 이기 때문에 하나의 동일한 상위 스코프를 사용한다.)

JS는 이처럼 정보 은닉을 완전하게 지원하지는 않는다.

자주 발생하는 실수

클로저를 사용할 때 자주 발생하는 실수를 보여주는 예제다.

var funcs = [];

for (var i = 0; i<3; i++) {
  funcs[i] = function () {return i; };
}

for (var j = 0; j<funcs.length; j++) {
  console.log(funcs[j]());
}

결과로 0, 1, 2를 기대했지만 var 키워드로 선언한 i 변수가 함수레벨 스코프이기 때문에 전역 변수로 취급하여 i의 값 3이 3번 출력된다.

클로저를 활용하여 바르게 동작하는 코드

var funcs = [];

for (var i = 0; i< 3; i++) {
  funcs[i] = (function id) {
    return function () {
      return id;
    };
  }(i));
}

for (var j = 0; j < funcs.length; j++) {
  console.log(funcs[j]());
}

i를 인수로 받아 매개변수 id에 할당한 후 중첩 함수를 반환하고 종료된다. 매개 변수 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재하기 때문에 자유 변수가 되어 그 값이 유지된다.
이러한 방법 말고 그냥 let 키워드를 사용하면 번거로움이 없어진다.

let 키워드 사용 시 for 문의 코드 블록이 반복 실행 될 때마다 for문 코드 블록의 새로운 렉시컬 환경이 생성된다.

또다른 방법으로는 고차함수를 사용하는 방법이 있는데 이것은 나중에 살펴보도록 할 것


25장 - 클래스

클래스는 프로토타입의 문법적 설탕인가?

클래스와 생성자 함수의 차이

  • 클래스를 new 연산자 없이 호출하면 에러가 발생한다. 생성자 함수는 new 없이 호출해도 일반 함수로 호출됨
  • 클래스는 상속을 지원하는 extends와 super 키워드를 제공한다.
  • 클래스는 let과 const처럼 호이스팅이 발생하지 않는 것 처럼 동작한다.
  • 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되면 해제할 수 없다.
  • 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]] 값이 모두 false다.

클래스를 프로토타입 기반 객체 생성 패턴의 단순한 문법적 설탕이라고 보기보다는 새로운 객체 생성의 메커니즘으로 보는 것이 좀 더 합당하다.

클래스의 정의

// 클래스 선언문
class Person {}

// 익명 클래스 표현식
const Person = class {};

// 기명 클래스 표현식
const Person = class MyClass {};

클래스를 표현식으로 정의한다는 것 --> 일급 객체
일급객체의 특징: 무명의 리터럴로 생성, 변수나 자료구조에 저장, 함수의 매개변수에 전달, 함수의 반환값으로 사용할 수 있다.

클래스 몸체에는 constructor, 프로토타입 메서드, 정적 메서드 이 3 가지의 메서드만 정의 할 수 잇다.

class Person {
  constructor (name) {
    this.name = name;
  }
  
  sayHi() {
    console.log(`Hi! My name is ${this.name}`);
  }
  
  static sayHello() {
    console.log('Hello!');
  }
}

const me = new Person('Lee');

console.log(me.name);
me.sayHi();
Person.sayHello();

클래스 호이스팅

클래스는 클래스 정의 이전에 참조할 수 없다. --> let, const 키워드로 선언한 변수 처럼 호이스팅 된다.(호이스팅 되지 않는 것처럼 동작, 일시적 사각지대 존재)

인스턴스 생성

반드시 new 연산자와 함께 호출해야한다.

클래스 표현식으로 만들어진 클래스인 경우 기명 클래스 표현식의 클래스 이름이 아닌 식별자를 사용해 인스턴스를 생성해야 한다.

메서드

constructor

인스턴스를 생성하고 초기화하기 위한 특수한 메서드

클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 다르다.
클래스의 constructor는 최대 1개만 존재할 수 있으며 생략할수도 있다. 생략한 경우 빈 constructor가 암묵적으로 정의되고, 이는 빈 객체를 생성한다.

인스턴스를 초기화하려면 constructor를 생략해서는 안되고 초기값을 매개변수로 전달해서 사용한다.
return문을 반드시 생략해야한다.

프로토타입 메서드

클래스 몸체에서 메서드 축약 표현으로 정의하면 프로토타입 메서드가 된다.

정적 메서드

클래스 몸체에서 static 키워드를 붙이면 정적 메서드가 된다.

정적 메서드와 프로토타입 메서드의 차이

  1. 자신이 속해 있는 프로토타입 체인이 다르다.
  2. 정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.
  3. 정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.

표준 빌트인 객체 Math, Number, JSON, Object, Reflect 또한 다양한 정적 메서드를 가지고 있고 이처럼 클래스 또는 생성자 함수를 하나의 네임스페이스로 사용하여 정적 메서드를 모아 놓으면 이름 충돌 가능성을 줄여주고 관련 함수들을 구조화할 수 있는 효과가 있다.

클래스에서 정의한 메서드의 특징

  1. function 키워드를 생략한 메서드 축약 표현을 사용한다.
  2. 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없다.
  3. 암묵적으로 strict mode로 실행된다.
  4. for...in 문이나 Object.keys 메서드 등으로 열거할 수 없다. ([[Enumerable]] 값이 false)
  5. 내부 메서드 [[Construct]]를 갖지 않는 non-constructor다. 따라서 new 연산자와 함께 호출할 수 없다.

클래스의 인스턴스 생성 과정

  1. 인스턴스 생성과 this 바인딩 (빈 객체 생성하는데 이것이 클래스가 생성한 인스턴스)

  2. 인스턴스 초기화

  3. 인스턴스 반환

프로퍼티

인스턴스 프로퍼티

인스턴스 프로퍼티는 언제나 constructor 내부에서 정의해야한다.

접근자 프로퍼티

getter 함수와 setter 함수가 있으며 클래스의 메서드가 기본적으로 프로토타입 메서드가 되므로 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.

이 두 접근자 프로퍼티의 이름은 인스턴스 프로퍼티처럼 사용되는데, 호출하는 것이 아닌 참조하는 형식으로 사용된다.

클래스 필드 정의 제안

class Person {
  // 클래스 필드 정의
  name = 'Lee';
}

const me = new Person('Lee');

클래스 몸체에서 클래스 필드를 정의할 수 있는 이 제안은 아직 정식 표준사양은 아니지만 미리 몇몇 최신 브라우저와 Node.js에서 구현해 놓아서 사용할 수 있다.
원래는 인스턴스 프로퍼티를 참조하려면 this를 사용하여 참조를 해야하지만 이 클래스 필드는 this를 생략해도 참조 가능

this에 클래스 필드를 바인딩 해서는 안되며 this는 클래스의 constructor와 메서드 내에서만 유효하다.

클래스 필드에 메서드또한 정의할 수 있는데 이 메서드는 인스턴스 프로퍼티가 된다.
모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문에 따라서 클래스 필드에 함수를 할당하는 것은 권장하지 않는다.

private 필드 정의 제안

#을 붙여서 private 필드를 참조할 수 있다.

class Person {
  #name = ' ';
  
  constructor(name) {
    this.#name = name;
  }
}

반드시 클래스 몸체에 정의하며, constructor에 정의하면 에러가 발생한다.

static 필드 정의 제안

static 키워드를 사용하여 정적 필드를 정의한다.

class MyMath {
  // static public 필드 정의
  
  static PI = 22/7;

  // static private 필드 정의
  static #num = 10;
  
  // static 메서드
  static increment() {
    return ++MyMath.#num;
  }
}

console.log(MyMath.PI);
console.log(MyMath.increment()); // 11

상속에 의한 클래스 확장

클래스 상속과 생성자 함수 상속

상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.

extends 키워드

상속을 통해 확장하려면 extends 키워드를 사용하여 클래스를 정의한다.

서브 클래스: 상속을 통해 확장된 클래스
수퍼 클래스: 서브클래스에게 상속된 클래스

동적 상속

extends 키워드는 클래스뿐만 아니라 생성자 함수를 상속받아 클래스 확장이 가능하다. 클래스뿐만 아니라 [[Consturct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식 사용이 가능하다.

이를 통해 동적으로 상속 받는 대상을 결정할 수 있다.

서브클래스의 constructor

서브클래스에서 constructor를 생략하면 다음과 같이 암묵적으로 정의된다.

constructor(...arge) { super(...args); }

super 키워드

super 키워드는 함수처럼 호출할 수도 있고 this와 같이 식별자처럼 참조할 수 있는 특수한 키워드다.

  • super를 호출하면 수퍼클래스의 constructor를 호출한다.
  • super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.

super 호출

super를 호출하면 수퍼클래스의 constructor를 호출한다.

수퍼클래스에서 추가한 프로퍼티와 서브클래스에서 추가한 프로퍼티를 갖는 인스턴스를 생성한다면 constructor를 생략할 수 없다. 따라서 이경우 서브클래스의 constructor에는 반드시 super를 호출해야 한다.

서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다.

super는 반드시 constructor에서만 호출한다.

super 참조

  1. 메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.

서브클래스의 메서드 내에서의 super.메서드는 수퍼클래스의 프로토타입 메서드를 가리킨다.

super 가 아닌 getPrototypeOf로 super를 정의하여 수퍼클래스의 메서드를 호출할 경우, call(this)를 붙여서 사용해야 한다. 왜냐하면 프로토타입 메서드이기 때문에 this는 인스턴스를 가리켜야 하기 때문이다.

따라서 메서드는 내부 슬롯 [[HomeObject]]를 가지며 이는 자신을 바인딩하고 있는 객체를 가리킨다.

super참조 의사코드

super = Object.getPrototypeOf([[HomeObject]])

ES6 메서드 축약표현으로 정의된 함수만이 [
[HomeObject]]를 갖는다.

클래스 뿐만 아니라 객체 리터럴에서 메서드 축약표현으로 정의된 함수또한 super 참조를 사용할 수 있다.

  1. 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.

상속 클래스의 인스턴스 생성 과정

Rectangle 클래스와 이를 확장한 ColorRectangle 클래스를 가지고 인스턴스 생성과정을 지켜보자

  1. 서브 클래스 super 호출

JS엔진은 수퍼클래스와 서브클래스를 구분하기 위해 'base' 또는 'derived' 값을 갖는 내부 슬롯 [[ConstructorKind]]를 갖는다. 상속받는 서브 클래스를 derived로, 상속받지 않는 클래스는 base로 설정된다.

new 연산자와 함게 호출될때, 서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임하한다. 이것이 바로 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유다.

  1. 수퍼클래스의 인스턴스 생성과 this 바인딩

new 연산자와 함께 호출된 클래스가 서브클래스 이기 때문에 실질적으로는 colorRectangle가 생성할 인스턴스에 this가 바인딩 된다.

  1. 수퍼클래스의 인스턴스 초기화

  2. 서브클래스 constructor로의 복귀와 this 바인딩

super가 반환한 인스턴스가 this에 바인딩 된다. 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다.

  1. 서브클래스의 인스턴스 초기화

  2. 인스턴스 반환

표준 빌트인 생성자 함수 확장

[[Constructor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 extends 다음 키워드로 사용할 수 있기 때문에 String, Number, Array 같은 표준 빌트인 객체도 확장할 수 있다.

Array를 확장하여 사용하는 경우 Array.prototype의 모든 메서드들을 사용할 수 있고 이를 통해 반환하는 메서드가 클래스가 MyArray라면 이 클래스의 인스턴스를 반환한다.

이를 통해 메서드 체이닝이 가능하며 만약 MyArray 클래스의 메서드가 인스턴스가 아닌 Array가 생성한 인스턴스를 반환하게 하려면 Symbol.species를 사용하여 정적 접근자 프로퍼티를 추가한다.

class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
  
  uniq() {
    return this.filter((v, i , self) => self.indexOf(v) === i);
  }
  
  average() {
    return this.reduce((pre, cur) => pre + cur, 0 ) / this.length;
  }
}

수업 시간 필기

22장

Apply, call은 첫번째로 하는 일이 함수 호출, 두 번째로 this 바인딩

Call은 인수 전달 시 펼쳐서, apply는 인수 전달 시 배열로 묶어서

화살표함수는 this가 없다. --> this를 찾으러 상위 스코프를 봄

this를 되도록이면 안쓰는 것이 좋다.


23장

4가지 소스코드 타입만 실행컨텍스트 생성!

그 중 2개만 보면 댐 (전역, 함수 코드)

대부분 프로그래밍 언어는 실행컨텍스트(심볼테이블)를 가지고 있음

변수 선언 --> 식별자를 실행 컨텍스트에 등록하는 것

식별자는 현재 실행 중인 실행 컨텍스트에서 찾는다.

전역 코드 평가가 시작되면 ast로 코드를 분석하여 위에서부터 시작
Const x = 1 --> 실제로는 ast에 2줄로 들어감. --> Const x; x = 1;

객체환경레코드의 bindingObject가 연결되어서 관리되는 이유 --> 환경이 달라지는 경우가 있기 때문에! (브라우저냐 노드제이에스냐)
각 환경에 따라 전역 객체를 연결해줌 브라우저 - window, 노드 제이에스 - global

실제로 window객체는 가장 먼저 존재함(실행 컨텍스트 스택에 무엇인가가 쌓이기도 전에)

반복문과 같이 실행컨텍스트를 생성하지 않는 코드라도 렉시컬 환경을 만들 수 있음(현재 실행중인 실행컨텍스트가 그것을 참조하다가 끝나면 다시 자신이 만든 렉시컬환경을 참조함)


24장

클로저는 함수다.

모든 함수는 클로저이다.

함수가 [[environment]]를 들고있는 이유는 외부 렉시컬환경에 대한 참조를 하기 위해!

Debugger --> 디버깅할 때 거기서 브레이크가 걸림

대부분 클로저를 만들 때 즉시실행함수로 감쌈 --> 하나의 변수를 여러개의 함수가 공유하기 위해서

캡슐화 --> 프로퍼티와 메서드를 하나로 묶고
정보은닉 --> 캡슐화가 되어 있는 프로퍼티와 메서드 중에 감출 목적으로 사용하기 위해 숨겨둠

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

결합도(다른 무언가에 의존하느냐)는 낮을수록, 응집도는 높을 수록 좋다.

프로퍼티를 보고, 쓸 수 있으면 결합도 높고
프로퍼티를 못보고 쓸 수 없으면 결합도 낮음.

결합도와 정보 은닉이 많이 밀접함 --> 정보 은닉이 많아지면 결합도 낮아짐

Person.prototype을 Person 생성자 함수 안에 넣으면 그냥 실행될 때마다 계속 생기므로 밖으로 빼면 age 변수를 못보게됨 --> 모듈 패턴을 사용(즉시 실행 함수) --> 응집도를 높이기 위해 즉시 실행 함수를 사용함

그래도 문제 발생 --> 정보은닉이 자바스크립트에서 불가능

불행의 시작 --> 자바스크립트를 자바와 같이 객체지향처럼 사용하려고 할때 많은 문제 발생!

실행 컨텍스트의 관점에서 클로저

실행 컨텍스트 스택에서 pop된 후에도 실행 컨텍스트 외의 다른 식별자에 의해 렉시컬환경이 참조되고 있어 렉시컬환경이 살아 있는 현상이 일어나는데, 이 때 해당 렉시컬 환경을 참조하는 함수를 클로저라 한다.


25장

클래스 메서드 --> super 가능

모듈 내부 --> strict mode (자동적으로)

Typeof class -> function (함수와 똑같이 호이스팅을 하지만 호이스팅을 하지 않는 것처럼 동작)

클래스는 함수다.

정적메서드로 만들 수 있으면 이게 더 안전(프로토타입 메서드보다)

표준빌트인 객체에서 생성자 함수가 아닌 Math.Json,Reflect 등등 에서 정적 메서드로 묶은 이유는 --> 응집도를 높이기 위해

클래스를 사용하는 경우 --> 생성자 함수를 사용하는 경우

강사님은 자료구조 만들때만 사용한다고 함(스택, 큐, 트라이)

Super 호출 --> 수퍼클래스의 constructor 호출

Super 참조 --> 자신 상위 수퍼클래스 메서드 참조

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글