[Core JavaScript] | May 06, 2021
Core JavaScript(코어 자바스크립트) 내용 정리
environmentRecord
에서 값을 찾지 못하므로 outerEnvironmentReference
에 지정된 상위 context인 outer의 LexicalEnvironment
에 접근해서 a를 찾음⇒ outer 함수의 실행 context가 종료되기 이전에 inner 함수의 실행 context가 종료되어 있으며, 이후 별도로 inner 함수를 호출할 수 없음
var outer = function(){
var a = 1;
var inner = function(){
console.log(++a); //2
};
inner();
};
outer();
⇒ 역시 outer 함수의 실행 context가 종료되면 이후 별도로 inner 함수를 호출할 수 없음
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); //2
⇒ outer2를 호출하면 앞서 반환된 함수인 inner가 실행
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); //2
console.log(outer2()); //3
위의 예제에서, inner 함수에서는 a를 선언하지 않았기 때문에 environmentRecord
에서 값을 찾지 못하므로 outerEnvironmentReference
에 지정된 상위 context인 outer의 LexicalEnvironment
에 접근해서 a를 찾음
그런데 inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment
에 어떻게 접근할 수 있는 걸까?
⇒ Garbage Collector의 동작 방식 때문
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않음
즉, 어떤 함수의 LexicalEnvirionment
가 이를 ‘참조할’ 예정인 다른 실행 context가 있는 한 실행 종료 이후에도 GC되지 않음
⇒ 위의 예시에서는 outer 함수가 실행 종료 시점에 inner 함수를 반환하므로, inner 함수는 언제가 호출될 가능성이 생겼고, inner 함수의 실행 context가 활성화되면 outerEnvironmentReference
가 outer 함수의 LexicalEnvironment
를 필요로 할 것이므로 수집 대상에서 제외됨. 그래서 inner 함수가 이 변수에 접근할 수 있는 것
스펙상으로는 (envrionmentRecord
와 outerEnvironmentReference
를 포함하는) LexicalEnvironment
전체를 GC하지 않고 남겨두도록 되어 있으나, 2019년 기준으로 크롬이나 Node.js 등에서 사용 중인 V8 엔진의 경우, 내부 함수에서 실제로 사용하는 변수만 남겨두고 나머지는 GC하도록 최적화 되어 있음.
⇒ 즉, 위의 예시에서는 outer 함수의 outerEnvironmentReference
와 environmentRecord
의 inner 함수는 GC 되고, environmentRecord
의 a 변수만 GC에서 제외됨
// setInterval, setTimeout
// 별도의 외부객체인 window의 method(setTimeout 또는 setInterval)에 전달할 콜백 함수 내부에서 지역변수를 참조
(function(){
var a = 0;
var intervalId = null;
var inner = function(){
if(++a >= 3){
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
>>>
1
2
3
// eventListener
// 별도의 외부객체인 DOM의 method(addEventListener)에 등록할 handler 함수 내부에서 지역변수를 참조
(function(){
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListener('click', function(){
console.log(++count, 'times clicked');
});
document.body.appendChild(button);
})();
>>>
1 "times clicked"
2 "times clicked"
3 "times clicked"
Closure는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생하므로, 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 됨
⇒ 참조 카운트를 0으로 만들어서 언젠가 GC가 수거해 가도록 만듦. 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당
// return에 의한 closure의 메모리 해제
var outer = (function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
// setInterval에 의한 closure의 메모리 해제
(function(){
var a = 0;
var intervalId = null;
var inner = function(){
if(++a >= 3){
clearInterval(intervalId);
inner = null; // inner 식별자의 함수 참조를 끊음
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
// eventListener에 의한 closure의 메모리 해제
(function(){
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
var clickHandler = function(){
console.log(++count, 'times clicked');
if (count >= 5){
button.removeEventListener('click', clickHandler);
clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
}
};
button.addEventListener('click', clickHandler);
document.body.appendChild(button);
})();
fruits의 변수를 순회하며 li를 생성하고, 각 li를 클릭하면 해당 Listener에 기억된 callback 함수를 실행
함수(A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 context가 활성화될 것
(A)의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 context의 (B)가 실행될 때는 (B)의 outerEnvrionmentReference가 (A)의 LexicalEnvrionment를 참조
⇒ 최소한 (B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능할 것임
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul'); //공통 코드
fruits.forEach(function(fruit){ //(A) 클로저 X
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function(){ //(B) 클로저 O
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
Example 1에서 (B) 함수의 쓰임새가 callback 함수에 국한되지 않는 경우라면, 반복을 줄이기 위해 (B)를 외부로 분리하는 편이 나을 수도 있음. 즉, fruit를 인자로 받아 출력하도록 만듦
그러나 직접 호출할 경우에는 정상적으로 실행되지만, li를 클릭한다면 you choice is [object MouseEvent]
라는 메시지가 출력됨
그 이유는 callback function의 인자에 대한 제어권을 addEventListener가 가진 상태이며, addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 ‘이벤트 객체’를 주입하기 때문
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function(fruit){
alert('your choice is ' + fruit);
};
fruits.forEach(function(fruit){
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit);
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
>>> your choice is banana
Example 2의 문제점은 bind method를 활용해서 해결 가능
다만, 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점 및 함수 내부에서의 this가 원래의 그것과 달라지는 점은 감안해야 함
(bind method의 첫 번째 인자가 바로 새로 바인딩할 this인데, 이 값을 생략할 수 없기 때문에 일반적으로 원래의 this를 유지하도록 할 수 없는 경우가 많음. 또한 예제에서는 두 번째 인자에 이벤트 객체가 넘어올 것임)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function(fruit){
alert('your choice is ' + fruit);
};
fruits.forEach(function(fruit){
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit));
$ul.appendChild($li);
});
document.body.appendChild($ul);
Example 3에서 발생하는 변경사항이 발생하지 않도록 고차함수(함수를 인자로 받거나 함수를 리턴하는 함수)를 활용
alertFruit(fruit)
의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 Listener에 callback 함수로써 전달
이후 클릭 이벤트가 발생하면 비로소 이 함수의 실행 context가 열리면서 alertFruit의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조 가능 (즉, alertFruit의 실행 결과로 반환된 함수에는 클로저가 존재)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function(fruit){
return function(){
alert('your choice is ' + fruit);
};
}; //익명 함수 자체를 반환
fruits.forEach(function(fruit){
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit(fruit));
$ul.appendChild($li);
});
document.body.appendChild($ul);
outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수의 a의 값을 외부에서도 읽을 수 있게 됨
이처럼 closure를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부의 변수에 대한 접근 권한 부여 가능
외부에서는 outer 함수 내부에는 개입할 수 없고 오직 outer 함수가 return한 정보에만 접근할 수 있기 때문에, 외부에 제공하고자 하는 정보들은 return하고 내부에서만 사용할 정보는 return하지 않는 것으로 접근 권한 제어 가능
var outer = function(){
var a = 1;
var inner = function(){
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
>>>
2
3
외부에 접근권한을 주고자하는 대상들로 구성된 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return
⇒ return한 변수들은 public member가 되고, 그렇지 않은 변수들은 private member가 됨
car 변수에 객체를 직접 할당. fuel를 무작위로 생성하고, run method를 실행할 때마다 fuel 값 변경
그러나 car.fuel = 10000;
식으로 무작위로 정해지는 값들을 마음대로 바꿀 가능성이 있음
var car = {
fuel: Math.ceil(Math.random() * 100),
run: function(){
var km = Math.ceil(Math.random() * 10);
if (this.fuel < km){
console.log('이동불가');
return;
}
this.fuel -= km;
console.log(km + 'km 이동 (남은 연료 ' + this.fuel + ')');
}
};
객체가 아닌 함수로 만들고, 필요한 member만을 return
외부에서는 오직 run method를 실행하는 것과 현재 남은 연료를 확인하는 left 두 가지 동작만 가능 (fuel 변수는 getter만을 부여함으로써 읽기 전용 속성 부여)
var createCar = function(){
var fuel = Math.ceil(Math.random() * 100);
return {
get left(){
return fuel;
},
run: function(){
var km = Math.ceil(Math.random() * 10);
if (fuel < km){
console.log('이동불가');
return;
}
fuel -= km;
console.log(km + 'km 이동 (남은 연료 ' + fuel + ')');
}
};
};
var car = createCar();
위와 같이 작성함으로써 fuel의 값을 변경하려는 시도는 대부분 실패하게 됨
비록 run method를 다른 내용으로 덮어씌우는 어뷰징(abusing)은 여전히 가능한 상태이긴 하지만, Example 1보다는 훨씬 안전한 코드
console.log(car.left) // 95
car.left = 1000;
console.log(car.left) // 95
console.log(car.fuel) // undefined
car.fuel = 1000;
console.log(car.fuel) // 1000
car.run(); // 3km 이동 (남은 연료 92)
car.run = 'aaa'
console.log(car.run) // 'aaa'
car.run(); // Uncaught TypeError: car.run is not a function
run method를 덮어씌우는 abusing까지 막기 위해서, 객체를 return하기 전에 미리 변경할 수 없게끔 조치를 취해야 함
var createCar = function(){
var fuel = Math.ceil(Math.random() * 100);
var publicMembers = {
get left(){
return fuel;
},
run: function(){
var km = Math.ceil(Math.random() * 10);
if (fuel < km){
console.log('이동불가');
return;
}
fuel -= km;
console.log(km + 'km 이동 (남은 연료 ' + fuel + ')');
}
};
Object.freeze(publicMembers);
return publicMembers;
};
var car = createCar();
car.run = 'aaa'
console.log(car.run) // f(){...}
car.run(); // 2km 이동 (남은 연료 60)
this를 바인딩해야 하는 점을 제외하면, bind
method 또한 부분 적용 함수 (3장 참조)
var add = function(){
var result = 0;
for (var i = 0; i < arguments.length; i++){
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2);
console.log(addPartial(3, 4, 5));
>>> 15
bind
는 this의 값을 변경할 수밖에 없기 때문에 method에서는 사용하기 어려움
this에 관여하지 않는 별도의 부분 적용 함수를 구현한다면: 첫 번째 인자에는 원본 함수를, 두 번째 인자 이후부터는 미리 적용할 인자를 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 이들을 concat해서 원본 함수에 apply
실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 됨
var partial = function(){
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function'){
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
// add 함수에 적용
var add = function(){
var result = 0;
for (var i = 0; i < arguments.length; i++){
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2);
console.log(addPartial(3, 4, 5));
>>> 15
// object의 method에 적용
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix){
return prefix + this.name + suffix;
}, '왈왈, ')
};
dog.greet('입니다!');
>>>
"왈왈, 강아지입니다!"
_
라는 property를 준비하면서 삭제 변경 등의 접근에 대한 방어 차원에서 여러 가지 property 속성 설정부분 적용 함수를 만들 때 미리부터 실행할 함수의 모든 인자 개수를 맞춰 빈 공간을 확보하지 않아도, 최종 실행 시 인자 개수가 많든 적든 잘 실행됨
Object.defineProperty(window, '_', {
value: 'EMPTY_SPACE',
writable: false,
configurable: false,
enumerable: false
});
var partial = function(){
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function'){
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
for (var i = 0; i < partialArgs.length; i++){
if (partialArgs[i] === _) {
partialArgs[i] = restArgs.shift();
}
}
return func.apply(this, partialArgs.concat(restArgs));
};
};
// add 함수에 적용
var add = function(){
var result = 0;
for (var i = 0; i < arguments.length; i++){
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, _, 3, _, _);
console.log(addPartial(2, 4, 5));
>>> 15
ES5 환경에서는 _
를 ‘비워놓음’으로 사용하기 위해 어쩔 수 없이 전역공간을 침범
ES6에서는 Symbol.for
를 활용 가능
Symbol.for
method는 전역 심볼공간에 인자로 넘어온 문자열이 이미 있으면 해당 값을 참조하고, 선언되어 있지 않으면 새로 만드는 방식으로, 어디서든 접근 가능하면서 유일무이한 상수를 만들고자 할 때 적합
var partial = function(){
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function'){
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function(){
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
for (var i = 0; i < partialArgs.length; i++){
if (partialArgs[i] === Symbol.for('EMPTY_SPACE')) {
partialArgs[i] = restArgs.shift();
}
}
return func.apply(this, partialArgs.concat(restArgs));
};
};
// add 함수에 적용
var add = function(){
var result = 0;
for (var i = 0; i < arguments.length; i++){
result += arguments[i];
}
return result;
};
var _ = Symbol.for('EMPTY_SPACE')
var addPartial = partial(add, 1, _, 3, _, _);
console.log(addPartial(2, 4, 5));
>>> 15
Debounce(디바운스): 짧은 시간 동안 동일한 이벤트가 많이 발생하는 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것으로, Frontend 성능 최적화에 큰 도움을 주는 기능 중 하나
scroll, wheel, mousemove, resize 등에 적용하기 좋음
최초 event가 발생하면 setTimeout
에 의해 timeout의 대기열에 ‘wait 시간 뒤에 func을 실행할 것’이라는 내용이 담김
그런데 wait 시간이 경과하기 전에 다시 동일한 event가 발생하면 clearTimeout
에 의해 앞서 저장했던 대기열을 초기화하고 다시 새로운 대기열을 setTimeout
으로 등록
아래의 디바운스 함수에서 클로저로 처리되는 변수는 eventName, func, wait, timeoutId
var debounce = function(eventName, func, wait){
var timeoutId = null;
return function(event){ //closure로 EventListener에 의해 호출될 함수 반환
var self = this; //setTimeout를 사용하기 위해 this를 별도의 변수에 담음
console.log(eventName, 'event 발생');
clearTimeout(timeoutId); //무조건 대기큐를 초기화
timeoutId = setTimeout(func.bind(self, event), wait); //setTimeout으로 wait 시간만큼 지연시킨 후 원래의 func 호출
};
};
var moveHandler = function(e){
console.log('move event 처리');
};
document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
Currying function(커링 함수): 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성
한 번에 하나의 인자만 전달하는 것이 원칙이며, 중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않음
(부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할 때 원본 함수가 무조건 실행됨)
Lazy execution(지연실행): 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루게 되는 것
원하는 시점까지 지연시켰다가 실행하는 것이 요긴한 경우, 혹은 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우 등에 유용하게 사용될 수 있음
부분 적용 함수와 달리 커링 함수는, 필요한 인자 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에만 조합해서 return해주면 되기 때문에 필요한 상황에 직접 만들어 쓰기가 용이
var curry = function(func){
return function(a){
return function(b){
return func(a, b);
};
};
};
var getMaxWith10 = curry(Math.max)(10);
console.log(getMaxWith10(8)); //10
console.log(getMaxWith10(25)); //25
var getMin = curry(Math.min);
console.log(getMin(15)(30)); //15
다만 인자가 많아질수록 가독성이 떨어진다는 단점이 있는데, ES6에서는 화살표 함수를 활용하여 한 줄 구현 가능
화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 한눈에 파악됨
각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 GC되지 않고 메모리에 차곡차곡 쌓였다가, 마지막 호출로 실행 context가 종료된 후에야 비로소 한꺼번에 GC의 수거 대상이 됨
var getInfo = function(baseUrl){
return function(path){
return function(id){
return fetch(baseUrl + path + '/' + id);
};
};
};
// ES6
var getInfo = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
var imageUrl = 'http://imageAddress.com/';
var getImage = getInfo(imageUrl); // http://imageAddress.com/
var getEmoji = getImage('emoji'); // http://imageAddress.com/emoji
var getIcon = getImage('icon'); // http://imageAddress.com/icon
// 실제요청
var emoji1 = getEmoji(100); // http://imageAddress.com/emoji/100
var icon1 = getIcon(253); // http://imageAddress.com/icon/253