모던 JavaScript 튜토리얼 파트 1 :: 6장 "함수 심화학습" 정리
모던 JavaScript 튜토리얼 파트 1의 6장을 읽으면서 정리하는 글입니다.
1. 재귀와 스택
자바스크립트는 재귀 함수의 깊이를 제한한다. (엔진에 따라 다르지만 만개 정도는 확실히 허용한다고 합니다.)
function A(i) {
i = i - 1;
if (i < 0)
return 0;
return A(i);
}
const result = A(10_000); // 허용!
const result = A(10_956); // 허용!
const result = A(10_957); // Uncaught RangeError: Maximum call stack size exceeded
크롬에서 간단하게 검사툴로 돌려보니깐 10,956까지만 허용하네용.
꼬리 재귀 최적화를 수행하긴 한다지만 간단한 경우만 지원된다고 하네용.
재귀 함수가 실행되면?
실행 컨텍스트란?
함수 실행에 대한 세부 정보를 담고 있는 내부 데이터 구조이다.
* 함수는 호출될 때마다 하나의 실행 컨텍스트가 생성된다.
A 함수가 실행되던 도중 내부에 B라는 함수의 중첩 호출이 있을 때는 아래와 같은 절차가 수행된다고 합니다.
A 함수의 실행이 일시 중지됩니다.
중지된 함수와 연관된 실행 컨텍스트는 실행 컨텍스트 스택(execution context stack == call stack)에 Push됩니다.
중첩 호출(B함수)이 실행됩니다.
중첩 호출 실행이 끝난 이후 실행 컨텍스트 스택에서 일시 중단한 함수의 실행 컨텍스트를 Pop합니다. 그리고 중단되었던 A 함수의 실행을 다시 이어갑니다.
2. 나머지 매개변수와 스프레드 문법
자바스크립트의 내장 함수는 보통 인수의 제한을 두지 않는다고 합니다.
max, min, 등을 쓸 때 파이썬이랑 달라서 항상 멈칫하는 부분이기도 합니당.
파이썬은 값자체가 아닌 iterable을 여러개 받지영.
자바스크립트는 나머지 매개변수를 ...연산자로 받을 수 있습니다.
function sum(a, b, ...etc) {
console.log(a); // 1
console.log(b); // 2
console.log(etc); // [3, 4, 5]
}
sum(1, 2, 3, 4, 5)
예전에는 arguements를 사용했습니다.
function legacy_sum() {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments);
}
legacy_sum(1, 2, 3, 4, 5)
보시다시피 arguements 객체는 이터러블이긴하지만 유사 배열이라서 map, filter와 같은 메서드는 사용할 수 없습니다.
그리고 Arrow 함수는 arguements 객체를 지원하지 않습니다. 외부 scope에 있는 arguements를 가져올 수 있으니 주의해야 합니다.
배열을 인수 목록으로 확장(spread)시키고 싶을 때는 spread 연산자를 사용합니다.
let arr1 = [1, 2, 3];
let arr2 = [5, 6, 7];
console.log([...arr1, 4, ...arr2];
spread 문법은 내부적으로 이터러블을 사용합니다. 따라서 for ... of와 같은 원리입니다.
그렇기 때문에 문자열도 간단하게 character들의 배열로 만들 수 있습니다.
Array.from도 이터러블 객체인 문자열을 배열로 바꿔줍니다.
let myString = "abc"
console.log([...myStr]);
console.log(Array.from(myStr));
하지만 Array.from은 이터러블이 아닌 유사 배열 객체에도 동작한다고 합니다. 그래서 보편적으로 무언가를 배열로 바꾸고자 한다면 Array.from을 사용한다고 합니다.
Array.from이 자주 보이던데.. 왜그런가 했더니 이런 이유가 있었네용..!
객체나 배열을 복사할 때도 spread 연산자를 사용하는 것을 자주 봤습니다.
Object.assign도 자주 봤었는데요.
let objCopy = Object.assign({a: 1, b: 2}, obj);
let arrCopy = Object.assign([a, b, c,d ], arr);
이것도 특별한 이유가 있나 했지만 책에서는 문법이 간편해서 spread 연산자로 자주한다고 합니다.
이것도 다 copy가 되지 않습니다. nested된 형태를 복사할 때는 structured clone이라는 것이 있습니다.
https://www.builder.io/blog/structured-clone
예전에 deep copy 공부하면서 누군가 이런저런 차이점은 상세하게 적을 것을 보면서 공부했었습니다. 그런데 다 까먹었습니다.. 나중에 다시 정리해보도록하겠습니다.
3. 변수의 유효범위와 클로저
글 내용이 많아져서 다른 글로 분리했습니다 ㅎㅎ.
https://coding-groot.tistory.com/189
4. 오래된 var
var에는 블록 스코프가 없습니다.
- 함수 or 전역 스코프입니다.
- 블록 밖에서 접근 가능합니다.
var는 중복 선언을 허용합니다.
var는 선언하기 전에 사용할 수 있습니다.
var로 선언한 모든 변수는 함수의 최상위로 호이스팅됩니다.
var도 블록 레벨 스코프를 가질 수 있도록 생각해서 만들어낸 방법이 IIFE입니다.
(async () {
try {
// do await
} catch(e) {
// handle e
}
})();
요즘은 top level에서 비동기를 바로 쓸 수 없어서 쓸 때말고는 자주 못 봤습니다.
이것도 이제 최신 문법을 통해 사라지게 되지 않을까요.
https://tc39.es/proposal-top-level-await/
5. 전역 객체
전역 객체를 사용하면 어디서든 접근 가능한 변수를 만들 수 있습니다.
브라우저에서는 모듈이 아니라면 var로 선언한 전역 변수는 전역 객체의 프로퍼티가 됩니다.
전역 객체는 globalThis라는 보편적인 이름으로 불리는데 관습적으로 브라우저는 window, 노드에서는 global이라고 부릅니다.
(globalThis는 제안 목록에 추가 된 지 얼마 안 된 기능이라서 크로미움 기반 브라우저만 지원한다고 하네용..)
6. 객체로서의 함수와 기명 함수 표현식
자바스크립트의 객체는 일급 객체라서 값으로 다룰 수 있습니다.
contextual name 기능으로 익명 함수도 컨텍스트로부터 이름을 가져온다고 합니다.
function f(lamdaFunc = function() {}) {
alert(lamdaFunc.name); // lamdaFunc
}
f();
묘하네요..
length 프로퍼티로 나머지 매개변수를 제외한 함수 선언부에 있는 인수의 수도 가져올 수 있습니다.
7. new Function 문법
함수는 new Function 문법으로도 만들 수 잇습니다.
let sum = new Function('a', 'b', 'return a + b');
함수는 프로퍼티 [[Environment]]에 저장된 정보를 이용해 자기가 어디로부터 생성됐는지 기억합니다.
[[Environment]]는 함수가 만들어진 LexicalEnvironment를 참조합니다.
하지만 new Function을 이용해 만든 함수는 [[Environment]] 프로퍼티가 전역 렉시컬 환경이 됩니다. 그렇기 때문에 외부 변수를 사용할 수 없업입니다.
책에 따르면 압축기에 의한 에러를 조심해야하는 번거로움이 있지만 외부에 대한 참조를 할 수 없는 특징이 에러를 예방해준다는 관점에서 도움이 된다고 합니다.
8. setTimeout과 setInterval을 이용한 호출 스케줄링
자바스크릡트는 setTimeout과 setInterval 함수로 일정 시간이 지난 후에 함수를 호출 할 수 있습니다.
이때 스케줄링에 관한 명세는 따로 존재하지 않기 때문에 호스트 환경마다 약간의 차이가 있다고 합니다..
브라우저는 HTML5의 timers section을 준수한다고 합니다.
https://www.w3.org/TR/html5/webappapis.html#timers
스케줄링 메서드를 사용할 때는 명시한 지연 간격이 보장되지 않을 수도 있습니다.
책에서는 아래와 같은 상황에서 브라우저 내 타이머가 느려져서 시간이 보장되지 않는다고 합니다.
- CPU가 과부하 상태인 경우
- 브라우저 탭이 백그라운드 모드인 경우
- 등
이런 상황에서 연장 시간은 브라우저나 구동 중인 운영 체제의 성능 설정에 따라 달라진다고 합니다.
실제로 제가 회고에서 모도코 첫 이벤트에서 버그가 났었다고 했습니다. 그게 바로 브라우저 탭이 백그라운드 모드인 경우에 타이머가 제대로 동작하지 않는 것 때문이었습니다.. 다들 화면 공유하면서 브라우저는 백그라운드 모드로 최소화 두었습니다. 브라우저는 그러면 리소스를 아끼기 위해 느리게 동작하고는 한다고 하는데요.. 그것 때문에 타어머가 제대로 동작하지 않았습니다.
9. call/apply와 데코레이터, 포워딩
이 챕터에서는 함수 간에 호출을 어떻게 forwarding하고 decorating하는지 알려줍니다.
slow함수를 cache할 수 있는 데코레이터를 예시로 드는데요. 핵심은 this를 어떻게 명시적으로 binding해줄까 입니다.
func.call(context, arg1, arg2, ...)
func.apply(context, [arg1, arg2, ...])
10. 함수 바인딩
이 챕터에서는 this를 context 또는 arguements들을 binding하는 법에 대해서 알려줍니다.
this에 대해서 알아보자
먼저 this가 어떻게 Bind되는지 코어 자바스크립트 내용으로 정리해보았습니다.
이 부분은 Core JavaScript에 나오는 내용을 참고했습니다.
CASE 1. 함수 호출
this = globalThis
CASE 2. 메서드로 호출
this = 메서드명 앞
- . 앞
- ['prop'] 앞
var a = 'global'
var obj = {
a: 'local',
outerFunc: function() {
console.log(this.a);
function innerFunc() {
console.log(this.a);
}
innerFunc(); // 'global' !!!
}
}
obj.outerFunc(); // 'local'
this가 obj가 되는 것을 막으려면?
1. Scope chain
var a = 'global'
var obj = {
a: 'local',
outerFunc: function() {
var self = this;
console.log(this.a);
function innerFunc() {
console.log(self.a);
}
innerFunc(); // 'local'
}
}
obj.outerFunc(); // 'local'
2. arrow function을 쓴다
this binding을 하지 않으므로 상위에 있는 this를 그대로 가져다 쓴다.
var a = 'global'
var obj = {
a: 'local',
outerFunc: function() {
console.log(this.a);
const innerFunc = () => {
console.log(this.a);
}
innerFunc(); // 'local'
}
}
obj.outerFunc(); // 'local'
3. (ES5 style) binding을 명시적으로 해준다.
var a = 'global'
var obj = {
a: 'local',
outerFunc: function() {
console.log(this.a);
function innerFunc() {
console.log(this.a);
}
innerFunc.call(this); // 'local' bind this!!
}
}
obj.outerFunc(); // 'local'
CASE 3: Callback 함수 호출
this = 케바케
- binding하는 것
- 없으면 globalThis
var a = 'global'
var obj = {
a: 'local',
callCallbackFunc: function(cb) {
cb();
}
}
obj.callCallbackFunc(function() {
console.log(this.a);
}); // 'global'
var a = 'global'
var obj = {
a: 'local',
callCallbackFunc: function(cb) {
cb.call(this); // callCallBackFunc의 this는? obj
}
}
obj.callCallbackFunc(function() {
console.log(this.a);
}); // 'local'
실제 예시 : setTimeout
var a = 'global';
var obj = {
a: 'local'
};
setTimeout(function() {
console.log(this.a)
}, 1000);
setTimeout((function() {
console.log(this.a)
}).bind(obj), 1000);
실제 예시 : addEventListener
var localVariable = 1;
function onMouseUp() {
console.dir(this)
}
myElement.addEventListener(
"mouseup",
onMouseUp,
);
CASE 4: 생성자 함수 호출
this = 생성자가 생성하는 인스턴스
function Person(name) {
this.name = name;
}
// 함수로서 호출
var juhyeong = Person('주형');
console.log(window.name)
// 생성자로서 호출
var juhyeong = new Person('주형');
console.log(juhyeong)
모던 자바스크립트에서는 기존 함수의 인자를 몇 개 고정한 부분 적용(partially applied) 함수에 대해서 다룹니다. 저는 생략하겠습니다. ㅎㅎ
11. 화살표 함수 다시 살펴보기
이 챕터에서는 화살표 함수를 되돌아봅니다.
- 작은 실행함수를 작성해야 하는 경우 자주 쓰입니다
- 이때 함수의 컨텍스트를 잃기 쉽습니다. 화살표 함수는 이때 유용합니다
- this가 없기 때문에 생성자 함수로 사용할 수 없습니다
- super가 없습니다
- 모든 인자 값에 접근하게 해주는 유사 배열 객체 arguments를 지원하지 않습니다
- 그래서 this, arguments를 함께 실어 호출을 포워딩해 주는 데코레이터를 만들 때 유용하게 사용됩니다
화살표 함수 내에서 this에 접근하면 외부에서 값을 가져옵니다.
정확하게 말하자면 화살표 함수 내에서 this를 사용하면 일반 변수를 찾듯이 scope chain을 타고 this의 값을 outerLexicalEnvironment에서 가져옵니다.
출처
댓글
이 글 공유하기
다른 글
-
모던 JavaScript 튜토리얼 파트 1 :: 8장 - "프로토타입과 프로토타입 상속", 9장 - "클래스" 정적 메서드까지 정리
모던 JavaScript 튜토리얼 파트 1 :: 8장 - "프로토타입과 프로토타입 상속", 9장 - "클래스" 정적 메서드까지 정리
2023.02.01 -
[JS] Lexical Environment로 알아보는 Closure
[JS] Lexical Environment로 알아보는 Closure
2023.01.16 -
모던 JavaScript 튜토리얼 파트 1 :: 5장 "자료구조와 자료형" 정리
모던 JavaScript 튜토리얼 파트 1 :: 5장 "자료구조와 자료형" 정리
2023.01.09 -
모던 JavaScript 튜토리얼 파트 1 :: 3장 "코드 품질", 4장 "객체:기본" 정리
모던 JavaScript 튜토리얼 파트 1 :: 3장 "코드 품질", 4장 "객체:기본" 정리
2023.01.02