execution context를 가진 반복문이 메모리도 덜 사용한다.그러나 메모리의 효율성을 따지는 것 보다 개발 효율성을 따지는 것이 경제적인 경우도 많다. 최적화를 위한 비용을 들일만한 가치가 있는지 따져야 할 것이며, 특히 복잡한 조건에 따라 다양한 방식으로 반복 실행해야 할 경우 그렇다.
재귀함수는 보다 짧고, 이해하거나 수정하기 쉽다. 최적화는 모든 영역에서 필수적이지 않다. 대부분의 경우 단지 좋은 코드가 필요할 뿐이다.
좋은 코드란 무엇인지 생각 해 볼 수 있는 대목이다.
순환 참조라고도 부를 수 있겠다.
예를 들어...
let company = {
sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],
development: {
sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],
internals: [{name: 'Jack', salary: 1300}]
}
};
function sumSalaries(department) {
if (Array.isArray(department)) {
return department.reduce((prev, current) => prev + current.salary, 0); // sum the array
} else {
let sum = 0;
for (let subdep of Object.values(department)) {
sum += sumSalaries(subdep);
}
return sum;
}
}
군더더기 없이 깔끔한 예시였다.
함수는 아니지만 재귀적 사고 방식은 자료구조의 발상에도 유용하다.
예를들면 linked list 가 있다.
const list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
...
...
...
next: null
}
}
}
}
배열의 경우 앞쪽을 건드리는 것은 큰 비용이 발생한다. queue로 이용하는 것이 불가능하진 않지만 stack형 자료구조에 가깝다. linked list 는이런 상황에서 유용할 수 있는 자료구조라 할 수 있다.
const newFirstValue = 100;
// 적은 비용으로 앞단에 값을 추가하거나
list.next = list
list.value = newFirstValue
// 특정 위치에서 조각내고, 나중에 이어붙일 수 있다.
const cutFromThird = list.next.next.next;
list.next.next.next = null;
물론 만능은 아니다. 딱 봐도 특정 위치의 요소에 접근하는데 비용이 발생한다. 배열의 경우 n번째 요소에 단번에 접근할 수 있는데 비해 ...next.next.next..... 계속 가야하는 단점이 있다.
Array.from()은 array-like와 iterable 모두 다룰 수 있는 반면,[...arr]는 iterable만 변환 할 수 있다.spread syntax는 마치for of구문처럼[Symbol.iterator]를 사용하기 때문이다.복습 차원에서 둘의 차이를 복기하자면,
array-like:numeric keys와lenth속성을 가진 객체iterable:[Symbol.iterator]속성을 가진 객체
따라서
Array.from()이 좀 더 범용적으로 쓰일 수 있다. 나를 돌아봐도 spread syntax를 두고Array.from이나Object.assign을 사용하는 경우는 매우 드물지만, 의도가 더 선명하게 드러난다는 점에서 복잡하거나 중요한 구문에서는 이것을 사용하는것이 좋을 것 같다.
언어 또는 실행환경 단위에서 정의 된, 언제나 접근 가능한 객체를 말한다. 환경에 따라 다른 이름을 가지고 있다. 예를들어 브라우저에서는 window , node.js에서는 global 이다.
다만 어떤 환경에서든 동일한 이름으로 호출 할 수도 있다.
globalThis가 그것이다.=== 연산자로 검사 해 봐도window또는global과 동일함을 알 수 있다.
이것은 객체인 만큼 속성을 할당할 수 있다. 브라우저의 경우, 모듈에서 선언되지 않는 한, 스크립트 단위에서 var 로 선언된 변수, 또는 함수 선언의 경우 자동으로 global object의 속성이 된다. 그러나 이런 (관점에 따라)부수효과에 의지하기보다는 아래의 예시처럼 명시적으로 할당 해 주는 것이 좋다.
globalThis.adminUser = {name: 'kim'}
console.log(globalThis.amdinUser.name) // 'kim'
그러나 일반적인 상황에서 권장 할 일은 아니다. 어디서나 접근 할 수 있다는 것은 편한 만큼 디버깅이 어려워 질 수 있기 때문이다.
물론 typeof 연산자로 찍어보면 'function'이라는 문자열이라 알려준다. 이것은 개발자 편리하라고 약간의 특별함을 더한 것이다. 함수는 정말로 객체다.
이 (함수)객체는 보통 실행하는 것에 쓰임이 있지만, 다른 속성 또한 사용 할 수 있다.
예를들어 name 또는 length 같은 내장 속성이 있는데, length의 경우 해당 함수가 호출 될 때 가지는 arguments의 개수를 뜻한다. 이것은 많은 JS library에서 활용되고 있다.
또는 직접 만들어 쓸 수 도 있다. 아래의 예시는 함수가 몇 번 호출됐는지 알려주는 속성을 만들어 준 경우다.
function sayHi() {
alert("Hi");
// let's count how many times we run
sayHi.counter++;
}
sayHi.counter = 0; // initial value
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
우리는 실행단계에서 동적으로 생성되는 변수를 가질 수 있다. 함수 또한 마찬가지로 실행 단계에서 생성 될 수 있다. 예를들어 스크립트가 실행되는 도중에 다른 서버에서 동적으로 받아온 정보로 함수를 생성 할 수 있다.
let func = new Function ([arg1, arg2, ...argN], functionBody);
// 용례를 보면 더 쉽다.
let sum = new Function('a', 'b', 'return a + b');
console.log( sum(1, 2) ); // 3
예시에서 보듯이 문자열로 생성된다. 맨 마지막 인자를 제외하곤 모두 매개변수로 취급된다.
특이한 것은 이렇게 생성된 함수는 클로져(함수)가 아니라는 점이다. new Function syntax로 생성된 함수는 [[environment]] 속성에 outer lexical environment 대신 무조건 global lexical environment 참조값을 가진다.
언뜻 약점으로 보일 수 있지만 이것은 컴파일과 비슷한 방식으로 사전 작업을 해 주는 minifier같은 놈들의 혼란을 방지하고, 동적으로 생성되는 함수인 측면에서 에러의 온상을 미연에 방지하는 것이다. new Function syntax로 정의된 함수가 어떤 값을 이용해야 할 경우 인자로 넘겨주는 것이 현명하다.
clearTimeout 또는 clearInterval 해 주자.setInterval 보다 중첩된 setTimeout이 나은 선택지가 될 수 있다. 둘의 차이점은 예약 된 함수의 실행에 소요되는 시간이 interval delay에 포함되는지, 그렇지 않은지이다. setInterval의 경우 이런 점을 고려하지 않는다.// instead of:
// let timerId = setInterval(() => alert('tick'), 2000);
let timerId = setTimeout(function tick() {
alert('tick'); // 이 라인이의 실행이 끝난 시점에 다음 예약이 실행 된다.
timerId = setTimeout(tick, 2000);
}, 2000);
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// increase the interval to the next run
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
zero-delay setTimeout은 말 그대로 조금도 기다리지 않고 예약 된 함수를 실행시킨다. 그러나 이렇게 예약 된 함수들은 현 실행 컨텍스트의 모든 구문이 실행 된 이후부터 시작된다. setTimeout(() => console.log("World"));
console.log("Hello");
// 0초 후 실행이지만, 결과는 'Hello' 이후에 'World'가 나오게 된다.
zero-delay setTimeout의 delay는 언제나 0초로 보장되지 않는다. 어불성설이지만 historical reasons가 있다. 브라우저 환경에서 5회 이상 중첩된 경우 4ms 이상의 오차를 보인다. 물론 nodejs처럼 백엔드 환경에서는 이런 제약이 없다.JS의 함수는 값이며 객체인 덕분에 반환하거나 인자로 전달하는 등 매우 유연하게 사용할 수 있다.
decorator함수 즉 warpper함수를 만들어서 핵심 로직과 관리하는 로직을 분리 할 수도 있는데, 캐싱 작업이 좋은 예가 된다.
물론 핵심적인 연산을 수행하는 함수 안에 캐싱 작업을 포함 할 수도 있지만 이것을 분리 할 때 우리는 몇가지 분명한 이점을 얻을 수 있다.
아래의 예시를 보자.
function slow(x) {
// there can be a heavy CPU-intensive job here
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // if there's such key in cache
return cache.get(x); // read the result from it
}
let result = func(x); // otherwise call func(인자로 들어올 함수)
cache.set(x, result); // and cache (remember) the result
return result;
};
}
slow = cachingDecorator(slow);
// 기존의 slow 함수를 인자로 넘겨주며 단일한 map 자료형을 사용하기 위한 lexical environment를
// 생성한다. 그리고 핵심 연산 작업만 수행하던 함수에 캐싱 작업을 결합 하여 decorated 된 함수를
// 반환받아 사용한다.
console.log( slow(1) ); // slow(1) is cached and the result returned
console.log( "Again: " + slow(1) ); // slow(1) result returned from cache
console.log( slow(2) ); // slow(2) is cached and the result returned
console.log( "Again: " + slow(2) ); // slow(2) result returned from cache
위 예시를 다시 한번 보자. 핵심 연산 작업을 수행하는 함수 slow는 함수이다. 그런데 이것이 함수가 아니라 어떤 객체의 메소드라면 어떨까? 그리고 이 메소드는 객체 그 자신의 다른 속성, 예를들면 this 같은 것을 참조하고 있다면?
// we'll make worker.slow caching
let worker = {
someMethod() {
return 1;
},
slow(x) {
// scary CPU-heavy task here
alert("Called with " + x);
return x * this.someMethod(); // 이 메소드를 소유한 worker 객체의 다른 속성을 참조한다.
}
};
그리고 같은 방식으로 캐싱 작업을 담당하는 decorator와 조합해 보는거다.
function cachingDecorator(func) {
...
}
console.log( worker.slow(1) ); // 1, 메소드 그대로 사용하면 전혀 문제가 없다.
worker.slow = cachingDecorator(worker.slow); // now make it caching
console.log( worker.slow(2) ); // undefined의 someMethod를 참조 할 수 없다는 에러가 발생한다.
당연하다. 어떤 객체의 메소드로서는 this의 값을 가지고 있지만, method만 똑 띠어서 함수를 전달하면 this는 그냥 undefined가 될 뿐이다.
이런 상황에서 우리를 구원하는 것이
function.call()이다.이것은 함수를 실행하는 약간 다른 방법인데, 첫번 째 매개변수가 코드블럭 내에
this에 할당되고 이후에 전달 받는 것들이 평소처럼 인자가 된다.
그러면 위 예시가 잘 돌아가게 만드는 것은 간단하다. func(x) 대신 func.call(worker, x)을 실행하면 되는 것이다.
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // (1)
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow);
// slow는 worker객체의 메소드이고, 따라서 slow의 this는 worker를 뜻한다.
// (1) 라인에서처럼 인자로 전달받은 함수를 call 메소드로 실행시키고, 첫 매개변수에 this를 주었는데,
// 이때의 this는 인자로 전달된 함수의 this, 즉 worker.slow의 this를 뜻하게 된다.
같은 용도의
function.apply(context, args)도 있다. 다만 차이점은.call(context, ...args)는 각각의 인자들을 차례로 받고, 전자는array-like한 객체를 받는다는 것이다. 같은 조건에서.apply()가 더 나은 성능을 보여준다.
어떤 객체의 메소드가 콜백함수로 활용 되면 this 값을 잃어버린다. 이것을 해결하는 방법을 다룰 것이다
잊어버려도 되는 간단한 방법으로는 익명함수로 감싸주는 것이 있다. 허나 이는 원본 객체에 다른 값이 할당 될 때 동적으로 대응하지 못한다.
좋은 방법은 .bind(context) 메소드를 활용하는 것이다.
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
애초 객체를 정의 할 때부터 메소드 자체에다가 이 객체를 binding 시켜 둘 수도 있다.
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
// user 객체 안의 모든 메소드에 user자신을 context로 전달 해 둔다.