모던 자바스크립트 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]]를 갖는다.
- super 키워드는 함수처럼 호출하면 수퍼클래스의 constructor를 호출한다.
-
클래스 상속에 관한 글을 따로 작성해야 할 것 같다...
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 메서드를 사용해 배열로 변환해야 하는 번거로움이 있었다.
-
매개변수 기본값을 지정할 수 있다.