요약
데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킵니다. 주요 작업은 여전히 함수에서 처리합니다.데코레이터는 함수에 추가된 ‘기능’ 혹은 ‘상(相, aspect)’ 정도로 보시면 됩니다. 하나 혹은 여러 개의 데코레이터를 추가해도 함수의 코드는 변경되지 않습니다.
cachingDecorator
는 아래와 같은 메서드를 사용해 구현하였습니다.
func.call(context, arg1, arg2…)
– 주어진 컨텍스트와 인수를 사용해func
를 호출합니다.func.apply(context, args)
–this
에 context가 할당되고, 유사 배열args
가 인수로 전달되어func
이 호출됩니다.콜 포워딩은 대개
apply
를 사용해 구현합니다.let wrapper = function() { return original.apply(this, arguments); };
특정 객체에서 메서드를 가져오고, 다른 객체를 컨텍스트로 고정한 후 함수를 호출(call)하는 형태인 메서드 빌리기에 대한 예제도 살펴보았습니다. 메서드 빌리기는 배열 메서드를 빌려서 이를
arguments
에 적용할 때 흔히 사용됩니다. 나머지 매개변수와 배열을 함께 사용하면 유사한 기능을 구현할 수 있습니다.
function slow(x) {
// CPU 집약적인 작업이 여기에 올 수 있습니다.
alert(`slow(${x})을/를 호출함`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // cache에 해당 키가 있으면
return cache.get(x); // 대응하는 값을 cache에서 읽어옵니다.
}
let result = func(x); // 그렇지 않은 경우엔 func를 호출하고,
cache.set(x, result); // 그 결과를 캐싱(저장)합니다.
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1)이 저장되었습니다.
alert( "다시 호출: " + slow(1) ); // 동일한 결과
alert( slow(2) ); // slow(2)가 저장되었습니다.
alert( "다시 호출: " + slow(2) ); // 윗줄과 동일한 결과
cachingDecorator
같이 인수로 받은 함수의 행동을 변경시켜주는 함수를 데코레이터(decorator) 라고 부릅니다.cachingDecorator
를 호출 할 수 있는데, 이때 반환되는 것은 캐싱 래퍼입니다. 함수에 cachingDecorator
를 적용하기만 하면 캐싱이 가능한 함수를 원하는 만큼 구현할 수 있기 때문에 데코레이터 함수는 아주 유용하게 사용됩니다.cachingDecorator(func)
를 호출하면 ‘래퍼(wrapper)’, function(x)
이 반환됩니다. 래퍼 function(x)
는 func(x)
의 호출 결과를 캐싱 로직으로 감쌉니다(wrapping).slow
본문을 수정하는 것 보다 독립된 래퍼 함수 cachingDecorator
를 사용할 때 생기는 이점을 정리하면 다음과 같습니다.cachingDecorator
를 재사용 할 수 있습니다. 원하는 함수 어디에든 cachingDecorator
를 적용할 수 있습니다.slow
자체의 복잡성이 증가하지 않습니다.cachingDecorator
뒤를 따릅니다).func.call(context, …args)
에 대해 알아봅시다.func.call(context, arg1, arg2, ...)
func(1, 2, 3);
func.call(obj, 1, 2, 3)
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// call을 사용해 원하는 객체가 'this'가 되도록 합니다.
sayHi.call( user ); // this = John
sayHi.call( admin ); // this = Admin
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert(`slow(${x})을/를 호출함`);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // 이젠 'this'가 제대로 전달됩니다.
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용
alert( worker.slow(2) ); // 제대로 동작합니다.
alert( worker.slow(2) ); // 제대로 동작합니다. 다만, 원본 함수가 호출되지 않고 캐시 된 값이 출력됩니다.
명확한 이해를 위해 this
가 어떤 과정을 거쳐 전달되는지 자세히 살펴보겠습니다.
worker.slow
는 래퍼 function (x) { ... }
가 됩니다.worker.slow(2)
를 실행하면 래퍼는 2를 인수로 받고, this=worker
가 됩니다(점 앞의 객체).func.call(this, x)
에서 현재 this
(=worker
)와 인수(=2
)를 원본 메서드에 전달합니다.세 번째 방법만으로 충분하기 때문에 이 방법을 사용해 코드를 수정해 보겠습니다.
let worker = {
slow(min, max) {
alert(`slow(${min},${max})을/를 호출함`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // 제대로 동작합니다.
alert( "다시 호출: " + worker.slow(3, 5) ); // 동일한 결과 출력(캐시된 결과)
그런데 여기서 func.call(this, ...arguments)
대신, func.apply(this, arguments)
를 사용해도 됩니다.
func.apply(context, args)
let wrapper = function() {
return func.apply(this, arguments);
};
// 에러: hash(arguments)를 호출할 때 인수로 넘겨주는 arguments는
// 진짜 배열이 아니고 이터러블 객체나 유사 배열 객체이기 때문
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
// 에러 해결: 메서드 빌리기
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
join
메서드를 빌려오고([].join
), [].join.call
를 사용해 arguments
를 컨텍스트로 고정한 후 join
메서드를 호출하는 것이죠.요약
func.bind(context, ...args)
는this
가context
로 고정되고 인수도 고정된 함수func
을 반환합니다.bind
는 보통 객체 메서드의this
를 고정해 어딘가에 넘기고자 할 때 사용합니다.setTimeout
에 넘길 때 같이 말이죠.- 기존 함수의 인수 몇 개를 고정한 함수를 부분 적용(partially applied) 함수 또는 부분(partial) 함수라고 부릅니다.
- 부분 적용은 같은 인수를 여러 번 반복하고 싶지 않을 때 유용합니다. send(from, to)라는 함수가 있는데 from을 고정하고 싶다면 send(from, to)의 부분 함수를 구현해 사용하면 됩니다.
this
가 사라집니다.let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
this.firstName
이 "John"이 되어야 하는데, 얼럿창엔 undefined
가 출력됩니다.setTimeout
에 객체에서 분리된 함수인 user.sayHi
가 전달되기 때문입니다. 위 예시의 마지막 줄은 다음 코드와 같습니다.let f = user.sayHi;
setTimeout(f, 1000); // user 컨텍스트를 잃어버림
setTimeout
메서드는 조금 특별한 방식으로 동작합니다. 인수로 전달받은 함수를 호출할 때, this
에 window
를 할당합니다this.firstName
은 window.firstName
가 되는데, window
객체엔 firstName
이 없으므로 undefined
가 출력됩니다. 다른 유사한 사례에서도 대부분 this
는 undefined
가 됩니다.let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
// refactor
setTimeout(() => user.sayHi(), 1000); // Hello, John!
setTimeout
이 트리거 되기 전에(1초가 지나기 전에) user
가 변경되면, 변경된 객체의 메서드를 호출하게 됩니다.let boundFunc = func.bind(context);
func.bind(context)
는 함수처럼 호출 가능한 '특수 객체(exotic object)'를 반환합니다. 이 객체를 호출하면 this
가 context
로 고정된 함수 func
가 반환됩니다.boundFunc
를 호출하면 this
가 고정된 func
를 호출하는 것과 동일한 효과를 봅니다.let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
func.bind(user)
는 func
의 this
를 user
로 '바인딩한 변형’이라고 생각하시면 됩니다.let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// this를 user로 바인딩합니다.
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (인수 "Hello"가 넘겨지고 this는 user로 고정됩니다.)
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// 이제 객체 없이도 객체 메서드를 호출할 수 있습니다.
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// 1초 이내에 user 값이 변화해도
// sayHi는 기존 값을 사용합니다.
user = {
sayHi() { alert("또 다른 사용자!"); }
};
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John (인수 "Hello"가 say로 전달되었습니다.)
say("Bye"); // Bye, John ("Bye"가 say로 전달되었습니다.)
bindAll로 메서드 전체 바인딩하기
for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } }
let bound = func.bind(context, [arg1], [arg2], ...);
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
this
를 사용하지 않았다는 점에 주목하시기 바랍니다. bind
엔 컨텍스트를 항상 넘겨줘야 하므로 null
을 사용했습니다.function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// 사용법:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// 시간을 고정한 부분 메서드를 추가함
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// 출력값 예시:
// [10:00] John: Hello!
partial(func[, arg1, arg2...])
을 호출하면 래퍼((*))가 반환됩니다. 래퍼를 호출하면 func
이 다음과 같은 방식으로 동작합니다.this
를 받습니다(user.sayNow
는 user
를 대상으로 호출됩니다).partial
을 호출할 때 받은 인수("10:00")는 ...argsBound
에 전달됩니다....args
가 됩니다.요약
화살표 함수가 일반 함수와 다른 점은 다음과 같습니다.
- this를 가지지 않습니다.
- arguments를 지원하지 않습니다.
- new와 함께 호출할 수 없습니다.
- 이 외에도 화살표 함수는 `super가 없습니다.
- 화살표 함수는 컨텍스트가 있는 긴 코드보다는 자체 '컨텍스트’가 없는 짧은 코드를 담을 용도로 만들어졌습니다.
arr.forEach(func)
– func
는 forEach
가 호출될 때 배열 arr
의 요소 전체를 대상으로 실행됩니다.setTimeout(func)
– func
는 내장 스케줄러에 의해 실행됩니다.this
에 접근하면, 외부에서 값을 가져옵니다.let group = {
title: "1모둠",
students: ["보라", "호진", "지민"],
showList() {
this.students.forEach(
student => alert(this.title + ': ' + student)
);
}
};
group.showList();
let group = {
title: "1모둠",
students: ["보라", "호진", "지민"],
showList() {
this.students.forEach(function(student) {
// TypeError: Cannot read property 'title' of undefined
alert(this.title + ': ' + student)
});
}
};
group.showList();
forEach
에 전달되는 함수의 this
가 undefined
이어서 발생했습니다. alert
함수에서 undefined.title
에 접근하려 했기 때문에 얼럿 창엔 에러가 출력됩니다.this
자체가 없기 때문에 이런 에러가 발생하지 않습니다.화살표 함수 vs. bind
- 화살표 함수와 일반 함수를 .bind(this)를 사용해서 호출하는 것 사이에는 미묘한 차이가 있습니다.
.bind(this)
는 함수의 '한정된 버전(bound version)'을 만듭니다.- 화살표 함수는 어떤 것도 바인딩시키지 않습니다. 화살표 함수엔 단지
this
가 없을 뿐입니다. 화살표 함수에서this
를 사용하면 일반 변수 서칭과 마찬가지로this
의 값을 외부 렉시컬 환경에서 찾습니다.
arguments
를 지원하지 않습니다.this
값과 arguments
정보를 함께 실어 호출을 포워딩해 주는 데코레이터를 만들 때 유용하게 사용됩니다.defer(f, ms)
는 함수를 인자로 받고 이 함수를 래퍼로 감싸 반환하는데, 함수 f
는 ms
밀리초 후에 호출됩니다.