기억할 만한 내용 정리(두서없음)
function foo() {
x = 10;
}
foo();
console.log(x);
10이 출력된다.
foo에서 x변수가 어디에서 선언되었는지 스코프 체인을 통해 찾기 시작한다. 전역 변수에도 없다.
ReferenceError를 발생시킬 것 같지만 자바스크립트 엔진은 암묵적으로
전역 객체에 x 프로퍼티를 동적 생성한다.
이때 전역 객체의 x 프로퍼티는 마치 전역 변수처럼 사용할 수 있다.
이러한 현상을 암묵적 전역(implicit global)이라 한다.
-> strict mode를 잘 쓰자
빌트인 객체
자바스크립트 객체의 분류
- 표준 빌트인 객체
ECMAscript 사양에 정의된 객체를 말하며, 애플리케이션 전역의 공통 기능을 제공한다.
표준 빌트인 객체는 ECMAscript 사양에 정의된 객체이므로 자바스크립트 실행 환경과 관계없이 언제나 사용할 수 있다.
표준 빌트인 객체는 전역 객체의 프로퍼티로서 제공된다. 따라서 별도의 선언 없이 전역 변수처럼 언제나 참조할 수 있다. - 호스트 객체
호스트 객체는 ECMAscript 사양에 정의되어 있지 않지만 자바스크립트 실행 환경(브라우저, node.js)에서 추가로 제공하는 객체를 말한다.
브라우저 환경에서는 DOM, BOM, Canvas, XMLHttpRequest, fetch 등 WEB API,,
node.js 환경에서는 node.js 고유의 API를 호스트 객체로 제공한다.
- 사용자 정의 객체
사용자가 직접 정의한 객체
표준 빌트인 객체
Object, String, Array, Number, Boolean, Symbol, Date, Math, Promise, Function, Proxy, JSON, Error 등 40여개의 표준 빌트인 객체를 제공한다.
Math, Reflect, JSON을 제외한 표준 빌트인 객체는 모두 인스턴스를 생성할 수 있는 생성자 함수 객체다. 생성자 함수 객체인 표준 빌트인 객체는 프로토타입 메서드와 정적 메서드를 제공하고 생성자 함수 객체가 아닌 표준 빌트인 객체는 정적 메서드만 제공한다.
원시값과 래퍼 객체
const str = 'hello';
console.log(str.length); // 5
console.log(str.toUpperCase()); // HELLO
원시값은 객체가 아니므로 프로퍼티나 메서드를 가질 수 없는데도 원시값인 문자열이 마치 객체처럼 동작한다.
이는 원시값인 문자열, 숫자, 불리언 값의 경우 이들 원시값에 대해 마치 객체처럼 마침표 표기법으로 접근하면 자바스킓트 엔진이 일시적으로 원시값을 연관된 객체로 변환해 주기 때문이다.
원시값을 객체처럼 사용하면 자바스크립트 엔진은 암묵적으로 연관된 객체를 생성하여 생성된 객체로 프로퍼티에 접근하거나 메서드를 호출하고 다시 원시값으로 되돌린다.
이처럼 문자열, 숫자, 불리언 값에 대해 객체처럼 접근하면 생성되는 임시 객체를 래퍼 객체라고 한다.
래퍼 객체의 처리가 종료되면 내부 슬롯에 할당된 원시값으로 원래의 상태, 즉 식별자가 우너시값을 갖도록 되돌리고 래퍼 객체는 가비지 컬렉션의 대상이 된다.
const str = 'hello';
// 여기서 str은 암묵적으로 생성된 래퍼 객채를 가리킨다.
// str의 값 'hello'는 래퍼 객체의 [[StringData]] 내부 슬롯에 할당됨
// 래퍼 객체에 name 프로퍼티가 동적 추가된다.
str.name = 'lee';
// str은 다시 원래의 문자열, 즉 래퍼 객체의 [[StringData]] 내부 슬롯에
// 할당된 원시값을 갖는다.
// 이떄 위에서 생성된 래퍼 객체는 아무도 참조하지 않는 상태이므로 가비지
// 컬렉션의 대상이 되고. 사라진다.
console.log(str.name); // undefined
전역 객체
전역 객체 자신은 어떤 객체의 프로퍼티도 아니며 객체의 계층적 구조상 표준 빌트인 객체와 호스트 객체를 프로퍼티로 소유한다.
- 전역 객체는 개발자가 의도적으로 생성할 수 없다.
- 전역 객체의 프로퍼티를 참조할 때 window(또는 global)를 생략할 수 있다
- 전역 객체는 Object, String 등 모든 표준 빌트인 객체를 프로퍼티로 가지고 있다.
- var 키워드로 선언한 전역 변수와 선언하지 않은 변수에 값을 할당한 암묵적 전역;; 그리고 전역 함수는 전역 객체의 프로퍼티가 된다.
- let이나 const키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아니다.
- let이나 const 키워드로 선언한 전역 변수는 보이지 않는 개념적인 블록(전역 랙시컬 환경의 선언적 환경 레코드) 내에 존재하게 된다.
this
this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수다.(self-referencing variable). this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
this는 자바스크립트 엔진에 의해 암묵적으로 생성되며, 코드 어디서든 참조할 수 있다.
함수를 호출하면 arguments 객체와 this가 암묵적으로 함수 내부에 전달된다. 함수 내부에서 arguments 객체를 지역 변수처럼 사용할 수 있는 것처럼 this도 지역 변수처럼 사용할 수 있다.
자바스크립트의 this는 함수가 호출되는 방식에 따라 this 바인딩이 동적으로 결정된다.
또한 strict mode역시 this 바인딩에 영향을 준다.
렉시컬 스코프와 this 바인딩은 결정 시기가 다르다.
함수의 상위 스코프를 결정하는 방식인 렉시컬 스코프는 함수 정의가 평가되어 함수 객체가 생성되는 시점에 상위 스코프를 결정한다. 하지만 this 바인딩은 함수 호출 시점에 결정된다.
함수를 호출하는 방식은 다음과 같다.
- 일반 함수 호출
- 메서드 호출
- 생성자 함수 호출
- Function.prototype.apply/call/bind 메서드에 의한 간접 호출
일반 함수로 호출하면 함수 내부의 this에는 전역 객체가 바인딩된다.
-> strict mode 에서는 undefined
메서드 내에서 정의한 중첩 함수도 일반 함수로 호출되면 중첩 함수 내부의 this에는 전역 객체가 바인딩된다.
const obj = {
value: 100,
foo() {
console.log(this); // obj
function bar() {
console.log(this); // global
}
bar();
},
};
obj.foo();
콜백 함수가 일반 함수로 호출된다면 콜백 함수의 내부의 this에도 전역 객체가 바인딩된다.
어떠한 함수라도 일반 함수로 호출되면 this에 전역 객체가 바인딩된다.
콜백함수에서 의도대로 사용하기
const obj = {
value: 100,
foo() {
setTimeout(
function () {
console.log(this);
}.bind(this),
100
);
},
};
obj.foo();
혹은 화살표 함수를 사용해서 this 바인딩을 일치시킬 수 있다.
const obj = {
value: 100,
foo() {
// 화살표 함수 내부의 this는 상위 스코프의 this를 가리킨다.
setTimeout(() => console.log(this), 100);
},
};
obj.foo();
메서드 호출
메서드 내부의 this는 메서드를 소유한 객체가 아닌 메서드를 호출한 객체에 바인딩된다.
// 객체의 getName 프로퍼티가 가리키는 함수 객체는
// person 객체에 포함된 것이 아니라 독립적으로 존재하는 별도의 객체다.
// getName 프로퍼티가 함수 객체를 가리키고 있을 뿐이다.
const person = {
name: 'Lee',
getName() {
return this.name;
},
};
console.log(person.getName()); // Lee
const anotherPerson = {
name: 'Kim',
};
anotherPerson.getName = person.getName;
console.log(anotherPerson.getName()); // Kim
const getName = person.getName;
console.log(getName());
// Node.js에서 undefined. global.name과 같다.
생성자 함수 호출
생성자 함수 내부의 this에는 생성자 함수가 (미래에) 생성할 인스턴스가 바인딩된다.
함수 호출 방식 | this 바인딩 |
---|---|
일반 함수 호출 | 전역 객체 |
메서드 호출 | 메서드를 호출한 객체 |
생성자 함수 호출 | 생성자 함수가(미래에) 생성할 인스턴스 |
Function.prototype.apply/call/bind 메서드에 의한 간접 호출 |
Function.prototype.apply/call/bind 메서드에 첫 번째 인수로 전달한 객체 |
실행 컨텍스트
소스코드의 타입
ECMAScript 사양은 소스코드(ECMAScript code)를 4가지 타입으로 구분한다.
4가지 타입의 소스코드는 실행 컨텍스트를 생성한다.
소스코드의 타입 | 설명 |
---|---|
전역 코드(global code) | 전역에 존재하는 소스코드를 말한다. 전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않는다. |
함수 코드(function code) | 함수 내부에 존재하는 소스코드를 말한다. 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않는다. |
eval 코드(eval code) | 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드를 말한다. |
모듈 코드(module code) | 모듈 내부에 존재하는 소스코드를 말한다. 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않는다. |
소스코드를 4가지 타입으로 구분하는 이유는 소스코드의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문이다.
- 전역 코드
전역 코드는 전역 변수를 관리하기 위해 최상위 스코프인 전역 스코프를 생성해야 한다.
그리고 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수를 전역 객체의 프로퍼티와 메서드로 바인딩하고 참조하기 위해 전역 객체와 연결되어야 한다.
이를 위해 전역 코드가 평가되면 전역 실행 컨텍스트가 생성된다. - 함수 코드
함수 코드는 지역 스코프를 생성하고 지역 변수, 매개변수, arguments 객체를 관리해야 한다.
그리고 생성한 지역 스코프를 전역 스코프에서 시작하는 스코프 체인의 일원으로 연결해야 한다.
이를 위해 함수 코드가 평가되면 함수 실행 컨텍스트가 생성된다. - eval 코드
eval 코드는 strict mode에서 자신만의 독자적인 스코프를 생성한다.
이를 위해 eval 코드가 평가되면 eval 실행 컨텍스트가 생성된다. - 모듈 코드
모듈 코드는 모듈별로 독립적인 모듈 스코프를 생성한다.
이를 위해 모듈 코드가 평가되면 모듈 실행 컨텍스트가 생성된다.
소스코드의 평가와 실행
자바스크립트 엔진은 소스코드를 2개의 과정, "소스코드의 평가"와 "소스코드의 실행"으로 나누어 처리한다.
소스코드 평가 과정에서는 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 생성된 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.
소스코드 평가 과정이 끝나면 비로소 선언문을 제외한 소스코드가 순차적으로 실행되기 시작한다. 즉, 런타임이 시작된다.
이때 소스코드 실행에 필요한 정보, 즉 변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색해서 취득한다. 그리고 변수 값의 변경 등 소스코드의 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록된다.
- 전역 코드 평가
전역 코드의 변수 선언문과 함수 선언문이 먼저 실행되고, 그 결과 생성된 전역 변수와 전역 함수가 실행 컨텍스트가 관리하는 전역 스코프에 등록된다.
이때 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 객체의 프로퍼티와 메서드가 된다. - 전역 코드 실행
런타임이 시작되어 전역 코드가 순차적으로 실행되기 시작한다.
이때 전역 변수에 값이 할당되고 함수가 호출된다. 함수가 호출되면 순차적으로 실행되던 전역 코드의 실행을 일시 중단하고 코드 실행 순서를 변경하여 함수 내부로 진입한다. - 함수 코드 평가
함수 호출에 의해 코드 실행 순서가 변경되어 함수 내부로 진입하면 함수 내부의 문들을 실행하기에 앞서 함수 코드 평가 과정을 거치며 함수 코드를 실행하기 위한 준비를 한다. 이때 매개변수와 지역 변수 선언문이 먼저 실행되고, 그 결과 생서된 매개변수와 지역 변수가 실행 컨텍스트가 관리하는 지역 스코프에 등록된다.
또한 함수 내부에서 지역 변수처럼 사용할 수 있는 arguments객체가 생성되어 지역 스코프에 등록되고 this 바인딩도 결정된다. - 함수 코드 실행
함수 코드 평가가 끝나면 런타임이 시작되어 함수 코드가 순차적으로 실행된다.
이때 매개 변수와 지역 변수에 값이 할당된다.
이처럼 코드가 실행되려면 다음과 같이 스코프, 식별자, 코드 실행 순서 등의 관리가 필요하다.
- 선언에 의해 생서된 모든 식별자를 스코프를 구분하여 등록하고 상태 변화를 지속적으로 관리할 수 있어야 한다.
- 스코프는 중첩 관계에 의해 스코프 체인을 형성해야 한다. 즉, 스코프 체인을 통해 상위 스코프로 이동하며 식별자를 검색할 수 있어야 한다.
- 현재 실행 중인 코드의 실행 순서를 변경할 수 있어야 하며 다시 되돌아갈 수도 있어야 한다.
이 모든 것을 관리하는 것이 바로 실행 컨텍스트다.
실행 컨텍스트는 소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.
식별자와 스코프는 실행 컨텍스트의 렉시컬 환경으로 관리하고 코드 실행 순서는 실행 컨텍스트 스택으로 관리한다.
렉시컬 환경우 두 개의 컴포넌트로 구성된다.
- 환경 레코드(Environment Record)
스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소다. 환경 레코드는 소스코드의 타입에 따라 관리하는 내용에 차이가 있다. - 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)
외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킨다. 이때 상위 스코프란 외부 렉시컬 환경, 즉 해당 실행 컨텍스트를 생성한 소스코드를 포함하는 상위 코드의 렉시컬 환경을 말한다. 외부 렉시컬 환경에 대한 참조를 통해 단방향 링크드 리스트인 스코프 체인을 구성한다.
Global execution context
LexicalEnvironment ->
Global Lexical Environment
GlobalEnvironmentRecord ->
OuterLexicalEnvironmentReference
Object Environment Record
BindingObject ->
window(Global Object)
var 선언 key: val
함수 선언문 전역 함수
Declarative Environment Record ==> let, const
key: val
전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 환경 레코드의 객체 환경 레코드에 연결된 BindingObject를 통해 전역 객체의 프로퍼티와 메서드가 된다
그리고 이때 등록된 식별자를 전역 환경 레코드의 객체 환경 레코드에서 검색하면 전역 객체의 프로퍼티를 검색하여 반환한다.
이것이 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수가 전역 객체의 프로퍼티와 메서드가 되고 전역 객체를 가리키는 식별자(window)없이 전역 객체의 프로퍼티를 참조할 수 있는 메커니즘이다.
(window.alert를 alert로)
var 키워드로 선언한 변수는 선언 단계와 초기화 단계가 동시에 진행된다.
(undefined)
다시 말해 전역 코드 평가 시점에 객체 환경 레코드에 바인딩된 BindingObject를 통해 전역 객체에 변수 식별자를 키로 등록한 다음 undefined를 바인딩한다.
let, const 키워드로 선언한 전역 변수는 Declarative Environment Record에 등록되고 관리된다.
OuterLexicalEnvironmentReference는 현재 평가중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킨다. 이를 통해 단방향 링크드 리스트인 스코프 체인을 구성한다.
자바스크립트 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 함수의 상위 스코프를 함수 객체의 내부 슬롯[[Environment]]에 저장한다.
함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 해당되는 것은 바로 함수의 상위 스코프를 가리키는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조다.
함숫 객체의 내부 슬롯 [[Environment]]가 바로 렉시컬 스코프를 구현하는 메커니즘이다.
실행 컨텍스트 스택에서 특정 함수 실행 컨텍스트가 제거되었다고 해서 그 함수의 렉시컬 환경까지 즉시 소멸하는 것은 아니다.
렉시컬 환경은 실행 컨텍스트에 의해 참조되기는 하지만 독립적인 객체다.
객체를 포함한 모든 값은 누군가에게 참조되지 않을 때 비로소 가비지 컬렉터에 의해 메모리 공간의 확보가 해제되어 소멸한다.
특정 함수 실행 컨텍스트가 소멸되었다 하더라도 만약 함수 렉시컬 환경을 누군가 참조하고 있다면 함수 렉시컬 환경은 소멸하지 않는다. -> 클로저
함수는 자신의 내부 슬롯[[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다. -> 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리킨다.
클로저와 렉시컬 환경
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.
이러한 중첩 함수를 클로저라고 부른다.
클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수(free variable)라고 부른다.
클로저란 "함수가 자유 변수에 대해 닫혀있다" 라는 의미다.
이를 좀더 알기 쉽게 의역하자면 "자유 변수에 묶여있는 함수"라고 할 수 있다.
이론적으로 클로저는 상위 스코프를 기억해야 하므로 불필요한 메모리의 점유를 걱정할 수도 있겠따. 하지만 모던 자바스크립트 엔진은 최적화가 잘 되어 있어서 클로저가 참조하고 있지 않는 식별자는 기억하지 않는다.
-> 상위 스코프의 식별자 중에서 기억해야 할 식별자만 기억한다.
클로저의 활용
클로저는 상태를 안전하게 은닉하고 변경하고 유지하기 위해 사용한다.
상태가 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.
const Person = (function() {
let _age = 0;
function Person(name, age) {
this.name = name;
_age = age;
}
Person.prototype.sayHi = function() {
console.log(`Hi! My name= ${this.name}. I am ${_age}`;
}
return Person;
} ());
const me = new Person('Lee', 20);
me.sayHi(); // Lee, 20
const you = new Person('Kim', 30);
you.sayHi(); // Kim, 30
me.sayHi(); // Lee, 30 => 값 변경됨
이처럼 자바스크립트는 정보 은닉을 완전하게 지원하지 않는다.
let이나 const 키워드를 사용하즌 반복문(for, for...in, for...of, while 등)은 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하여 반복할 당시의 상태를 마치 스냅숏을 찍는 것처럼 저장한다.
-> Go랑은 다름
단 이는 반복분의 코드 블록 내부에서 함수를 정의할 때 의미가 있다.
반복문의 코드 블록 내부에 함수 정의가 없는 반복문이 생성하는 새로운 렉시컬 환경은 반복 직후 아무도 참조하지 않기 때문에 가비지 컬렉션의 대상이 된다.
또 다른 방법으로는 함수형 프로그래밍 기법인 고차 함수는 사용하는 방법이 있다.
이 방법은 변수와 반복문의 사용을 억제할 수 있기 때문에 오류를 줄이고 가독성을 좋게 만든다.
// 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
/// 배열의 요소로 추가된 함수들은 모두 클로저다.
const funcs = Array.from(new Array(3), (_, i) => () => i);
// (3) [f, f, f]
// 배열의 요소로 추가된 함수들을 순차적으로 호출한다.
funcs.forEach(f => console.log(f()));
클래스
클래스가 기존의 프로토타입 기반 객체지향 모델을 폐지하고 새롭게 클래스 기반 객체지향 모델을 제공하는 것은 아니다.
사실 클래스는 함수이며 기존 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 syntactic sugar 라고도 볼 수 있다.
단 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스를 생성하지만 정확히 동일하게 동작하지는 않는다.
클래스는 생성자 함수보다 엄격하며 생성자 함수에서는 제공하지 않는 기능도 제공한다.
클래스는 생성자 함수와 매우 유사하게 동작하지만 다음과 같이 몇 가지 차이가 있다.
- 클래스를 new 연산자 없이 호출하면 에러가 발생한다. 하지만 생성자 함수를 new 없이 호출하면 일반 함수로서 호출된다.
- 클래스는 상속을 지원하는 extends와 super 키워드를 제공한다. 하지만 생성자 함수는 extends와 super키워드를 지원하지 않는다.
- 클래스는 호이스팅이 발생하지 않는 것처럼 동작한다. 하지만 함수 선언문으로 정의된 생성자 함수는 함수 호이스팅이, 함수 표현식으로 정의한 생성자 함수는 변수 호이스팅이 발생한다.
- 클래스 내의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 strict mode를 해제할 수 없다. 하지만 생성자 함수는 그렇지 않다.
- 클래스의 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 false다. 열거되지 않는다.
생성자 함수와 클래스는 프로토타입 기반의 객체지향을 구현했다는 점에서 매우 유사하다. 하지만 클래스는 생성자 함수 기반의 객체 생성 방식보다 견고하고 명료하다.
특히 클래스의 extends와 super 키워드는 상속 관계 구현을 더욱 간겷라고 명료하게 한다.
따라서 클래스를 프로토타입 기반 객체 생성 패턴의 단순한 문법적 설탕이라고 보기보다는 새로운 객체 생성 메커니즘으로 보는것이 좀 더 합당하다.
클래스는 class 키워드를 사용하여 정의한다.
class Person {} // 파스칼 케이스
// 익명 클래스 표현식
const Person = class {};
// 기명 클래스 표현식
const Person = class MyClass{};
클래스를 표현식으로 정의할 수 있다는 것은 클래스가 값으로 사용할 수 있는 일급 객체라는 것을 의미한다.
클래스는 일급 객체로서 다음과 같은 특징을 갖는다.
- 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
- 변수나 자료구조에 저장할 수 있다.
- 함수의 매개변수에 전달할 수 있다.
- 함수의 반환값으로 사용할 수 있다.
좀 더 자세히 말하자면 클래스는 함수다. 따라서 클래스는 값처럼 사용할 수 있는 일급 객체다.
클래스 몸체에는 0개 이상의 메서드만 정의할 수 있다.
클래스 몸체에 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드의 세 가지가 있다.
class Person {
// 생성자
constructor(name) {
this.name = name; // name 프로퍼티는 public
}
// 프로토타입 메서드
sayHi() {
console.log("name = ", name);
}
static sayHello() {
console.log("Hello");
}
}
console.log(typeof Person); // function
클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성한다. 이때 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 함수, 즉 constructor다.
클래스의 constructor()는 메서드로 해석되는 것이 아니라 클래스가 평가되어 생성한 함수 객체 코드의 일부가 된다. 다시 말해, 클래스 정의가 평가되면 constructor의 기술된 동작을 하는 함수 객체가 생성된다.
클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티
클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 이름이 같이 혼동하기 쉽지만 직접적인 관련이 없다. 프로토타입의 constructor 프로퍼티는 모든 프로토타입이 가지고 있는 프로퍼티이며, 생성자 함수를 가리킨다.(포인팅?)
클래스 몸체에서 정의한 메서드는 생성자 함수에 대한 객체 생성 방식과는 다르게 클래스의 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으롶 르토톼입 메서드가 된다.
정적 메서드는 인스턴스로 호출할 수 없다.
정적 메서드가 바인딩된 클래스는 인스턴스의 프로토타입 체인 상에 존재하지 않기 때문이다.
다시 말해, 인스턴스으 피르토톼입 체인 상에는 클래스가 존재하지 않기 때문에 인스턴스로 클래스의 메서드를 상속받을 수 없다.
- 정적 메서드는 인스턴스 프로퍼티를 참조할 수없지만, 프로토타입 메서드는 인스턴스의 프로퍼티를 참조할 수 있다.
클래스에서 정의한 메서드의 특징
- function 키워드를 생략한 메서드 축약 표현을 사용한다.
- 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없다.
- 암묵적으로 strict mode로 실행된다.
- for...in문이나 Object.keys 메서드 등으로 열거할 수 없다. [[Enumberable]] 의 값이 false다.
- 내부 메서드 [[Constructor]]를 갖지 않는 non-constructor다. 따라서 new 연산자와 함께 호출할 수 없다.
클래스의 메서드는 기본적으로 프로토타입 메서드가 된다.
따라서 클래스의 접근자 프로퍼티 또한 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.
클래스 필드에 함수를 할당하는 경우, 이 함수는 프로토타입ㄷ 메서드가 아닌 인스턴스 메서드가 된다.
모든 클래스 필드는 인스턴스 프로퍼티가 되기 때문이다. 따라서 클래스 필드에 함수를 할당하는 것은 권장하지 않는다.
// 클래스 필드와 함수
class Person {
name = 'Lee';
getName = () => this.name;
}
private field에는 #
을 붙여준다. priavte field를 참조할 때도 #
을 붙여주어야 한다.
private field는 클래스 내부에서만 참조할 수 있다!!
하지만 접근자 프로퍼티를 통해 간접적으로 접근하는 방법은 유효하다.
class Person {
#name = '';
constructor(name) {
this.#name = name;
}
get name() {
return this.#name.trim();
}
}
const me = new Person('Lee');
console.log(me.name);
private field는 반드시 클래스 몸체에 정의해야 한다.
private field를 직접 constructor에 정의하면 에러가 발생한다.
class Person {
constructor(name) {
this.#name = name; // 에러
}
}
static field도 있다.
static public은 기존 필드 앞에 static을 붙여주면 되고,
static private은 기존처럼 #
변수명 앞에 static을 붙여주면 된다.
상속에 의한 클래스 확장
클래스 상속과 생성자 함수 상속
상속에 의한 클래스 확장은 지금까지 살펴본 프로토타입 기반 상속과는 다른 개념이다.
프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념이지만,
상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.
클래스는 상속을 통해 기존 클래스를 확장할 수 있는 문법이 기본적으로 제공되지만 생성자 함수는 그렇지 않다
extends 키워드의 역할은 수퍼클래스와 서브클래스 간의 상속 관계를 설정하는 것이다. 클래스도 프로토타입을 통해 상속 관계를 구현한다.
수퍼클래스와 서브클래스는 인스턴스의 프로토타입 체인 뿐 아니라 클래스 간의 프로토타입 체인도 생성한다.
이를 통해 프로토타입 메서드, 정적 메서드 모두 상속이 가능하다.
extneds키워드 다음에는 클래스뿐만이 아니라 [[Consturctor]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다.
이를 통해 동적으로 상속받을 대상을 결정할 수 있다.
function Base1() {}
class Base2 {}
let condition = true;
// 왓더..
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false
클래스는 constructor를 생략하면 비어있는 constructor가 암묵적으로 정의된다.
constructor() {}
서브클래스에서 constructor를 생략하면 다음과 같은 constructor가 암묵적으로 정의된다.
constructor(...args) { super(...args);}
super()
는 수퍼클래스의 constructor(super-constructor)를 호출하여 인스턴스를 생성한다.
super 키워드
super 키워드는 함수처럼 호출할 수도 있고 this와 같인 식별자처럼 참조할 수 있는 특수한 키워드다. super는 다음과 같이 동작한다.
- super를 호출하면 수퍼클래스의 constructor를 호출한다.
- super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
다음과 같이 수퍼클래스에서 추가한 프로퍼티와 서브클래스에서 추가한 프로퍼티를 갖는 인스턴스를 생성한다면 서브클래스의 constructor를 생략할 수 없다.
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends Base {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
const derived = new Derived(1, 2, 3);
console.log(derived);
// Derived { a: 1, b: 2, c: 3 }
super 호출 주의사항
- 서브클래스에서 constructor를 생략하지 않는 경우 서브클래스의 constructor에는 반드시 super를 호출해야 한다.
- 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다.
- super는 반드시 서브클래스의 constructor에서만호출한다. 서브클래스가 아닌 클래스의 constructor나 함수에서 super를 호출하면 에러가 발생한다.
super 참조
- 서브클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 프로토타입 메서드 sayHi를 가리킨다.
- 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.
서브클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다.
이것이 바로 서브클래스의 constructor에서 반드시 super를 호출해야 하는 이유다.
이때 인스턴스는 수퍼클래스가 생성한 것이다. 하지만 new연산자와 함께 호출된 클래스가 서브클래스라는 것이 중여하다. 즉 new 연산자와 함께 호출된 함수를 가리키는 new.target은 서브클래스를 가리킨다. 따라서 인스턴스는 new.target이 가리키는 서브클래스가 생성한 것으로 처리된다.
따라서 생성된 인스턴스의 프로토타입은 수퍼클래스의 prototype 프로퍼티가 가리키는 객체가 아니라 new.target, 즉 서브클래스의 prototype프로퍼티가 가리키는 객체다.
-> super가 반환한 인스턴스가 서브클래스의 constructor의 this에 바인딩된다. 서브클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩하여 그대로 사용한다.
이처럼 super가 호출되지 않으면 인스턴스가 생성되지 않으며 this 바인딩도 할 수 없다. 서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없는 이유가 바로 이 때문이다.
앞에서 살펴보았듯이 extends 키워드 다음에는 클래스뿐만이 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다. 따라서 String, Number, Array 같은 표준 빌트인 객체도 extends 키워드를 사용하여 확장할 수 있다.
ES6에서는 함수를 사용 목적에 따라 세 가지 종류로 명확히 구분했다.
ES6 함수의 구분 | constructor | prototype | super | arguments |
---|---|---|---|---|
일반 함수(Normal) | O | O | X | O |
메서드(Method) | X | X | O | O |
화살표 함수(Arrow)X | X | X | X | X |
메서드
일반 함수는 함수 선언문이나 함수 표현식으로 정의한 함수를 말하며, ES6 이전의 함수와 차이가 없다. 하지만 ES6의 메서드와 화살표 함수는 ES6 이전의 함수와 명확한 차이가 있다.
메서드랑 메서드 축약 표현으로 정의된 함수만을 의미한다.
conset obj = {
x: 1,
foo() {return this.x;}, // 메서드
bar: function() {return this.x;}, // 메서드 아님
}
메서드는 인스턴스를 생성할 수 없는 non-constructor다.
메서드는 인스턴스를 생성할 수 없으므로 prototype 프로퍼티가 없고, 프로토타입도 생성하지 않는다.
메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다. super 참조는 내부 슬롯 [[HomeObject]]를 사용하여 수퍼클래스의 메서드를 참조하므로 내부 슬롯 [[HomeObject]]를 갖는 ES6메서드는 super 키워드를 사용할 수 있다.
화살표 함수
화살표 함수는 function 키워드 대신 화살표(=>, fat arrow)를 사용하여 기존의 함수 정의 방식보다 간략하게 함수를 정의할 수 있다. 화살표 함수는 표현만 간략한 것이 아니라 내부 동작도 기존의 함수보다 간략하다. 특히 화살표 함수는 콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위한 대안으로 유용하다.
(x, y) => {...};
함수 몸체가 하나의 문으로 구성된다면 함수 몸체를 감싸는 중괄호를 생략할 수 있다.
이때 함수 몸체 내부의 문이 값으로 평가될 수 있는 표현식인 문이라면 암묵적으로 변환된다.
const power = x => x ** 2;
표현식이 아닌 문이라면 에러가 발생한다.
const arrow = () => const x = 1; // 에러
객체 리터럴을 반환하는 경우 객체 리터럴을 소괄호로 감싸주어야 한다.
const create = (id, content) => ({ id, content });
// 위 표현은 다음과 동일하다.
const create = (id, content) => { return {id, content};};
객체 리터럴을 소괄호로 감싸지 않으면 객체 리터럴의 중괄호를 함수 몸체를 감싸는 중괄호로 잘못 해석한다.
화살표 함수는 일반 함수의 기능을 간략화했으며 this 도 편리하게 설계되었다.
일반 함수와 화살표 함수의 차이
- 화살표 함수는 인스턴스를 생성할 수 없는 non-constructor다.
화살표 함수는 인스턴스를 생성할 수 없으므로 prototype 프로퍼티가 없고 프로토타입도 생성하지 않는다.
- 화살표 함수는 함수 자체의 this, arguments, super, new.target 바인딩을 갖지 않는다.
따라서 화살표 함수 내부에서 this, arguments, super, new.target 을 참조하면 스코프 체인을 통해 상위 스코프의 this, arguments, super, new.target을 참조한다.
만약 화살표 함수와 화살표 함수가 중첩되어 있다면 상위 화살표 함수에도 this, arguments, super, new.target 바인딩이 없으므로 스코프 체인 상에서 가장 가까운 상위 함수 중에서 화살표 함수가 아닌 함수의 this, arguments, super, new.target을 참조한다.
일반 함수의 호출에서
this -> strict mode에서는 undefined, 아니면 전역 객체
Rest 파라미터
Rest파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받는다.
function foo(...rest) {
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// [1, 2, 3, 4, 5]
일반 매개변수와 Rest파라미터는 함께 사용할 수 있다.
이때 함수에 전달된 인수들은 매개변수와 Rest파라미터에 순차적으로 할당된다.
function foo(param, ...rest) {
console.log(param);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 1
// [2, 3, 4, 5]
Rest파라미터는 이름 그대로 먼저 선언된 매개변수에 할당된 인수를 제외한 나머지 인수들로 구성된 배열이 할당된다.
따라서 Rest 파라미터는 반드시 마지막 파라미터이어야 한다.
Rest 파라미터는 단 하나만 선언할 수 있따.
Rest 파라미터는 함수 정의 시 선언한 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티에 영향을 주지 않는다.
function f1(...rest) {}
console.log(f1.length); // 0
function f2(p1, ...rest) {}
console.log(f2.length); // 1
function f3(p1, p2, ...rest) {}
console.log(f3.length); // 2
매개변수 기본값은 매대변수에 인수를 전달하지 않은 경우와 undefined를 전달한 경우에만 유효하다.
function logName(name = 'me') {
console.log(name);
}
logName(); // me
logName(undefined); // me
logName(null); // null
배열
자바스크립트의 모든 값은 배열의 요소가 될 수 있다.
즉 원시값은 물론 객체, 함수, 배열 등 자바스크립트에서 값으로 인정하는 모든 것은 배열의 요소가 될 수 있다.
배열은 요소의 개수를 나타내는 length 프로퍼티를 갖는다.
자바스크립트에 배열이라는 타입은 존재하지 않는다. 배열은 객체 타입이다.
배열은 배열 리터럴, Array 생성자 함수, Array.of, Array.from 메서드로 생성할 수 있다.
배열의 생성자 함수는 Array이며, 배열의 프로토타입 객체는 Array.prototype이다.
Array.prototype은 배열을 위한 빌트인 메서드를 제공한다.
배열은 객체지만 일반 객체와는 구별되는 독특한 특징이 있다.
구분 | 객체 | 배열 |
---|---|---|
구조 | 프로퍼티 키와 프로퍼티 값 | 인덱스와 요소 |
값의 참조 | 프로퍼티 키 | 인덱스 |
값의 순서 | X | O |
length 프로퍼티 | X | O |
자바스크립트의 배열은 자료구조에서 말하는 일반적인 의미의 배열과 다르다. 즉 배열의 요소를 위한 각각의 메모리 공간은 동일한 크기를 갖지 않아도 되며, 연속적으로 이어져 있지 않을 수 도 있다. 배열의 요소가 연속적으로 이어져 있지 않는 배열을 희소 배열이라 한다.
자바스크립트의 배열은 일반적인 배열의 동작을 흉내 낸 특수한 객체다.
console.log(Object.getOwnPropertyDescriptors([1, 2, 3]));
// OUTPUT
{
'0': { value: 1, writable: true, enumerable: true, configurable: true },
'1': { value: 2, writable: true, enumerable: true, configurable: true },
'2': { value: 3, writable: true, enumerable: true, configurable: true },
length: { value: 3, writable: true, enumerable: false, configurable: false }
}
자바스크립트 배열은 인덱스를 나타내는 문자열을 프로퍼티 키로 가지며, lenght 프로퍼티를 갖는 특수한 객체다.
자바스크립트 배열의 요소는 사실 프로퍼티 값이다. 자바스크립트에서 사용할 수 있는 모든 값은 객체의 프로퍼티 값이 될 수 있으므로 어떤 타입의 값이라도 배열의 요소가 될 수 있다.
- 일반적인 배열은 인덱스로 요소에 빠르게 접근할 수 있다. 하지만 요소를 삽입 또는 삭제하는 경우에는 효율적이지 않다.
- 자바스크립트 배열은 해시 테이블로 구현된 객체이므로 인덱스로 요소에 접근하는 경우 일반적인 배열보다 성능적인 면에서 느릴수밖에 없는 구조적인 단점이 있다. 하지만 요소를 삽입 또는 삭제하는 경우에는 일반적인 배열보다 빠른 성능을 기대할 수 있다.
정규표현식 리터럴은 /regexp/i
시작 /, 패턴, 종료 /, 플래그로 구성된다.
기본적인 플래그
플래그 | 의미 | 설명 |
---|---|---|
i | ignore case | 대소문자를 구별하지 않고 패턴을 검색한다. |
g | Global | 대상 문자열 내에서 패턴과 일치하는 모든 문자열을 전역 검색한다. |
m | Multi line | 문자열의 행이 바뀌더라도 패턴 검색을 계속한다. |
플래그는 옵션이므로 선택적으로 사용할 수 있으며, 순서와 상관없이 하나 이상의 플래그를 동시에 설정할 수도 있다.
정규 표현식의 패턴은 /로 열고 닫으며 문자열의 따옴표는 생략한다.
따옴표를 포함하면 따옴표까지도 패턴에 포함되어 검색된다. 또한 패턴은 특별한 의미를 가지는 메타문자 또는 기호로 표현할 수 있다.
어떤 문자열 내에 패턴과 일치하는 문자열이 존재할 때 '정규표현식과 매치한다'고 표현한다.
정규 표현식의 패턴에 문자 또는 문자열을 지정하면 검색 대상 문자열에서 패턴으로 지정한 문자 또는 문자열을 검색한다.
.은 임의의 문자 한 개를 의미한다. 문자의 내용은 무엇이든 상관없다.
{n,m}은 앞선 패턴이 최소 n번, 최대 m번 반복되는 문자열을 의미한다.
콤마 뒤에 공백이 있으면 정상 동작하지 않으므로 주의.
=> /A{1,2}/g => A, AA를 찾음
{n}은 앞선 패턴이 n번 반복되는 문자열을 의미한다. 즉 {n,n}과 같다.
{n,}은 앞선 패턴이 최소 n번 이상 반복되는 문자열을 의미한다.
+는 앞선 패턴이 최소 한번 이상 반복되는 문자열을 의미한다. {1,}과 같다.
?는 앞선 패턴이 최대 한 번 포함되는 문자열을 의미한다. {0,1}과 같다.
const target = "color colour';
const regExp = /colou?r/g;
traget.match(regExp); // -> ['color', 'colour']
|은 or의 의미를 갖는다. /A|B/는 'A' 또는 'B'를 의미한다.
/A+|B+/g
A또는 B가 한 번 이상 반복되는 문자열 전역 검색
'A', 'AA', 'AAA' ,, 'B', 'BB' ,,,,/[AB]+/g
와 같다.
범위를 지정하려면 []내에 -를 사용한다./[A-Z]+/
=> 'A', 'AA',,, 'B', 'BB', ,,, 'Z', 'ZZ',,,,
대소문자를 구별하지 않고 알파벳을 검색하는 방법, 숫자 검색 방법/[A-Za-z]+/g
/[0-9]+/g
숫자와 ,(콤마)가 한 번 이상 반복되느 문자열/[0-9,]+/g
=> 12,345 매치
\d
는 숫자를 의미한다. => [0-9]\D
는 숫자가 아닌 문자를 의미한다.
\w
는 알파벳, 숫자, 언더스코어를 의미한다. => [0-9A-Za-z_]\W
는 \w
와 반대로 동작한다. 즉 알파벳, 숫자, 언더스코어가 아닌 문자를 의미한다.
[ ... ] 내의 ^(캐럿)은 not의 의미를 갖는다.
예를 들어 [^0-9]
는 숫자를 제외한 문자를 의미한다.\D = [^0-9], \W = [^A-Za-z0-9]
[] 밖의 ^은 문자열의 시작을 의미한다./^https/
https로 시작하는지 검사
$은 문자열의 마지막을 의미한다.
/com$/
com으로 끝나는지 검사
\s
는 여러가지 공백문자를 의미한다. 즉 \s
는 [\t\r\n\v\f]
와 같은 의미다.
검색 대상 문자열이 알파벳 대소문자 또는 숫자로 시작하고 끝나며 4~10 자리인지/^[A-Za-z0-9]{4,10}/.test(id)
특수문자 포함 여부 검사(/[^A-Za-z0-9]/gi).test(target)
특수문자를 제거할 때는 String.prototype.replace 메서드를 사용한다.target.replace(/[^A-Za-z0-9]/gi, '');
String
배열에는 원본 배열을 직접 변경하는 메서드와 원본 배열을 직접 변경하지 않고 새로운 배열으 생성하여 반환하는 메서드가 있다.
하지만 String객체에는 원본 String래퍼 객체(String 메서드를 호출한 String 래퍼 객체)를 직접 변경하는 메서드는 존재하지 않는다.
즉, String 객체의 메서드는 언제나 새로운 문자열을 반환한다. 문자열은 변경 불가능한 원시 값이기 때문에 String 래퍼 객체도 읽기 전용 객체로 제공된다.
-> String 래퍼 객체는 읽기 전용 객체다. 즉 writable 프로퍼티 어트리뷰트 값이 false다.
Symbol
Symbol은 변경 불가능한 원시 타입의 값이다. 심벌 값은 다른 값과 중복되지 않는 유일무이한 값이다. 따라서 주로 이름의 충돌 위험이 없는 유일한 프로퍼티 키를 만들기 위해 사용한다.
이전에 알아본 것처럼 프로퍼티 키로 사용할 수 있는 값은 빈 문자열을 포함하는 모든 문자열 또는 심벌 값이다.
Symbol 값의 생성
심벌 값은 Symbol 함수를 호출하여 생성한다.
다른 원시값은 리터럴 표기법을 통해 값을 생성할 수 있지만 심벌 값은 Symbol 함수를 호출하여 생성해야 한다.
이때 생성된 심벌 값은 외부로 노출되지 않아 확인할 수 없으며, 다른 값과 절대 중복되지 않는 유일무이한 값이다.
const mySymbol = Symbol();
console.log(typeof mySymbol); // symbol
console.log(mySymbol); // Symbol()
생성자 함수로 객체를 생성하는것처럼 보이지만 Symbol 함수는 다른 생성자 함수들과 달리 new 연산자와 함께 호출하지 않는다.
new 연산자와 함께 생성자 함수 또는 클래스를 호출하면 객체가 생성되지만 심벌 값은 변경 불가능한 원시 값이다.
Symbol 함수에는 선택적으로 문자열을 인수로 전달할 수 있다.
이 문자열은 생성된 심벌 값에 대한 설명으로 디버깅 용도로만 사용되며, 심벌 값 생성에 어떠한 영향도 주지 않는다.
심벌 값에 대한 설명이 같더라도 생성된 심벌 값은 유일무이한 값이다.
const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');
console.log(sym1 === sym2); // false
심벌 값도 문자열, 숫자, 불리언과 같이 객체처럼 접근하면 암묵적으로 래퍼 객체를 생성한다.
const sym = Symbol('mySym');
// Symbol.prototype의 프로퍼티들
console.log(sym.description); // mySym
console.log(sym.toString()); // Symbol(mySym)
심벌 값은 암무적으로 문자열이나 숫자 타입으로 변환되지 않는다.
단 불리언 타입으로 는 암묵적으로 타입 변환된다. 이를 통해 if 등에서 존재 확인이 가능하다.
Symbol.for 메서드는 인수로 전달받은 문자열을 키로 사용하여 키와 심벌 값의 쌍드링 저장되어 있는 전역 심벌 레지스트리에서 해당 키와 일치하는 심벌 값을 검색한다.
- 검색에 성공하면 새로운 심벌 값을 생성하지 않고 검색된 심벌 값을 반환한다.
- 검색에 실패하면 새로운 심벌 값을 생성하여 Symbol.for 메서드의 인수로 전달된 키로 전역 심벌 레지스트리에 저장한 후, 생성된 심벌 값을 반환한다.
const s1 = Symbol.for('mySym'); // Symbol('mySym')과는 다르다
const s2 = Symbol.for('mySym');
console.log(s1 === s2); // true
Symbol 함수는 호출될 때마다 유일무이한 심벌 값을 생성한다.
이때 자바스크립트 엔진이 관리하는 심벌 값 저장소인 전역 심벌 레지스트리에서 심벌 값을 검색할 수 있는 키를 지정할 수 없으므로 전역 심벌 레지스트리에 등록되어 관리되지 않는다.
하지만 Symbol.for 메서드를 사용하면 애플리케이션 전역에서 중복되지 않는 유일무이한 상수인 심벌 값을 단 하나만 생성하여 전역 심벌 레지스트리를 통해 공유할 수 있다.
Symbol.keyFor 메서드를 사용하면 전역 심벌 레지스트리에 저장된 심벌 값읠 키를 추출할 수 있다.
const s1 = Symbol.for('mySymbol');
Symbol.keyFor(s1); // => mySymbol
// Symbol 함수를 호출하여 생성한 심벌 값은 전역 심벌 레지스트리에 등록되어 관리되지 않는다.
const s2 = Symbol('foo');
Symbol.keyFor(s2); // undefined
const Direction = {
UP: Symbol('up'),
DOWN: Symbol('down'),
LEFT: Symbol('left'),
RIGHT: Symbol('right'),
};
const myDirection = Direction.UP;
if (myDirection === Direction.UP) {
console.log('You are going UP');
}
// 만약 UP을 심벌이 아니라 1이라는 숫자로 했다면
// if (myDirection === 1) 를 실수로 비교한다거나 할 때
// true가 될 수도 있기 때문에 심벌을 사용하는 건가..?
심벌과 프로퍼티 키
객체의 프로퍼티 키는 빈 문자열을 포함하는 모든 문자열 또는 심벌 값으로 만들 수 있으며 동적으로 생성할 수도 있다.
심벌 값으로 프로퍼티 키를 동적으로 생성하여 프로퍼티를 만들어 보자.
심벌 값을 프로퍼티 키로 사용하려면 프로퍼티 키로 사용할 심벌 값에 대괄호를 사용해야 한다. 프로퍼티에 접근할 때도 마찬가지로 대괄호를 사용해야 한다.
const obj = {
[Symbol.for('mySymbol')]: 1
};
obj[Symbol.for('mySymbol')]; // 1
심벌 값을 유일무이한 값이므로 심벌 값으로 프로퍼티 키를 만든다면 다른 프로퍼티 키와 절대 충돌하지 않는다.
기존 프로퍼티 키와 충돌하지 않는 것은 물론, 미래에 추가될 어떤 프로퍼티 키와도 충돌할 위험이 없다.
심벌과 프로퍼티 은닉
심벌 값을 프로퍼티 키로 사용하여 생성한 프로퍼티는 for...in 문이나 Object.keys, Object.getOwnPropertyNames 메서드로 찾을 수 없다.
이처럼 심벌 값을 프로퍼티 키로 사용하여 프로퍼티를 생성하면 외부에 노출할 필요가 없는 프로퍼티를 은닉할 수 있다.
const obj = {
[Symbol('mySymbol')]: 1
};
for (const key in obj) {
console.log(key); // 아무것도 출력되지 않음
}
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertyNames(obj)); // []
하지만 프로퍼티를 완전하게 숨길 수 있는 것은 아니다.
Object.getOwnPropertySymbols 메서드를 사용하면 심벌 값을 프로퍼티 키로 사용하여 생성한 프로퍼티를 찾을 수 있다.
const obj = {
[Symbol('mySymbol')]: 1
};
// getOwnPropertySymbols 메서드는 인수로 전달한 객체의 심벌 프로퍼티 키를 배열로 반환한다.
console.log(Object.getOwnPropertySymbols(obj));
// [Symbol(mySymbol)]
const symbolKey1 = Object.getOwnPropertySymbols(obj)[0];
console.log(obj[symbolKey1]); // 1
심벌과 표준 빌트인 객체 확장
일반적으로 표준 빌트인 객체에 사용자 정의 메서드를 직접 추가하여 확장하는 것은 권장하지 않는다. 표준 빌트인 객체는 읽기 전용으로 사용하는 것이 좋다.
그 이유는 개발자가 직접 추가한 메서드와 미래에 표준 사양으로 추가될 메서드의 이름이 중복될 수 있기 때문이다.
하지만 중복될 가능성이 없는 심벌 값으로 프로퍼티 키를 생성하여 표준 빌트인 객체를 확장하면 표준 빌트인 객체의 기존 프로퍼티 키와 충돌하지 않는 것은 물론, 표준 사양의 버전이 올라감에 따라 추가될지 모르는 어떤 프로퍼티 키와도 충돌할 위험이 없어 안전하게 표준 빌트인 객체를 확장할 수 있다.
Array.prototype[Symbol.for('sum')] = function () {
return this.reduce((acc, cur) => acc + cur, 0);
}
console.log([1, 2][Symbol.for('sum')]()); // 3
Well-known Symbol
자바스크립트가 기본 제공하는 빌트인 심벌 값이 있다. 빌트인 심벌 값은 Symbol 함수의 프로퍼티에 할당되어 있다.
빌트인 심벌 값을 ECMAScript 사양에서는 Well-known Symbol 이라 부른다.
Well-known Symbol은 자바스크립트 엔진의 내부 알고리즘에 사용된다.
예를들어 Array, String, Map, Set 과 같이 for...of 문으로 순회 가능한 빌트인 이터러블은 Well-known Symbol인 Symbol.iterator를 키로 갖는 메서드를 가지며, Symbol.iterator 메서드를 호출하면 이터레이터를 반환하도록 규정되어 있다.
빌트인 이터러블은 이 규정 즉, 이터레이션 프로토콜을 준수한다.
만약 빌트인 이터러블이 아닌 일반 객체를 이터러블처럼 동작하도록 구현하고 싶다면 이터레이션 프로토콜을 따르면 된다.
ECMAScript 사양에 규정되어 있는 대로 Well-known Symbol인 Symbol.iterator를 키로 갖는 메서드를 객체에 추가하고 이터레이터를 반환하도록 구현하면 그 객체는 이터러블이 된다.
const iterable = {
[Symbol.iterator]() {
let cur = 1;
const max = 5;
return {
next() {
return {value: cur++, done: cur > max + 1 };
},
};
},
};
for (const num of iterable) {
console.log(num);
}
이때 이터레이션 프로토콜을 준수하기 위해 일반 객체에 추가해야 하는 메서드의 키 Symbol.iterator는 기존 프로퍼티 키 또는 미래에 추가될 프로퍼티 키와 절대로 중복되지 않을 것이다.
이처럼 심벌은 중복되지 않는 상수 값을 생성하는 것은 물론 기존에 작성된 코드에 영향을 주지 않고 새로운 프로퍼티를 추가하기 위해, 즉 하위 호환성을 보장하기 위해 도입되었다.
이터러블
이터레이션 프로토콜은 순회 가능한 데이터 컬렉션(자료구조)을 만들기 위한 규칙이다.
ES6 이전의 순회 가능한 데이터 컬렉션, 즉 배열, 문자열, 유사 배열 객체, DOM 컬렉션 등은 통일된 규약 없이 각자 나름의 구조를 가지고 for문, for...in문, forEach 메서드 등으로 다양한 방법으로 순회할 수 있었다.
ES6에서는 순회 가능한 데이터 컬렉션을 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for...of문, 스프레드 문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화했다.
이터레이션 프로토콜에는 이터러블 프로토콜과 이터레이터 프로토콜이 있다.
- 이터러블 프로토콜
Well-known Symbol인 Symbol.iterator를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이러한 규약을 이터러블 프로토콜이라 하며, 이터러블 프로토콜을 준수한 객체를 이터러블이라 한다.
이터러블은 for...of 문으로 순회할 수 있으며 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있다. - 이터레이터 프로토콜
이터러블의 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.
이터레이터는 next메서드를 소유하며 next메서드를 호출하면 이터러블을 순회하며 value 와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다.
이러한 규약을 이터레이터 프로토콜이라 하며, 이터레이터 프로토콜을 준수한 객체를 이터레이터라 한다. 이터레이터는 이터러블의 요소를 탐색하기 위한 포인터 역할을 한다.
const obj = { a: 1, b: 2 };
// 일반 객체는 Symbol.iterator 메서드를 구현하거나 상속받지 않는다.
// 따라서 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다.
console.log(Symbol.iterator in obj); // false
// 이터러블이 아닌 일반 객체는 for...of 문으로 순회할 수 없다.
for (const item of obj) {
console.log(item);
}
// 이터러블이 아닌 일반 객체는 배열 디스트럭처링 할당의 대상으로 사용할 수 없다.
const [a, b] = obj;
// 객체 리터럴 내부에서 스프레드 문법의 사요여을 허용한다.
console.log({...obj}); // {a:1, b:2} 이건 가능
이터러블의 Symbol.iterator 메서드가 반환한 이터레이터는 next메서드를 갖는다.
이터레이터의 next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터 역할을 한다.
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// OUTPUT
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
자바스크립트는 이터레이션 프로토콜을 준수한 객체인 빌트인 이터러블을 제공한다.
Array, String, Map, Set, typedArray, arguments
for...of 문
for...of 문은 이터러블을 순회하면서 이터러블의 요소를 변수에 할당한다.for(변수 선언문 of 이터러블) {...}
for...of 문은 for...in 문의 형식과 매우 유사하다.
for...in 문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumeable]]의 값이 true인 프로퍼티를 순회하면서 열거한다.
이때 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않는다.
for...of문은 내부적으로 이터레이터의 next 메서드를 호출하여 이터러블을 순회하며 next메서드가 반환한 이터레이터 리절트 객체의 value프로퍼티 값을 for...of문의 변수에 할당한다.
그리고 이터레이터 리절트 객체의 done 프로퍼티 값이 false이면 이터러블의 순회를 계속하고 true이면 이터러블의 순회를 중단한다.
for(const item of [1, 2, 3]) {
// item 변수에 순차적으로 1, 2, 3이 할당된다.
console.log(item);
}
// 사용자 정의 이터러블
const fibo = {
[Symbol.iterator]() {
let [pre, cur] = [0, 1];
const max = 10;
return {
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: cur >= max };
},
};
},
};
for (const num of fibo) {
console.log(num);
}
// 이터러블은 스프레드 문법의 대상이 될 수 있다.
const arr = [...fibo];
console.log(arr); // [1, 2, 3, 5, 8]
// 이터러블은 배열 디스트럭처링 할당의 대상이 될 수 있다.
const [first, second, ...rest] = fibo;
console.log(first, second, rest); // 1 2 [3, 5, 8]
이터러블을 생성하는 함수
const fibo = function (max) {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return {
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: cur >= max };
},
};
},
};
};
for (const item of fibo(15)) {
console.log(item);
}
// 1, 2, 3, 5, 8, 13
이터러블이면서 이터레이터인 객체
const fibo = function (max) {
let [pre, cur] = [0, 1];
return {
[Symbol.iterator]() {
return this;
},
next() {
[pre, cur] = [cur, pre + cur];
return { value: cur, done: cur >= max };
},
};
};
이터러블은 데이터 공급자의 역할을 한다.
배열이나 문자열 등은 모든 데이터를 메모리에 미리 확보한 다음 데이터를 공급한다.
하지만 이터러블은 지연 평가를 통해 데이터를 생성한다.
지연 평가는 데이터가 필요한 시점 이전까지는 데이터를 생성하지 않다가 데이터가 필요한 시점이 되면 그때야 비로소 데이터를 생성하는 기법이다.
즉 평가 결과가 필요할 때까지 평가를 늦추는 기법이 지연 평가다.
지연 평가를 사용하며네 불필요한 데이터를 미리 생성하지 않고 필요한 데이터를 필요한 순간에 생성하므로 빠른 실행 속도를 기대할 수 있고, 불필요한 메모리를 소비하지 않으며 무한도 표현할 수 있따는 장점이 있다.
스프레드 문법 ... 은 하나로 뭉쳐 있는 여러 값들의 집합을 펼쳐서 개별적인 값들의 목록으로 만든다.
스프레드 문법을 사용할 수 있는 대상을 for...of 문으로 순회할 수 있는 이터러블에 한정된다.
console.log(...[1, 2, 3]);
console.log(...'hello');
console.log(
...new Map([
['a', '1'],
['b', '2'],
])
);
console.log(...new Set([1, 2, 3]));
// OUTPUT
1 2 3
h e l l o
[ 'a', '1' ] [ 'b', '2' ]
1 2 3
스프레드 문법의 결과는 값이 아니다.
이는 스프레드 문법 ...이 피연산자를 연산하여 값을 생성하는 연산자가 아님을 의미한다. 따라서 스프레드 문법의 결과는 변수에 할당할 수 없다.
const list = ...[1, 2, 3]; // 에러
이처럼 스프레드 문법의 결과물은 값으로 사용할 수 없고, 다음과 같이 쉼표로 구분한 값의 목록을 사용하는 문법에서만 사용할 수 있다.
- 함수 호출문의 인수 목록
- 배열 리터럴의 요소 목록
- 객체 리터럴의 프로퍼티 목록
함수 호출문에서
const arr = [1, 2, 3];
console.log(Math.max(...arr)); // 3
function t(...rest) {
console.log(rest); // 1, 2, 3 => [1, 2, 3]
}
t(...arr); // [1, 2, 3] => 1, 2, 3
배열 리터럴 내부에서
const arr [...[1, 2], ...[3, 4]];
console.log(arr); // [1, 2, 3, 4]
const arr1 = [1, 4];
const arr2 = [2, 3];
arr1.splice(1, 0, ...arr2);
console.log(arr1); // [1, 2, 3, 4]
// arr1.splice(1, 0, arr2) 의 경우 => [1, [2, 3], 4] 가 된다
function sum() {
return [...arguments].reduce((pre, cur) => pre + cur, 0);
}
// OR
const sum = (...args) => args.reduce((pre, cur) => pre + cur, 0);
console.log(sum(1, 2, 3)); // 6
객체 리터럴 내부에서
// 스프레드 프로퍼티
// 객체 복사(얕은 복사)
const obj = {x: 1, y: 2};
const copy = {...obj};
console.log(copy); // {x:1, y: 2}
console.log(obj === copy); // false
// 객체 병합
const merged = {x:1, y:2, ...{a:3, b:4}};
console.log(merged); // {x:1, y:2, a:3, b:4}
// 객체 병합. 프로퍼티가 중복되는 경우 뒤에 위치한 프로퍼티가 우선권을 가진다.
const merged = {...{x:1, y:2}, ...{y:10, z:3}};
console.log(merged); // {x:1, y:10, z:3}
디스트럭처링
배열 디스트럭처링
디스트럭처링 할당은 구조화된 배열과 같은 이터러블 또는 객체를 destructuring 하여 1개 이상의 변수에 개별적으로 할당하는 것을 말한다.
배열과 같은 이터러블 또는 객체 리터럴에서 필요한 값만 추출하여 변수에 할당할 때 유용하다.
const arr = [1, 2, 3];
// 변수 one, two, three를 선언하고 배열 arr을 디스트럭처링하여 할당한다
// 이떄 할당 기준을 배열의 인덱스다.
const [one, two, three] = arr;
console.log(one, two, three); // 1 2 3
배열 디스트럭처링 할당을 위해서는 할당 연산자 왼쪽에 값을 할당받을 변수를 선언해야 한다.
이때 변수를 배열 리터럴 형태로 선언한다.
const [x, y] = [1, 2];
let x, y;
[x, y] = [1, 2];
배열 디스트럭처링 할당의 기준을 배여르이 인덱스다. 즉 순서대로 할당된다.
이때 변수의 개수와 이터러블의 요소 개수가 반드시 일치할 필요는 없다.
const [a, b] = [1, 2];
console.log(a, b); // 1 2
const [c, d] = [1];
console.log(c, d); // 1 undefined
const [e, f] = [1, 2, 3];
console.log(e, f); // 1 2
const [g, ,h] = [1, 2, 3];
console.log(g, h); // 1 3
배열 디스트럭처링 할당을 위한 변수에 기본값을 설정할 수 있다.
const [a, b, c = 3] = [1, 2];
console.log(a, b, c); // 1 2 3
// 기본값보다 할당된 값이 우선한다.
const [e, f = 10, g = 3] = [1, 2];
console.log(e, f, g); // 1 2 3
배열 디스트럭처링 할당을 위한 변수에 Rest 파라미터와 유사하게 Rest 요소 ...를 사용할 수 있다.
Rest 요소는 Rest 파라미터와 마찬가지로 반드시 마지막에 위치해야 한다.
const [x, ...y] = [1, 2, 3];
console.log(x, y); // 1 [2, 3]
객체 디스트럭처링
ES6의 객체 디스트럭처링 할당은 객체의 각 프로퍼티를 객체로부터 추출하여 1개 이상의 변수에 할당한다.
이때 객체의 디스터럭처링 할당의 대상(할당문의 우변)은 객체이어야 하며, 할당 기준은 프로퍼티 키다.
즉, 순서는 의미가 없으며 선언된 변수 이름과 프로펕 기키가 일치하면 할당된다.
const user = {firstName: 'first', lastName: 'last'};
const { lastName, firstName } = user;
console.log(firstName, lastName); // first, last
배열 디스트럭처링 할당과 마찬가지로 객체 디스트럭처링 할당을 위해서는 할당 연산자의 왼쪽에 프로퍼티를 할당받을 변수를 선언해야 한다.
이때 변수를 객체 리터럴 형태로 선언한다.
객체의 프로퍼티 키와 다른 변수 이름으로 프로퍼티 값을 할당받으려면 다음과 같이 변수를 선언한다.
const user = {firstName: 'first', lastName: 'last'};
const { lastName: ln, firstName: fn } = user;
console.log(fn, ln); // first, last
// 객체 디스트럭처링 할당을 위한 변수에 기본값을 설정할 수 있다.
const { lastName: ln, firstName: fn = 'fn' } = { lastName: 'me' };
console.log(fn, ln); // fn, me
객체 디스트럭처링 할당은 객체에서 프로퍼티 키로 필요한 프로퍼티 값만 추출하여변수에 할당하고 싶을 때 유용하다.
const str = 'Hello';
const {length} = str;
console.log(length); // 5
// 객체를 인수로 전달받는 함수의 매개변수에도 사용할 수 있다.
function printTodo({ content, completed }) {
console.log(`할일 ${content}은 ${completed}`);
}
printTodo({ id: 1, content: 'HTML', completed: true });
// 할일 HTML은 true
배열의 요소가 객체인 경우 배열 디스트럭처링 할당과 객체 디스트럭처링 할당을 혼용할 수 있다.
const todos = [
{ id: 1, content: 'html', completed: true},
{ id: 2, content: 'css', completed: true},
]
const [, {id}] = todos;
console.log(id); // 2
객체 디스트럭처링 할당을 위한 변수에 Rest 파라미터나 Rest 요소와 유사하게 Rest 프로퍼티...을 사용할 수 있다.
Rest프로퍼티는 Rest파라미터나 Rest요소와 마찬가지로 만드시 마지막에 위치해야 한다.
const {x, ...rest} = {x:1, y:2, z:3};
console.log(x, rest); // 1 {y: 2, z: 3}
타이머
자바스크립트는 타이머를 생성할 수 있는 타이머 함수 setTimeout 과 setInterval, 타이머를 제거할 수 있는 clearTimeout과 clearInterval을 제공한다.
타이머함수는 ECMAScript 사양에 정의된 빌트인 함수가 아니다. 하지만 브라우저 환경과 Node.js 환경에서 모둔 전역 객체의 메서드로서 타이머 함수를 제공한다. 즉 타이머 함수는 호스트 객체다.
setTimeout과 setInterval은 모두 일정 시간이 경과된 이후 콜백 함수가 호출되도록 타이머를 생성한다. 다시 말해, 타이머 함수 setTimeout과 setinterval이 생성한 타이머가 만료되면 콜백 함수가 호출된다.
setTimeout함수가 생성한 타이머는 단 한 번 동작하고, setInterval 함수가 생성한 타이머는 반복 동작한다.
자바스크립트 엔진은 단 하나의 실행 컨텍스트 스택을 갖기 때문에 두 가지 이상의 태스크를 동시에 실행할 수 없다.
자바스크립트 엔진은 싱글 스레드로 동작한다. 이런 이유로 타이머 함수 setTimeout과 setInterval은 비동기 처리 방식으로 동작한다.
const timeoutId = setTimeout(func|code[,delay, param1,,,]);
delay는 생략한 경우 기본값 0이 지정된다.
- delay 시간이 설정된 타이머가 만료되면 콜백 함수가 즉시 호출되는 것이 보장되지는 않는다. delay시간은 태스크 큐에 콜백 함수를 등록하는 시간을 지연할 뿐이다.
- delay가 4ms 이하인 경우 최소 지연 시간 4ms가 지정된다.
- param... 호출 스케줄링된 콜백 함수에 전달해야 할 인스가 존재하는 경우 세 번째 이후의 인수로 전달할 수 있다.
setTimeout 함수는 생성된 타이머를 식별할 수 있는 고유한 타이머 id를 반환한다.
setTimeout 함수가 반환한 타이머 id는 브라우저의 경우 숫자이며 Node.js 환경인 경우 객체다.
setTimeout 함수가 반환한 타이머 id를 clearTimeout 함수의 인수로 전달하여 타이머를 취소할 수 있다.
clearTimeout 함수는 호출 스케줄링을 취소한다.
const timerId = setTimeout(() => console.log('hi'), 1000);
clearTimeout(timerId);
setInterval 함수는 두 번째 인수로 전달받은 시간(ms)으로 반복 동작하는 타이머를 생성한다. 타이머가 만료될 때마다 첫 번째 인수로 전달받은 콜백 함수가 반복 호출된다.
이는 타이머가 취소될 때까지 계속된다.
let count = 1;
const timeoutId = setInterval(() => {
console.log(count);
if(count++ === 5) clearInterval(timeoutId);
}, 1000);
디바운스와 스로틀
scroll, resize, inpuut, mousemove 같은 이벤트는 짤븡ㄴ 시간 간격으로 연속해서 발생한다.
이러한 이벤트에 바인딩한 이벤트 핸들러는 과도하게 호출되어 성능에 문제를 일으킬 수 있다.
디바운스와 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 과도한 이벤트 핸들러의 호출을 방지하는 프로그래밍 기법이다.
디바운스는 짧은 시간 간격으로 이벤트가 연속해서 발생하면 이벤트 핸들러를 호출하지 않다가 일정 시간이 경과한 이후에 이벤트 핸들러가 한 번만 호출되도록 한다.
즉, 디바운스는 짧은 시간 간격으로 발생하는 이벤트를 그룹화해서 마지막에 한 번만 이벤트 핸들러가 호출되도록 한다.
스로틀은 짧은 시간 간격으로 이벤트가 연속 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 한다. 즉, 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해서 일정 시간 단위로 이벤트 핸들러가 호출되도록 호출 주기를 만든다.
실무에서는 Underscore, Lodash의 함수를 이용하는 것이 좋다.
비동기 프로그래밍
실행 컨텍스트에서 살펴본 바와 같이 함수를 호출하면 함수 코드가 평가되어 함수 실행 컨텍스트가 생성된다.
이때 생성된 함수 실행 컨텍스트는 실행 컨텍스트 스택(콜 스택이라고도 부른다)에 푸시되고 함수 코드가 실행된다.
함수 코드의 실행이 종료되면 함수 실행 컨텍스트는 실행 컨텍스트에서 팝되어 제거된다.
자바스크립트 엔진은 단 하나의 실행 컨텍스트를 갖는다. 이는 함수를 실행할 수 있는 창구가 단 하나이며, 동시에 2개 이상의 함수를 실행할 수 없다는 것을 의미한다.
실행 컨텍스트 스택의 최상위 요소인 "실행 중인 컨텍스트"를 제외한 모든 실행 컨텍스트는 모두 실행 대기 중인 태스크 들이다.
대기 중인 태스크들은 현재 실행 중인 실행 컨텍스트가 팝되어 실행 컨텍스트 스택에서 제거되면 비로소 실행 되기 시작한다.
-> 싱글 스레드 방식으로 동작한다.
싱글 스데르 방식은 한 번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 블로킹이 발생한다.
동기 처리
실행 중인 태스크가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식을 동기처리 라고한다. 동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료할 때까지 이후 태스크들이 블로킹되는 단점이 있다.
비동기 처리
현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기 처리라고 한다.
비동기 처리 방식은 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하므로 블로킹이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서가 보장되지 않는 단점이 있다.
비동기 처리를 수행하는 비동기 함수는 전통적으로 콜백 패턴을 사용한다. 비동기 처리를 위한 콜백 패턴은 콜백 헬을 발생시켜 가독성을 나쁘게 하고, 비동기 처리 중 발생한 에러의 예외 처리가 곤란하며, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다.
타이머 함수인 setTimeout, setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작한다.
비동기 처리는 이벤트 루프와 태스크 큐와 깊은 관계가 있다.
이벤트 루프와 태스크 큐
자바스크립트의 동시성을 지원하는 것이 바로 이벤트 루프다.
구글의 V8 자바스크립트 엔진을 비롯한 대부분의 자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있다.
- 콜 스택
실행 컨텍스트에서 살펴본 바와 같이 소스코드 평가 과정에서 생성된 실행 컨텍스트가 추가되고 제거되는 스택 자료구조인 실행 컨텍스트 스택이 발로 콜 스택이다.
함수를 호출하면 함수 실행 컨텍스트가 순차적을오 콜 스택에 푸시되어 순차적으로 실행된다. 자바스크립트 엔진은 단 하나의 콜 스택을 사용하기 때문에 최상위 실행 컨텍스트가 종료되어 콜 스택에서 제거되기 전까지는 다른 어떤 태스크도 실행되지 않는다. - 힙
힙은 객체가 저장되는 메모리 공간이다. 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 잠조한다.
메모리에 값을 저장하려면 먼저 값을 저장할 메모리 공간의 크기를 결정해야 한다. 객체는 원시 값과 달리 크기가 정해져 있지 않으므로 할당해야 할 메모리 공간의 크기를 런타임에 결정해야 한다. 따라서 객체가 저장되는 메모리 공간인 힙은 구조화되어있지 않다는 특징이 있다.
이처럼 콜 스택과 힙으로 구성되어 있는 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 실행할 뿐이다.
비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저 또는 Node.js가 담당한다.
예를 들어, 비동기 방식으로 동작하는 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진인 담당하지만 호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저 또는 Node.js 가 담당한다.
이를 위해 브라우저 환경은 테스크 큐와 이벤트 루프를 제공한다.
- 태스크 큐
setTimeout이나 setInterval과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역이다. 태스크 큐와는 별도로 프로미스의 후속 처리 메서드의 콜백 함수가 일시적으로 보관되는 마이크로태스크 큐도 존재한다. - 이벤트 루프
이벤트 루프는 콜 스택에 현재 실행중인 실행 컨택스트가 있는지, 그리고 태스크 큐에 대기중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해서 확인한다. 만약 콜 스택이 비어있고 태스크 큐에 대기중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기중인 함수를 콜 스택으로 이동시킨다. 이때 콜 스택으로 이동한 함수는 실행된다. 즉, 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다.
setTimeout의 경우
-> 타이머를 설정하고 타이머의 만료를 기다린다. 이후 타이머가 만료되면 콜백 함수가 태스크 큐에 푸시되어 대기하게 된다.
setTimeout(() => console.log('HELLO'), 500);
for (let i = 0; i < 10000000000; i++) {
continue;
}
console.log('HI');
for (let i = 0; i < 10000000000; i++) {
continue;
}
// HI
// HELLO
// 메인 실행 컨텍스트가 끝나야 setTimeout의 콜백이 실행됨..
비동기 한수인 setTimeout의 콜백 함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비게 되면, 다시 말해서 전역 코드 및 명시적으로 호출된 함수가 모두 종료하면 비로소 콜 스택에 푸시되어 실행된다.
싱글 스레드 방식으로 동작하는 것은 브라우적 아니라 브라우저에 내장된 자바스크립트 엔진이라는 것에 주의하기 바란다. 만약 모든 자바스크립트 코드가 자바스크립트 엔진에서 싱글 스데르 방식으로 동작한다면 자바스크립트는 비동기로 동작할 수 없다. 즉 자바스크립트 엔진은 싱글 스레드로 동작하지만 브라우저는 멀티 스레드로 동작한다.
REST => HTTP를 기반으로 클라이언트가 서버의 리소스에 접근하는 방식을 규정한 아키텍쳐
REST API => REST를 기반으로 서비스 API를 구성한 것을 의미함
REST API의 구성
구성 요소 | 내용 | 표현 방법 |
---|---|---|
자원(Resource) | 자원 | URI(엔드포인트) |
행위(Verb) | 자원에 대한 행위 | HTTP 요청 메서드 |
표현(Representations) | 자원에 대한 행위의 구체적 내용 | 페이로드 |
프로미스
전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하며 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다.
ES6에서는 비동기 처리를 위한 또 다른 패턴으로 Promise를 도입했다.
비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료된다.
즉, 비동기 함수 내부의 비동기로 동작하는 코드는 비동기 함수가 종료된 이후에 완료된다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않는다.
따라서 비동기 함수의 처리 결과에 대한 후속 처리는 비동기 함수 내부에서 수행해야 한다.
이때 비동기 함수를 범용적으로 사용히기 위해 비동기 함수에 비동기 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달받는 것이 일반적이다. 필요에 따라 비동기 처리가 성공하면 호출될 콜백 함수와 비동기 처리가 실패하면 호출될 콜백 함수를 전달할 수 있다.
이처럼 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행한 느 비동기 함수가 비동기 처리 결과를 가지고 또 다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상이 발생하는데, 이를 콜백 헬이라고 한다.
get('/step1', (a) => {
get(`/step2/${a}`, (b) => {
get(`/sept3/${b}`, (c) => {
get(`/step4/${c}`, (d) => {
console.log(d);
});
});
});
});
비동기 처리를 위한 콜백 패턴의 문제점 중에서 가장 심각한 것은 에러 처리가 곤란하다는 것이다.
try {
setTimeout(() => {
throw new Error('Error!');
}, 1000);
} catch (e) {
console.error(e); // 에러를 캐치하지 못한다.
}
에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향으로 전파된다. 하지만 위와 같이 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout함수가 아니다. 따라서 setTimeout함수의 콜백 함수가 발생시킨 에러는 catch 블록에서 캐치되지 않는다.
Promise 생성자 함수는 비동기 처리를 수행할 콜백 함수를 인수로 전달받는데 이 콜백 함수는 resolve와 reject함수를 인수로 전달받는다.
const promiseGet = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(xhr.DONE.status));
}
};
});
};
promiseGet('http://example.com');
Promise생성자 함수가 인수로 전달받은 콜백 함수 내부에서 비동기 처리를 수행한다.
이때 비동기 처리가 성공하면 콜백 함수의 인수로 전달받은 resolve함수를 호출하고, 비동기 처리가 실패하면 reject 함수를 호출한다.
프로미스에는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태 정보를 갖는다
프로미스의 상태 정보 | 의미 | 상태 변경 조건 |
---|---|---|
pending | 비동기 처리가 아직 수행되지 않은 상태 | 프로미스가 생성된 직후 기본 상태 |
fulfilled | 비동기 처리가 수행된 상태(성공) | resolve함수 호출 |
rejected | 비동기 처리가 수행된 상태(실패) | reject함수 호출 |
생성된 직후의 프로미스는 기본적으로 pending이다. 이후 비동기 처리가 수행되면 비동기 처리 결과에 따라 다음과 같이 프로미스의 상태가 변경된다.
- 비동기 처리 성공 => resolve 함수를 호출해 프로미스를 fulfilled 상태로 변경한다.
- 비동기 처리 실패 => reject 함수를 호출해 프로미스를 rejected 상태로 변경한다.
프로미스는 pending 상태에서 fulfilled 또는 rejected 상태, 즉 settled 상태로 변화할 수 있다.
하지만 일단 settled 상태가 되면 더는 다른 상태로 변화할 수 없다.
프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과(result)도 상태로 갖는다.
프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체다.
프로미스의 후속 처리 메서드
예를 들어 프로미스가 fulfilled 상태가 되면 프로미스의 처리 결과를 가지고 무언가를 해야 하고, 프로미스가 rejected 상태가 되면 프로미스의 처리 결과(에러)를 가지고 에러 처리를 해야한다.
이를 위해 프로미스는 후속 메서드then, catch, finally를 제공한다.
프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 성택적으로 호출된다.
이때 후속 처리 메서드의 콜백 함수에 프로미스의 처리 결과가 인수로 전달된다.
모든 후속 처리 메서드는 프로미스를 반화니하며 비동기로 동작한다.
Promise.prototype.then
then 메서드는 두 개의 콜백 함수를 인수로 전달받는다.
- 첫 번째 콜백 함수는 프로미스가 fulfilled 상태가 되면 호출된다. 이때 콜백 함수는 프로미스으 비디오기 처리 결과를 인수로 전달받는다.
- 두 번째 콜백 함수는 프로미스가 rejected 상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다.
new Promise((resolve) => resolve('fulfilled')).then(
(v) => console.log(v),
(e) => console.error(e)
); // fulfilled
new Promise((_, reject) => reject(new Error('rejected'))).then(
(v) => console.log(v),
(e) => console.error(e)
); // Error: rejeted
then 함수는 언제나 프로미스를 반환한다.
만약 then 메서드으 키로백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고, 콜백 함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성해 반환한다.
Promise.prototype.catch
catch 메서드는 한 개의 콜백 함수를 인수로 전달받는다.
catch 메서드의 콜백 함수는 프로미스가 rejected 상태인 경우만 호출된다.
new Promise((_, reject) => reject(new Error('rejected')))
.catch((e) => console.error(e)); // Error: rejeted
Promise.prototype.finally
finally 메서드는 한 개의 콜백 함수를 인수로 전달받는다. finally 메서드의 콜백 함수는 프로미스의 성공 또는 실패와 상관없이 무조건 한 번 호출된다.
new Promise(() => {})
.finally(() => console.log('finally')); // finally
then 메서드의 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.
catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러 뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.
또한 then 메서드에 두 번째 콜백함수를 전달하는 것보다 catch 메서드르 사용하는 것이 가독성이 좋고 명확하다.
Promise.all은 여러 개의 비동기 처리를 병렬로 처리하고 처리에 걸리는 시간에 상관없이 결과 배열의 순서를 보장한다.
마이크로태스크 큐
setTimeout(() => console.log(1), 0);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// 2, 3, 1
마이크로태스크 큐는 태스크 큐와는 별도의 큐다. 마이크로태스크 큐에는 프로미스의 후속 처리 메서드의 콜백 함수가 일시 저장된다. 그 외의 비동기 함수의 콜백 함수나 이벤트 핸들러는 태스크 큐에 일시 저장된다.
콜백 함수나 이벤트 핸들러를 일시 저장한다는 점에서 태스크 큐와 동일하지만 마이크로태스크 큐는 태스크 큐보다 우선순위가 높다. 즉 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는함수를 가져와 실행한다.
제너레이터와 async/await
제너레이터란 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수다.
- 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다.
일반 함수를 호출하면 제어권이 함수에게 넘어가고 함수 코드를 일괄 실행한다. 즉, 함수 호출자는 함수를 호출한 이후 함수 실행을 제어할 수 없다.
제너레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있다. 다시 말해, 함수 호출자가 함수 실행을 일시 중지시키거나 재개시킬 수 있다.
이는 함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도할 수 있다는 것을 의미한다. - 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.
일반 함수를 호출하면 매개변수를 통해 함수 외부에서 값을 주입받고 함수 코드를 일괄 실행하여 결과값을 함수 외부로 반환한다.
즉, 함수가 실행되고 있는 동안에는 함수 외부에서 함수 내부로 값을 전달하여 함수의 상태를 변경할 수 없다.
제너레이터 함수는 함수 호출자와 양방향으로 함수의 상태를 주고받을 수 있다.
다시 말해, 제너레이터 함수는 함수 호출자에게 상태를 전달할 수 있고 함수 호출자로부터 상태를 전달받을 수도 있다. - 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
일반 함수를 호출하면 함수 코드를 일괄 실행하고 값을 반환한다.
하지만 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환한다.
제너레이터 함수는 function*
키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다.
화살표 함수로 정의할 수 없고, new 연산자와 함께 생성자 함수로 호출할 수 없다.
function* genDecFunc() {
yield 1;
}
const obj = {
* genObjMethod() {
yield 1;
}
}
class MyClass {
* genClassMethod() {
yield 1;
}
}
function* genFunc() {
yield 1;
yield 2;
}
const generator = genFunc()
console.log(Symbol.iterator in generator); // true
console.log('next' in generator); // true
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.return('End!')); // {value: 'End!', done: true}
console.log(generator.throw('Error!')); // {value: undefined, done: true}
제너레이터 객체는 next 메서드를 갖는 이터레이터지만 이터레이터에 없는 return, throw 메서드를 갖는다.
제너레이터 객체의 세 개의 메서드를 호출하면 다음과 같이 동작한다.
- next 메서드를 호출하면 제너레이터 함수의 yield 표현식까지 코드 블록을 실행하고 yield된 값을 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.
- return 메서드를 호출하면 인수로 전달받은 값을 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.
- throw 메서드를 호출하면 인수로 전달받은 에러를 발생시키고 undefined를 value값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.
함수의 마지막까지 실행되면 value는 리턴값, done은 true가 된다.
이터레이터의 next 메서드와 달리 제너레이터 객체의 next 메서드에는 인수를 전달할 수 있다.
제너레이터의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 할당한다.
yield 표현식을 할당받는 변수에 yield 표현식의 평가 결과가 할당되지 않는 것에 주의하기 바란다.
function* gen() {
const x = yield 1;
const y = yield (x + 10);
return x + y;
}
const g = gen();
g.next(); // value: 1, done: false
g.next(10); // value: 20, done: false
g.next(20); // value: 30, done: true
const fib = (function* () {
let [pre, cur] = [0, 1];
while(true) {
[pre, cur] = [cur, per + cur];
yield cur;
}
}());
for (const num of fib) {
if(num > 10000) break;
console.log(num);
}
const async = (generatorFunc) => {
const generator = generatorFunc();
const onResolved = (arg) => {
const result = generator.next(arg);
return result.done
? result.value
: result.value.then((res) => onResolved(res));
};
return onResolved;
};
async(function* fetchTodo() {
const url = 'url';
const response = yield fetch(url);
const todo = yield response.json();
console.log(todo);
})();
async/await
async/await 은 프로미스를 기반으로 동작한다.
async/await을 사용하면 프로미스의 then/catch/finally 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 마치 동기 처리처럼 프로미스를 사용할 수 있다.
다시 말해, 프로미스의 후속 처리 메서드 없이 마치 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있다.
async function fetchTodo() {
const url = 'url';
const response = await fetch(url);
const todo = await response.json();
console.log(todo);
}
fetchTodo();
await 키워드는 반드시 async 함수 내부에서 사용해야 한다.
async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다.
async 함수가 명시적으로 프로미스를 반환하지 않더라고 async 함수는 암묵적으로 반환값을 resolve 하는 프로미스를 반환한다.
await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve 한 처리 결과를 반환한다.
await 키워드는 반드시 프로미스 앞에서 사용해야 한다.
async/await 에서 에러 처리는 try...catch문을 사용할 수 있다.
Error 객체
Error 생성자 함수는 에러 객체를 생성한다.
Error 생성자 함수에는 에러를 상세히 설명하는 에러 메시지를 인수로 전달할 수 있다.
const error = new Error('invalid');
Error 생성자 함수가 생성한 에러 객체는 message 프로퍼티와 stack 프로퍼티를 갖는다.
message 프로퍼티의 값은 Error 생성자 함수에 인수로 전달한 에러 메시지이고, stack 프로퍼티의 값은 에러를 발생시킨 콜 스택의 호출 정보를 나타내는 문자열이다.
에러를 발생시키려면 try 코드 블록에서 throw문으로 에러 객체를 던져야 한다.throw 표현식;
throw문의 표현식은 어떤 값이라도 상관없지만 일반적으로 에러 객체를 지정한다.
에러를 던지면 catch 문의 에러 변수가 생성되거 던져진 에러 객체가 할당된다.
에러는 호출자 방향으로 전파된다. 즉, 콜 스택의 아래 방향으로 전파된다.
이처럼 throw된 에러를 캐치하지 않으면 호출자 방향으로 전파된다. 이때 throw 된 에러를 캐치하여 적절히 대응하면 플고그램을 강제 종료시키지 않고 코드의 실행 흐름을 복구할 수 있다.
throw된 에러를 어디에서도 캐치하지 않으면 프로그램은 강제 종료된다.
주의할 것은 비동기 함수인 setTimeout이나 프로미스 후속 처리 메서드의 콜백 함수는 호출자가 없다는 것이다.
setTimeout이나 프로미스의 후속 처리 메서드의 콜백 함수는 태스크 큐나 마이크로태스크 큐에 일시 저장되었다가 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행된다.
이때 콜 스택에 푸시된 콜백 함수의 실행 컨텍스트는 콜 스택의 가장 하부에 존재하게 된다.
따라서 에러를 전파할 호출자가 존재하지 않는다.