
요약
데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킵니다. 주요 작업은 여전히 함수에서 처리합니다.데코레이터는 함수에 추가된 ‘기능’ 혹은 ‘상(相, 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 밀리초 후에 호출됩니다.