Published on

코어자바스크립트

Authors
  • avatar
    Name
    Na Hyunwoo
    Twitter

이 글은 코어자바스크립트 책을 읽은 후 목차 별 핵심 부분만 발췌하였습니다.

이는 복습에 용이하게 하기 위함 입니다.


목차



1. 데이터 타입

1.1 - 데이터 타입의 종류

데이터 타입에는 기본형과 참조형으로 크게 두 가지가 있습니다.

기본형에는 숫자, 문자열, 불리언, null, undefined, symbol등이 있습니다.

참조형에는 객체, 배열, 함수, 날짜, 정규표현식, Map, WeakMap, Set, WeakSet 등이 있습니다.

기본형은 할당이나 연산시 복제되고 참조형은 참조됩니다.

기본형은 불변성을 띕니다.

1.2 - 데이터 타입에 관한 배경지식

1.2.1 - 메모리와 데이터

0 또는 1만 표현할 수 있는 하나의 메모리 조각을 비트라고 합니다. 각 비트는 고유한 식별자를 통해 위치를 확인할 수 있습니다.

1바이트는 8개의 비트로 구성돼 있습니다.

자바스크립트는 숫자의 경우 정수형인지 부동소수형인지를 구분하지 않고 64비트, 즉 8바이트를 확보합니다.

모든 데이터는 바이트 단위의 식별자, 더 정확하게는 메모리 주솟값을 통해 서로 구분하고 연결할 수 있습니다.

1.2.2 - 식별자와 변수

변수는 변할 수 있는 수 입니다.

식별자는 어떤 데이터를 식별하는 데 사용하는 이름, 즉 변수명입니다.

1.3 - 변수 선언과 데이터 할당

1.3.1 - 변수 선언

변수란 결국 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇입니다.

1.3.2 - 데이터 할당

var a = 'abc'
invariant

변수 영역에 값을 직접 대입하지 않고 굳이 번거롭게 한 단계를 더 거치는 이유는 다음과 같습니다.

  1. 데이터 변환을 자유롭게 할 수 있다.
  2. 메모리를 더욱 효율적으로 관리할 수 있다.

기존 문자열에 어떤 변환을 가하든 상관 없이 무조건 새로 만들어 별도의 공간에 저장합니다.

1.4 - 기본형 데이터와 참조형 데이터

1.4.1 - 불변값

불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역 메모리입니다.

기본형 데이터인 숫자, 문자열, boolean, null, undefined, Symbol은 모두 불변값입니다.

한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않습니다.

1.4.2 - 가변값

기본형 데이터와의 차이는 ‘객체의 변수(프로퍼티) 영역’이 별도로 존재한다는 점입니다.

데이터 영역에 저장된 값은 모두 불변값입니다. 그러나 변수에는 다른 값을 얼마든지 대입할 수 있습니다.

아래 사진을 통해 쉽게 이해할 수 있습니다.

variable-value

1.4.3 - 변수 복사 비교

변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일합니다.

복사 과정은 동일하지만 데이터 할당 과정에서 이미 차이가 있기 때문에 변수 복사 이후의 동작에도 큰 차이가 발생합니다.

사실은 어떤 데이터 타입이든 변수에 할당하기 위해서는 주솟값을 복사해야 하기 때문에, 엄밀히 따지면 자바스크립트의 모든 데이터 타입은 참조형 데이터일 수밖에 없습니다. 다만 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있는 것입니다.

참조형 데이터가 ‘가변값’이라고 설명할 때의 ‘가변’은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립합니다.

1.5 - 불변 객체

1.5.1 - 불변 객체를 만드는 간단한 방법

참조형 데이터의 ‘가변’은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립합니다. 데이터 자체를 변경하고자 하면(새로운 데이터를 할당하고자 하면) 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않습니다.

불변 객체를 만드는 간단한 방법은 새로운 참조형 데이터를 return하는 함수를 만들고, 해당 함수를 통해서만 복사하길 합의하는 것입니다.

1.5.2 - 얕은 복사와 깊은 복사

얕은 복사는 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사는 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법입니다.

얕은 복사만 수행한다는 것의 의미는 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사한다는 의미입니다. 그러면 해당 프로퍼티에 대해 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 됩니다.

기본형 데이터일 경우에는 그대로 복사하면 되지만, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사해야 합니다.

깊은 복사의 방법에는 여러 방법 재귀를 통한 복사와 같은 여러 방법이 있지만, 간단하게 JSON을 활용하여 깊은 복사를 처리하는 방법이 있습니다.

var copyObjectViaJSON = function (target) {
  return JSON.parse(JSON.stringify(target))
}

이해가 조금 안되는게, 깊은 복사를 한다고 해서 왜 중첩된 객체의 프로퍼티를 바꾼 뒤에 값을 비교했을 때 false가 되는거지 ? 그야 중첩된 객체가 같은 주솟값을 바라보고 있으니까 그렇지.

중첩된 객체를 얕은 복사하게 되면 참조형 데이터를 가지는 프로퍼티는 원본 데이터 프로퍼티의 주솟값을 참조하게 되니까 문제가 생기는거다. 어떻게 보면 연결이 되는거지.

얕은 복사는 결국에 새로운 객체를 반환하는 것이 나에게는 이해가 되는 포인트였다.

그러면 결국 중첩된 객체를 복사하려면 깊은 복사를 해야한다는 의미이다.

1.6 - undefined와 null

자바스크립트에는 ‘없음’을 나타내는 값이 두 가지가 있습니다. 바로 undefined와 null입니다.

‘비어있음’을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 쓰면 됩니다.

이렇게 하면 undefined는 오직 ‘값을 대입하지 않은 변수에 접근하고자 할 때 자바스크립트 엔진이 반환해주는 값’으로서만 존재할 수 있겠죠.

1.7 - 정리

자바스크립트 데이터 타입에는 크게 기본형과 참조형이 있습니다. 기본적으로 기본형은 불변값이고 참조형은 가변값입니다.

변수는 변경 가능한 데이터가 담길 수 있는 공간이고, 식별자는 그 변수의 이름을 말합니다.

참조형 데이터는 깊은 복사를 통해 불변값으로 사용할 수 있습니다.

‘없음’을 나타내는 값은 1. 어떤 변수에 값이 존재하지 않을 때를 의미하는 undefined 와 2. 사용자가 명시적으로 ‘없음’을 표현하기 위한 null로 두가지가 있습니다.


2. 실행 컨텍스트

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로, 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념입니다.

자바스크립트는 어떤 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행합니다.

2.1 - 실행 컨텍스트란 ?

실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것뿐입니다.

처음 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담깁니다. 최상단의 공간은 코드 내부에서 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하므로 자바스크립트 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 이해하면 됩니다.

스택 구조를 잘 생각해보면 한 실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점임을 알 수 있습니다.

이렇게 어떤 실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장합니다.

이 객체는 자바스크립트 엔진이 활용할 목적으로 생성할 뿐 개발자가 코드를 통해 확인할 수는 없습니다. 여기에 담기는 정보들은 다음과 같습니다.

  • VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보. 선언 시점의 LexicalEnvironment의 스냅샷으로, 변경 사항은 반영되지 않는다.
  • LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.
  • ThisBinding: this 식별자가 바라봐야 할 대상 객체.

2.2 - VariableEnvironment

VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 다릅니다.

실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 됩니다.

VariableEnvironment와 LexicalEnvironment의 내부는 environmentRecord와 outer-EnvironmentReference로 구성돼 있습니다.

2.3 - LexicalEnvironment

컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것입니다.

2.3.1 - environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.

environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없습니다.

변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면 함수 선언은 함수 전체를 끌어올립니다.

함수 선언문과 함수 표현식

함수 선언문은 function 정의부만 존재하고 별도의 할당 명령이 없는 것을 의미하고, 반대로 함수 표현식은 정의한 function을 별도의 변수에 할당하는 것을 말합니다.

함수 선언문은 전체를 호이스팅한 반면 함수 표현식은 변수 선언부만 호이스팅합니다.

함수를 다른 변수에 값으로써 ‘할당’한 것이 곧 함수 표현식입니다.

2.3.2 - 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위입니다.

ES5까지의 자바스크립트는 특이하게도 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성됩니다. ES6에서는 블록에 의해서도 스코프 경계가 발생하게 함으로써 다른 언어와 훨씬 비슷해졌습니다.

‘식별자의 유효범위’를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인이라고 합니다. 그리고 이를 가능케 하는 것이 바로 LexicalEnvironment의 두 번째 수집 자료인 outerEnvironmentReference입니다.

스코프 체인

outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조합니다. 따라서, 이는 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능합니다.

이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 됩니다.

함수 내부에서 a 변수를 선언했다면, 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없습니다. 이를 변수 은닉화라고 합니다.

전역변수와 지역변수

전역 공간에서 선언한 변수는 전역변수이고, 함수 내부에서 선언한 변수는 지역변수입니다.

2.4 - this

실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장됩니다. 실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 this에는 전역 객체가 저장됩니다. 그밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다릅니다.

2.5 - 정리

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체입니다. 실행 컨텍스트 객체는 활성화되는 시점에 VariableEnvironment, LexicalEnvrionment, ThisBinding의 세 가지 정보를 수집합니다.

실행 컨텍스트를 생성할 때 VariableEnvironment와 LexicalEnvironment가 동일한 내용으로 구성되지만 LexicalEnvironment는 함수 실행 도중에 변경되는 사항이 즉시 반영되지만, VariableEnvironment는 초기 상태를 유지합니다. VariableEnvironment와 LexicalEnvironemnt는 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 environemntRecord와 바로 직전 컨텍스트의 LexicalEnvironemnt 정보를 참조하는 outerEnvironmentReference로 구성돼 있습니다.

호이스팅은 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 “끌어올린다”고 해석하는 것입니다.

변수 선언과 값 할당이 동시에 이뤄진 문장은 ‘선언부’만을 호이스팅하고, 할당 과정은 원래 자리에 남아있습니다. 여기서 함수 선언문과 함수 표현식의 차이가 발생합니다.

스코프는 변수의 유효범위를 의미합니다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironment를 참조합니다. 코드 상에서 어떤 변수에 접근하려고 하면 현재 컨텍스트의 LexicalEnvironemnt를 탐색해서 발견되면 그 값을 반환하고, 발견하지 못할 경우 다시 outerEnvironmentReference에 담긴 LexicalEnvironment를 탐색하는 과정을 거칩니다.

전역 컨텍스트의 LexicalEnvironment에 담긴 변수를 전역변수라 하고, 그 밖의 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수입니다.

this에는 실행 컨텍스트를 활성화하는 당시에 지정된 this가 지정됩니다. 함수를 호출하는 방법에 따라 그 값이 달라지는데, 지정되지 않은 경우에는 전역 객체가 저장됩니다.


3. this

다른 대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미합니다. 클래스에서만 사용할 수 있기 때문에 혼란의 여지가 없거나 많지 않습니다. 그러나 자바스크립트에서의 this는 어디서든 사용할 수 있습니다. 상황에 따라 this가 바라보는 대상이 달라지는데, 어떤 이유로 그렇게 되는지를 파악하기 힘든 경우가 많습니다.

3.1 - 상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됩니다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, 바꿔 말하면 this는 함수를 호출할 때 결정된다고 할 수 있겠습니다.

3.1.1 - 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킵니다.

자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작합니다.

특정 객체란 바로 실행 컨텍스트의 LexicalEnvironment입니다.

이는 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다는 의미입니다.

var a = 1
console.log(a) // 1
console.log(window.a) // 1
console.log(this.a) // 1

여기서 a를 호출해도 1이 나오는 이유는, 변수 a에 접근하고자 하면 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프의 LexicalEnvironment에서 해당 프로퍼티 a를 발견해서 그 값을 반환하기 때문입니다.

var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보입니다.

3.1.2 - 메서드로서 호출할 때 그 메서드 내부에서의 this

함수 vs 메서드

어떤 함수를 실행하는 대표적인 두 가지 방법은 1. 함수로서 호출 2. 메서드로서 호출이 있습니다.

이 둘을 구분하는 유일한 차이는 독립성에 있습니다.

함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행합니다.

어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작합니다.

‘함수로서 호출’과 ‘메서드로서 호출’은 함수 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있습니다.

메서드 내부에서의 this

어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체입니다.

3.1.3 - 함수로서 호출할 때 그 함수 내부에서의 this

함수 내부에서의 this

함수에서의 this는 전역 객체를 가리킵니다.

메서드 내부함수에서의 this

내부함수 역시 이를 함수로서 호출했는지 메서드로서 호출했는지만 파악하면 this의 값을 정확히 맞출 수 있습니다.

메서드의 내부 함수에서의 this를 우회하는 방법

self라는 변수에 this를 저장한 상태에서 호출한 innerFunc2의 경우 self에는 객체 obj가 출력됩니다.

var obj = {
  outer: function () {
    var self = this
    var innerFunc2 = function () {
      console.log(self) // obj
    }
    innerFunc2()
  },
}
obj.outer()

this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수를 새로 도입했습니다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있습니다. 내부 함수를 화살표 함수로 바꾸면 3-1-3-3절의 ‘우회법’이 불필요해집니다.

3.1.4 - 콜백 함수 호출 시 그 함수 내부에서의 this

함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 합니다.

콜백 함수도 함수이기 때문에 기본적으로 3-1-3절에서와 마찬가지로 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

콜백 함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 무엇으로 할지를 결정하며, 특별히 정의하지 않은 경우에는 기본적으로 함수와 마찬가지로 전역객체를 바라봅니다.

3.1.5 - 생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수입니다.

객체지향 언어에서는 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라고 합니다.

프로그래밍적으로 “생성자”는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다.

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했습니다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 됩니다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됩니다.

3.2 - 명시적으로 this를 바인딩하는 방법

3.2.1 - call 메서드

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령입니다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 합니다.

3.2.2 - apply 메서드

apply 메서드는 call 메서드와 기능적으로 완전히 동일합니다. call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있습니다.

3.2.3 - call / apply 메서드의 활용

유사배열객체에 배열 메서드를 적용

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
}
Array.prototype.push.call(obj, 'd')
console.log(obj) // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 };

객체에는 배열 메서드를 직접 적용할 수 없습니다. 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있습니다.

ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했습니다.

생성자 내부에서 다른 생성자를 호출

생성자 내부에서 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있습니다.

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 좋습니다.

그러나, ES6에서는 **펼치기 연산자(spread operator)**를 이용하면 apply를 적용하는 것보다 더욱 간편하게 작성할 수 있습니다.

3.2.4 - bind 메서드

bind 메서드는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드입니다.

bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닙니다.

name 프로퍼티

name 프로퍼티에 동사 bind의 수동태인 ‘bound’라는 접두어가 붙습니다.

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

메서드의 내부함수에서 메서드의 this를 그대로 바라보게 하기 위해 call, apply 또는 bind 메서드를 활용할 수 있습니다.

3.2.5 - 화살표 함수의 예외사항

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐습니다. 따라서, this에 접근하고자 하면 스코프체인상 가장 가까운 this에 접근합니다. 이를 사용하면 별도의 변수로 this를 우회하거나 call/apply/bind를 적용할 필요가 없습니다.

3.2.6 - 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있습니다. 이와 같은 예시로는 다음과 같은 메서드가 있습니다.

forEach, map, filter, some, every, fine, findIndex, flatMap, from 등등

3.3 - 정리

명시적 바인딩이 없는 경우

  • 전역공간에서의 this는 전역객체를 참조합니다.
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다.
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드의 내부함수에서도 같습니다.
  • 콜백함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.

명시적 바인딩이 있는 경우

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.

4. 콜백 함수

4.1 - 콜백 함수란 ?

콜백 함수는 다른 코드의 인자로 넘겨주는 함수입니다.

B는 알람시계를 세팅합니다. 시계가 정한 시각에 울리지 않을 염려는 없고 평소 알람소리에 쉽게 눈을 뜨곤 했던지라 안심하고 꿀잠을 잡니다. B는 시계 함수에게 요청을 하면서 알람을 울리는 명령에 대한 제어권을 시계에게 넘겨준 것이죠. 이처럼 콜백 함수는 제어권과 관련이 깊습니다.

어떤 함수 X를 호출하면서 ‘특정 조건일 때 함수 Y를 실행해서 나에게 알려달라’는 요청을 함께 보내는 거죠. 이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출합니다.

이처럼 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수입니다.

4.2 - 제어권

4.2.1 - 호출 시점

콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가집니다.

4.2.2 - 인자

콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가집니다.

4.2.3 - this

콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

4.3 - 콜백 함수는 함수다

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(this, v, i)
  },
}
obj.logValues(1, 2) // { vals: [1, 2, 3], logValues: f } 1 2
;[4, 5, 6].forEach(obj.logValues) // Window { ... } 4 0 ...

어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐입니다.

4.4 - 콜백 함수 내부의 this에 다른 값 바인딩하기

전통적으로 해결하는 여러 방식이 있지만, ES5에서 bind 메서드가 등장함에 따라, 손쉽게 해결할 수 있습니다.

var obj = {
  name: 'obj1',
  func: function () {
    console.log(this.name)
  },
}
setTimeout(obj1.func.bind(obj1), 1000)

4.5 - 콜백 지옥과 비동기 제어

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 뜻합니다.

동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식입니다. 반대로 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어갑니다.

별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드입니다.

콜백 지옥을 해결하기 위해서 ES6에서는 Promise, Generator 등이 도입됐고 ES2017에서는 async/await가 도입됐습니다.

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행합니다.

4.6 - 정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수입니다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가집니다.
    1. 콜백 함수를 호출하는 시점을 스스로 판단해서 실행합니다.
    2. 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있습니다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 됩니다.
    3. 콜백 함수의 this가 무엇을 바라보돍 할지가 정해져 있는 경우도 있습니다. 정하지 않은 경우에는 전역객체를 바라봅니다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 됩니다.
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됩니다.
  • 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉽습니다. 이를 Promise, Generator, async/await 등을 통해 해결할 수 있습니다.

5. 클로저

5.1 - 클로저의 의미 및 원리 이해

클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상입니다.

가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다. 이러한 가비지 컬렉터의 동작 방식으로 인해 함수에 선언된 변수를 참조하는 내부 함수를 외부로 전달하려 할 때, 내부 함수의 outerEnvironmentReference는 함수의 LexicalEnvironment를 소멸되지 않은 상태로 가지고 있을 수 있습니다.

“어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상”이란 “외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상”을 뜻합니다.

결국, 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우 함수의 실행 컨텍스트가 종료된 이후에도 변수가 사라지지 않는 현상을 뜻합니다.

다만, 외부로의 전달이 return만을 의미하지는 않습니다. 이는 콜백 함수 혹은 eventListener에서 종종 볼 수 있습니다.

근데 만약에 a를 리턴하지 않으면 어떻게 될까 ? 참조는 하는데 return이 없는거지. 그러면 GC한테 수거될 것 같아. 무엇보다 리턴하지 않으면 해당 변수가 살아있는지 확인할 방도도 없어. 그래서 내가 생각했을 때는 내가 말한 상황은 클로저가 아닐거라고 생각돼.

아니야, 그냥 그 함수에서 참조하기만 하면 모두 남아있는거야. 그걸 return하지 않으면 private해지는 거고, 그걸 return 하면 public 해지는 거지.

5.2 - 클로저와 메모리 관리

클로저는 메모리 누수의 위험이 있습니다. 이를 관리하기 위해서는 필요성이 사라진 시점에 더는 메모리를 소모하지 않게 해주면 됩니다. 이를 위해 식별자에 기본형 데이터(보통 null, undefined)를 할당하면 됩니다.

var outer = (function () {
  var a = 1
  var inner = function () {
    return ++a
  }
  return inner
})()
console.log(outer())
outer = null

5.3 - 클로저 활용 사례

5.3.1 - 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

  1. 콜백 함수를 내부함수로 선언해서 외부변수를 직접 참조하는 방법
  2. bind 메서드 사용
  3. 콜백 함수를 고차함수로 교체 (고차 함수란 함수를 인자로 받거나 함수를 리턴하는 함수)
var alertFruitBuilder = function (fruit) {
  return function () {
    alert('your choice is ' + fruit)
  }
}
fruits.forEach(function (fruit) {
  var $li = document.createElement('li')
  $li.innerText = fruit
  $li.addEventListener('click', alertFruitBuilder(fruit))
  $ul.appendChild($li)
})

이렇게 하는 이유는, 콜백 함수에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 ‘이벤트 객체’를 주입하기 때문입니다.

5.3.2 - 접근 권한 제어(정보 은닉)

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나입니다.

클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능합니다.

closure라는 영어 단어는 사전적으로 ‘닫혀있음, 폐쇄성, 완결성’ 정도의 의미를 가집니다.

외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 접근 제어가 가능한 것입니다. return한 변수들은 공개멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 되는 것이죠.

클로저를 활용해 접근 권한을 제어하는 방법

  1. 함수에서 지역변수 및 내부함수 등을 생성한다.
  2. 외부에 접근 권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return한다. → return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됩니다.

5.3.3 - 부분 적용 함수

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 n-m개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수입니다. bind 메서드의 실행 결과가 바로 부분 적용 함수입니다.

디바운스 예시는 아래와 같습니다.

var debounce = function (eventName, func, wait) {
  var timeoutId = null
  return function (event) {
    var self = this
    console.log(eventName, 'event 발생')
    clearTimeout(timeoutId)
    timeoutId = setTimeout(func.bind(self, event), wait)
  }
}

var moveHandler = function (e) {
  console.log('move event 처리')
}

var wheelHandler = function (e) {
  console.log('wheel event 처리')
}

document.body.addEventListener('mousemove', debounce('move', moveHandler, 500))
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700))
  • 이 디바운스 함수에서 return하는 function의 event 인자는 어디서 어떻게 받는 것일까 ?
    • 이 event 인자는 addEventListener에서 호출하는 event이다.
  • wait 시간이 경과하기 이전에 다시 동일한 event가 발생하면 이번에는 6번째 줄에 의해 앞서 저장했던 대기열을 초기화하고, 다시 7번째 줄에서 새로운 대기열을 등록합니다.
  • 이제 debounce를 짜라고 하면 짤 수 있겠는데, 만약 온라인 코딩 테스트에서 debounce를 짜라고 했을 때, 이걸 설명하면서 짜라고 하면 설명을 못하겠음.
  • 한번 설명을 해보자.
  • debounce라는 함수는 여러 요청이 동시에 들어왔을 때, 마지막 요청만 실행하는 함수를 의미합니다. 이를 구현하기 위해서는 setTimeout과, clearTimeout을 잘 이용해야 합니다. 코드는 다음과 같습니다.
  • 참고로, 이는 addEventListener에 사용되기 위한 debounce 입니다.
function debounce(this, func, waitTime) {
	let timeoutId = null;
	return function(event) {
		clearTimeout(timeoutId);
		timeoutId = setTimeout(func.bind(this, event), waitTime);
	}
}
  • 그럼 debounce hook을 짜라고 하면 어떻게 될까 ?
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay || 500)

    return () => {
      clearTimeout(timer)
    }
  }, [value, delay])

  return debouncedValue
}

생각보다 쉬운데 ? setTimeout만 사용할 줄 알면 되겠어.

5.3.4 - 커링 함수

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다.

앞의 부분 적용 함수와 기본적인 맥락은 일치하지만, 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 하는 것이 다릅니다.

중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않습니다.

예제 1

var curry3 = function (func) {
  return function (a) {
    return function (b) {
      return func(a, b)
    }
  }
}

var getMaxWith10 = curry3(Math.max)(10)
getMaxWith10(8) // 10
getMaxWith10(25) // 25

예제 2

var curry5 = (func) => (a) => (b) => (c) => (d) => (e) => func(a, b, c, d, e)

각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 GC되지 않고 메모리에 차곡차곡 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로소 한꺼번에 GC의 수거 대상이 됩니다.

결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 됩니다. 이를 함수형 프로그래밍에서는 지연실행이라고 칭합니다.

Flux 아키텍처의 구현체 중 하나인 Reduxdml 미들웨어에도 사용할 수 있습니다.

const logger = (store) => (next) => (action) => {
  console.log('dispatching', action)
  console.log('next state', store.getState())
  return next(action)
}

const thunk = (store) => (next) => (action) => {
  return typeof action === 'function' ? action(dispatch, store.getState) : next(action)
}

store와 dispatch의 의미를 가지는 next는 프로젝트 내에서 한 번 생성된 이후로는 바뀌지 않습니다. 그에 반해 action은 매번 달라집니다. 따라서, store, next값이 결정되면 Redux 내부에서 logger 또는 thunk에 store, next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게 한 것입니다.

5.4 - 정리

클로저란 어떤 함수에 선언한 변수를 참조하는 내부 함수를 외부로 전달하려 할 때, 함수의 실행 컨텍스트가 종료되어도 변수가 사라지지 않는 현상을 의미합니다.

여기서 외부로 전달하는 것은 함수를 return하는 것 뿐 아니라, 콜백으로 전달하는 경우도 포함됩니다.

클로저는 계속해서 메모리를 차지하는 개념이므로 더이상 사용하지 않는 클로저에 대해서는 메모리를 차지하지 않도록 관리해야 합니다.


6. 프로토타입

자바스크립트는 프로토타입 기반 언어입니다. 클래스 기반 언어에서는 ‘상속’을 사용하지만 프로토타입 기반 언어에서는 어떤 객체를 원형으로 삼고 이를 복제함으로써 상속과 비슷한 효과를 얻습니다.

6.1 - 프로토타입의 개념 이해

6.1.1 - constructor, prototype, instance

prototype이라는 프로퍼티와 proto라는 프로퍼티가 새로 등장했는데, 이 둘의 관계가 프로토타입 개념의 핵심입니다. prototype은 객체입니다. 이를 참조하는 proto 역시 당연히 객체겠죠. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장합니다. 그러면 인스턴스에서도 숨겨진 프로퍼티인 proto를 통해 이 메서드들에 접근할 수 있게 됩니다.

note: ES5.1 명세에는 proto가 아니라 [[prototype]]이라는 명칭으로 정의돼 있습니다. proto라는 프로퍼티는 사실 브라우저들이 [[prototype]]을 구현한 대상에 지나지 않았습니다. 명세에는 instance.proto와 같은 방식으로 직접 접근하는 것은 허용하지 않고 오직 Object.getPrototypeOf(instance) / Refelect.getPrototypeOf(instance)를 통해서만 접근할 수 있도록 정의했었습니다. 그러나 브라우저들이 proto에 직접 접근하는 방식을 포기하지 않았고, 결국 ES6에서는 이를 인정했습니다. 그러나 가급적 직접 접근 방식은 피하는게 좋습니다.

var Person = function (name) {
  this._name = name
}
Person.prototype.getName = function () {
  return this._name
}

var suzi = new Person('Suzi')
suzi.__proto__.getName() //undefined

Person.prototype === suzi.__proto__ // true

어떤 함수를 ‘메서드로서’ 호출할 때는 메서드명 바로 앞의 객체가 곧 this가 된다고 했습니다. 그러니까 thomas.proto.getName()에서 getName 함수 내부에서의 this는 thomas가 아니라 thomas.proto라는 객체가 되는 것입니다.

그럼 만약 proto객체에 name 프로퍼티가 있다면 어떨까요 ?

var suzi = new Person('Suzi')
suzi.__proto__._name = 'SUZI__proto__'
suzi.__proto__.getName() // SUZI__proto__

예상대로 잘 출력됩니다. 관건은 this입니다. proto없이 인스턴스에서 곧바로 메서드를 사용하면 어떻게 될까요 ?

var suzi = new Person('Suzi', 28)
suzi.getName() // Suzi
var iu = new Person('Jieun', 28)
iu.getName() // Jieun

proto를 빼면 this는 instance가 되는 것이 맞지만, 이대로 메서드가 호출되고 원하는 값이 나오는 것은 이상합니다.

그 이유는 바로 proto생략 가능한 프로퍼티이기 때문입니다.

proto를 생략하지 않으면 this는 suzi.proto를 가리키지만, 이를 생략하면 suzi를 카리킵니다. suzi.proto에 있는 메서드인 getName을 실행하지만 this는 suzi를 바라보게 할 수 있게 된 것이죠.

prototype

프로토타입을 도식화 하면 위와 같습니다.

“new 연산자로 Constructor를 호출하면 instance가 만들어지는데, 이 instance의 생략 가능한 프로퍼티인 proto는 Constructor의 prototype을 참조한다.”

결과적으로 정리하면 다음과 같습니다. proto 프로퍼티는 생략 가능하도록 구현돼 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 프로퍼티가 있다면 인스턴스에서도 마치 자신의 것처럼 해당 메서드나 프로퍼티에 접근할 수 있게 됩니다.

참고: console.dir로 브라우저에서 특정 인스턴스 등을 호출했을 때, 짙은색과 옅은색이 보입니다. 이 때, 짙은색은 enumerable, 즉 열거 가능한 프로퍼티임을 의미하고, 옅은 색은 innumerable, 즉 열거할 수 없는 프로퍼티임을 의미합니다.

어떤 생성자 함수의 인스턴스는 해당 생성자 함수의 이름을 표기함으로써 해당 함수의 인스턴스임을 표기하고 있습니다.

참고: Array 함수는 from, isArray, of와 같이 인스턴스에서 접근할 수 없는 메서드를 정적 메서드라고 한다.

6.1.2 - constructor 프로퍼티

생성자 함수의 프로퍼티인 prototype 객체 내부에는 constructor라는 프로퍼티가 있습니다. 이 프로퍼티는 단어 그대로 원래의 생성자 함수를 참조합니다.

이를 통해 인스턴스로부터 그 원형이 무엇인지를 알 수 있습니다.

constructor는 읽기 전용 속성이 부여된 기본형 리터럴 변수(number, string, boolean)를 제외하고 값을 바꿀 수 있습니다.

constructor를 변경하더라도 참조하는 대상이 변경될 뿐 이미 만들어진 인스턴스의 원형이 바뀐다거나 데이터 타입이 변하는 것은 아닙니다.

6.2 - 프로토타입 체인

6.2.1 - 메서드 오버라이드

만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면 어떻게 될까요 ?

var Person = function (name) {
  this.name = name
}
Person.prototype.getName = function () {
  return this.name
}

var iu = new Person('지금')
iu.getName = function () {
  return '바로' + this.name
}
console.log(iu.getName()) // 바로 지금

이를 메서드 오버라이드라고 합니다. 메서드 위에 메서드를 덮어씌웠다는 표현입니다. 원본을 제거하고 다른 대상으로 교체하는 것이 아니라 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는 이미지를 떠올리면 정확합니다.

자바스크립트 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그다음으로 가까운 대상인 proto를 검색하는 순서로 진행됩니다. 그러니까 proto에 있는 메서드는 자신에게 있는 메서드보다 검색 순서에서 밀려 호출되지 않은 것이죠.

메서드가 오바라이딩된 경우에는 자신으로부터 가장 가까운 메서드에만 접근할 수 있지만, 우회적인 방법을 통해서 proto의 메서드도 접근할 수 있습니다.

6.2.2 - 프로토타입 체인

배열 리터럴의 proto에는 pop, push 등의 익숙한 배열 메서드 및 constructor가 있습니다. 추가로, 이 proto 안에는 또다시 proto가 등장합니다. 이는 객체의 proto와 동일한 내용으로 이뤄져 있습니다. 그 이유는 prototype 객체가 ‘객체’이기 때문입니다.

기본적으로 모든 객체의 proto에는 Object.prototype이 연결됩니다. prototype 객체도 예외가 아닙니다. 이를 그림으로 표현하면 아래와 같습니다.

prototype-chain
var arr = [1, 2];
arr(.__proto__).push(3);
arr(.__proto__)(.__proto__).hasOwnProperty(2); // true

어떤 데이터의 proto 프로퍼티 내부에 다시 proto 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라 하고, 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 합니다.

프로토타입 체이닝은 메서드 오버라이드와 동일한 맥락입니다.

어떤 메서드를 호출하면 자바스크립트 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행하고, 없으면 proto를 검색해서 있으면 그 메서드를 실행하고, 없으면 다시 proto를 검색해서 실행하는 식으로 진행합니다.

var arr = [1, 2]
Array.prototype.toString.call(arr) // 1, 2
Object.prototype.toString.call(arr) // [object Array]
arr.toString() // 1, 2

arr.toString = function () {
  return this.join('_')
}
arr.toString() // 1_2

위의 예시는 메서드 오버라이드와 프로토타입 체이닝에 관한 예시입니다.

prototype-chain-multi

자바스크립트 데이터는 모두 위의 그림과 동일한 형태의 프로토타입 체인 구조를 지닙니다.

여기서 두 가지 질문을 드리겠습니다. 1) 위쪽 삼각형의 우측 꼭짓점에는 무조건 Object.prototype이 있는 걸까요 ? 2) 삼각형은 꼭 두 개만 연결될까요 ?

  1. Yes(6-2-3절), 2) No(6-2-4절)

6.2.3 - 객체 전용 메서드의 예외사항

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 됩니다. 따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수가 없습니다. 객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있게 되기 때문입니다.

이 같은 이유로 객체만을 대상으로 동작하는 객체 전용 메서드들은 부득이 Object.prototype이 아닌 Object에 스태틱 메서드로 부여할 수밖에 없었습니다.

또한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메서드처럼 ‘메서드명 앞의 대상이 곧 this’가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현돼 있습니다.

따라서, Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들만 있습니다. toString, hasOwnProperty, valueOf, isPrototypeOf 등은 모든 변수가 마치 자신의 메서드인 것처럼 호출할 수 있습니다.

6.2.4 - 다중 프로토타입 체인

대각선의 proto를 연결하는 방법은 proto가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 됩니다. 예시는 다음과 같습니다.

var Grade = function () {
  var args = Array.prototype.slice.call(arguments)
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i]
  }
  this.length = args.length
}
var g = new Grade(100, 80)

Grade.prototype = [] // Grade.prototype이 배열의 인스턴스를 바라보게 하면 됩니다.

이번 절은 ‘두 단계 이상의 체인을 지니는 다중 프로토타입 체인’도 가능하다는 사실을 확인한 정도로 만족하고, 본격적으로 이렇게 하는 이유, 구현 방식 및 문제 해결 등은 다음장에서 더 자세히 살펴보겠습니다.

6.3 - 정리

어떤 생성자 함수를 new 연산자와 함께 호출하면 Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스가 생성되는데, 이 인스턴스에는 proto라는, Constructor의 prototype 프로퍼티를 참조하는 프로퍼티가 자동으로 부여됩니다. proto는 생략 가능한 속성이라서, 인스턴스는 Constructor.prototype의 메서드를 마치 자신의 메서드인 것처럼 호출할 수 있습니다.

Constructor.prototype에는 constructor라는 프로퍼티가 있는데, 이는 다시 생성자 함수 자신을 가리킵니다. 이 프로퍼티는 인스턴스가 자신의 생성자 함수가 무엇인지를 알고자 할 때 필요한 수단입니다.

직각삼각형의 대각선 방향, 즉 proto 방향을 계속 찾아가면 최종적으로는 Object.prototype에 당도하게 됩니다. 이런 식으로 proto 안에 다시 proto를 찾아가는 과정을 프로토타입 체이닝이라고 하며, 이 프로토타입 체이닝을 통해 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있습니다. 이때 접근 방식은 자신으로부터 가장 가까운 대상부터 점차 먼 대상으로 나아가며, 원하는 값을 찾으면 검색을 중단합니다.

Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메서드만이 존재하며, 객체 전용 메서드는 여느 데이터 타입과 달리 Object 생성자 함수에 스태틱하게 담겨있습니다.

프로토타입 체인은 반드시 2단계로만 이뤄지는 것이 아니라 무한대의 단계를 생성할 수도 있습니다. 이에 대해서는 7장에서 자세히 살펴보겠습니다.


7. 클래스

ES6에는 클래스 문법이 추가됐습니다. 다만 ES6의 클래스에서도 일정 부분은 프로토타입을 활용하고 있기 때문에, ES5 체제 하에서 클래스를 흉내내기 위한 구현 방식을 학습하는 것은 여전히 큰 의미가 있습니다.

7.1 - 클래스와 인스턴스의 개념 이해

어떤 클래스의 속성을 지니는 실존하는 개체를 일컬어 인스턴스라고 합니다. 영한사전에서는 인스턴스를 ‘사례’라고 번역하고 있습니다. 풀어쓰면 ‘어떤 조건에 부합하는 구체적인 예시’가 되겠죠.

현실세계에서는 개체들이 이미 존재하는 상태에서 이들을 구분짓기 위해 클래스를 도입합니다.

클래스를 바탕으로 인스턴스를 만들 때 비로소 어떤 개체가 클래스의 속성을 지니게 됩니다. 또한 한 인스턴스는 하나의 클래스만을 바탕으로 만들어집니다.

클래스들은 모두 인스턴스 입장에서는 ‘직계존속’입니다. 다중상속을 지원하는 언어이든 그렇지 않은 언어이든 결국 인스턴스를 생성할 때 호출할 수 있는 클래스는 오직 하나뿐일 수밖에 없기 때문입니다.

프로그래밍 언어에서의 클래스는 사용하기에 따라 추상적인 대상일 수도 있고 구체적인 개체가 될 수도 있습니다.

7.2 - 자바스크립트의 클래스

프로토타입을 일반적인 의미에서의 클래스 관점에서 접근해보면 비슷하게 해석할 수 있는 요소가 없지 않습니다.

생성자 함수 Array를 new 연산자와 함께 호출하면 인스턴스가 생성됩니다. 이때 Array를 일종의 클래스라고 하면, Array의 prototype 객체 내부 요소들이 인스턴스에 ‘상속’된다고 볼 수 있습니다. 엄밀히는 상속이 아닌 프로토타입 체이닝에 의한 참조지만 결과적으로는 동일하게 동작하므로 이렇게 이해해도 무방합니다.

인스턴스가 참조하는지 여부에 따라 스태틱 멤버와 인스턴스 멤버로 나뉩니다.

예제를 통해 클래스 관점에서 바라본 프로토타입 시스템을 좀 더 살펴봅시다.

// 생성자
var Rectangle = function (width, height) {
  this.width = width
  this.height = height
}
// 프로토타입 메서드
Rectangle.prototype.getArea = function () {
  return this.width * this.height
}
// 스태틱 메서드
Rectangle.isRectangle = function (instance) {
  return instance instanceof Rectange && instance.width > 0 && instance.height > 0
}

var rect1 = new Rectangle(3, 4)
console.log(rect1.getArea()) // 12
console.log(rect1.isRectangle(rect1)) // Error
console.log(Rectangle.isRectangle(rect1)) // true

getArea() 처럼 인스턴스에서 직접 호출할 수 있는 메서드가 바로 프로토타입 메서드입니다.

반면, isRectangle() 처럼 인스턴스에서 직접 접근할 수 없는 메서드를 스태틱 메서드라고 합니다.

스태틱 메서드는 생성자 함수를 this로 해야만 호출할 수 있습니다.

구체적인 인스턴스가 사용할 메서드를 정의한 ‘틀’의 역할을 담당하는 목적을 가질 때의 클래스는 추상적인 개념이지만, 클래스 자체를 this로 해서 직접 접근해야만 하는 스태틱 메서드를 호출할 때는 하나의 개체로서 취급됩니다.

7.3 - 클래스 상속

7.3.1 - 기본 구현

클래스 상속은 객체지향에서 가장 중요한 요소 중 하나입니다.

머리에 담고자 하기보다는 가벼운 마음과 이해를 목표로 ‘예전에는 이런 다양한 방식으로 고군분투해왔구나’라는 마음으로 읽어보시기 바랍니다.

자바스크립트에서 클래스 상속을 구현했다는 것은 결국 프로토타입 체이닝을 잘 연결한 것으로 이해하면 됩니다.

그러나 프로토타입으로 클래스의 상속을 구현하고자 했을 때, 클래스에 있는 값이 인스턴스에 영향을 줄 수 있는 구조라는 문제가 생깁니다.

하위 클래스로 삼을 생성자 함수의 prototype에 상위 클래스의 인스턴스를 부여하는 것만으로도 기본적인 메서드 상속은 가능하지만 다양한 문제가 발생할 여지가 있어 구조적으로 안전성이 떨어집니다.

7.3.2 - 클래스가 구체적인 데이터를 지니지 않게 하는 방법

클래스가 구체적인 데이터를 지니지 않게 하는 방법은 여러가지가 있습니다.

방법 1. 일단 만들고 나서 프로퍼티들을 일일이 지우고 더는 새로운 프로퍼티를 추가할 수 없게 하는 방법

방법 2. 아무런 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 만들어 해당 prototype이 SuperClass의 prototype을 바라보게끔 한 다음, SubClass의 prototype에는 Bridge의 인스턴스를 할당하게 하는 방법

방법 3. Object.create 이용하는 방법

7.3.3 - constructor 복구하기

위 세 가지 방법 모두 기본적인 상속에는 성공했지만 SubClass 인스턴스의 constructor는 여전히 SuperClass를 가리키는 상태입니다. 엄밀히는 SubClass 인스턴스에는 constructor가 없고, SubClass.prototype에도 없는 상태입니다.

이를 해결하기 위해서는 SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주면 되겠습니다.

7.4 - ES6의 클래스 및 클래스 상속

ES6의 기능을 자세히 다루는 것은 이 책의 목적에서 벗어나므로, ES5 체계에서 추구하던 자바스크립트 클래스(프로토타입)의 방향성을 재확인하는 목적으로만 간략히 다루겠습니다.

var ES6 = class {
  constructor(name) {
    this.name = name
  }
  static staticMethod() {
    return this.name + ' staticMethod'
  }
  method() {
    return this.name + ' method'
  }
}
var es6Instance = new ES6('es6')
console.log(ES6.staticMethod()) // es6 staticMethod
console.log(es6Instance.method()) // es6 method

나는 여기서 ES6.staticMethod()의 결괏값이 es6 staticMethod인지 이해가 되지 않는다.

분명히 생성자 함수를 통해서 정적 메서드를 호출했는데 어떻게 this.name을 판별할 수 있는걸까 ?

크롬에서 직접 해보면 ES6 staticMethod가 나온다. 저자가 틀렸다고 생각하자.

이번에는 클래스 상속을 살펴봅시다.

var Rectangle = class {
  constructor(width, height) {
    this.width = width
    this.height = height
  }
  getArea() {
    return this.width * this.height
  }
}
var Square = class extends Rectangle {
  constructor(width) {
    // constructor 내부에서는 super라는 키워드를 함수처럼 사용할 수 있는데,
    // 이 함수는 SuperClass의 constructor를 실행합니다.
    super(width, width)
  }
  getArea() {
    // constructor 메서드를 제외한 다른 메서드에서 super는 마치 객체처럼 사용할 수 있습니다.
    // 이때 객체는 SuperClass.prototype을 바라보는데, 호출한 메서드의 this는 'super'가 아닌
    // 원래의 this를 그대로 따릅니다.
    console.log('size is :', super.getArea())
  }
}

7.5 - 정리

자바스크립트는 프로토타입 기반 언어라서 클래스 및 상속 개념은 존재하지 않지만 프로토타입을 기반으로 클래스와 비슷하게 동작하게끔 하는 다양한 기법들이 도입돼 왔습니다.

상위 클래스의 조건을 충족하면서 더욱 구체적인 조건이 추가된 것을 하위 클래스라고 합니다.

클래스 prototype 내부에 정의된 메서드를 프로토타입 메서드라고 하며, 이들은 인스턴스가 마치 자신의 것처럼 호출할 수 있습니다. 한편 클래스(생성자 함수)에 직접 정의한 메서드를 스태틱 메서드라고 하며, 이들은 인스턴스가 직접 호출할 수 없고 클래스(생성자 함수)에 의해서만 호출할 수 있습니다.

7.6 - 마치며

클래스라는 주제는 실무에서 클래스를 구현해서 사용하는 경우도 있지만 그렇지 않은 경우도 많기 때문에 자바스크립트의 핵심을 관통하는 내용이라고 보기는 어렵습니다.

그럼에도 이 주제를 별도의 장으로 분리해서 다룬 이유는 독자 여러분이 스스로 이 책 전반에 대한 학습 정도를 측정하기에 가장 적합한 수단이라는 판단 때문이었습니다.

만약 7장이 너무 어려웠다면 두어 달 정도 지난 뒤에 이 책의 내용을 처음부터 다시 학습해 보시기 바랍니다.