6. 함수 심화학습(3)
6.9 call/apply와 데코레이터, 포워딩
요약
데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킵니다. 주요 작업은 여전히 함수에서 처리합니다.데코레이터는 함수에 추가된 ‘기능’ 혹은 ‘상(相, 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는 래퍼로 감싼 이전이나 이후나 동일한 일을 수행합니다. 행동 양식에 캐싱 기능이 추가된 것뿐입니다.
slow
본문을 수정하는 것 보다 독립된 래퍼 함수cachingDecorator
를 사용할 때 생기는 이점을 정리하면 다음과 같습니다.
-cachingDecorator
를 재사용 할 수 있습니다. 원하는 함수 어디에든cachingDecorator
를 적용할 수 있습니다.
- 캐싱 로직이 분리되어slow
자체의 복잡성이 증가하지 않습니다.
- 필요하다면 여러 개의 데코레이터를 조합해서 사용할 수도 있습니다(추가 데코레이터는cachingDecorator
뒤를 따릅니다).
'func.call’를 사용해 컨텍스트 지정하기
- 먼저, this를 명시적으로 고정해 함수를 호출할 수 있게 해주는 특별한 내장 함수 메서드
func.call(context, …args)
에 대해 알아봅시다.
func.call(context, arg1, arg2, ...)
func(1, 2, 3);
func.call(obj, 1, 2, 3)
- 둘 다 인수로 1, 2, 3을 받죠. 유일한 차이점은 func.call에선 this가 obj로 고정된다는 점입니다.
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
- 래퍼 안에서 call을 사용해 컨텍스트를 원본 함수로 전달하면 에러가 발생하지 않습니다.
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
)를 원본 메서드에 전달합니다.
여러 인수 전달하기
- 복수 인수를 가진 메서드, worker.slow를 캐싱하려면 어떻게 해야 할지 생각해 봅시다.
- 복수 키를 지원하는 맵과 유사한 자료 구조 구현하기(서드 파티 라이브러리 등을 사용해도 됨)
- 중첩 맵을 사용하기. (max, result) 쌍 저장은 cache.set(min)으로, result는 cache.get(min).get(max)을 사용해 얻습니다.
- 두 값을 하나로 합치기. 맵의 키로 문자열 "min,max"를 사용합니다. 여러 값을 하나로 합치는 코드는 해싱 함수(hashing function) 에 구현해 유연성을 높입니다.
세 번째 방법만으로 충분하기 때문에 이 방법을 사용해 코드를 수정해 보겠습니다.
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.apply
그런데 여기서 func.call(this, ...arguments)
대신, func.apply(this, arguments)
를 사용해도 됩니다.
func.apply(context, args)
- apply는 func의 this를 context로 고정해주고, 유사 배열 객체인 args를 인수로 사용할 수 있게 해줍니다.
- call과 apply의 문법적 차이는 call이 복수 인수를 따로따로 받는 대신 apply는 인수를 유사 배열 객체로 받는다는 점뿐입니다.
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.calledCount 등의 프로퍼티가 있으면 데코레이터를 적용한 함수에선 프로퍼티를 사용할 수 없으므로 안전하지 않습니다. 함수에 프로퍼티가 있는 경우엔 데코레이터 사용에 주의해야 합니다.
6.10 함수 바인딩
요약
func.bind(context, ...args)
는this
가context
로 고정되고 인수도 고정된 함수func
을 반환합니다.bind
는 보통 객체 메서드의this
를 고정해 어딘가에 넘기고자 할 때 사용합니다.setTimeout
에 넘길 때 같이 말이죠.- 기존 함수의 인수 몇 개를 고정한 함수를 부분 적용(partially applied) 함수 또는 부분(partial) 함수라고 부릅니다.
- 부분 적용은 같은 인수를 여러 번 반복하고 싶지 않을 때 유용합니다. send(from, to)라는 함수가 있는데 from을 고정하고 싶다면 send(from, to)의 부분 함수를 구현해 사용하면 됩니다.
- setTimeout에 메서드를 전달할 때처럼, 객체 메서드를 콜백으로 전달할 때 ’this 정보가 사라지는’ 문제가 생깁니다. 이번 챕터에선 이 문제를 어떻게 해결할지에 대해 알아보겠습니다.
사라진 ‘this’
- 객체 메서드가 객체 내부가 아닌 다른 곳에 전달되어 호출되면
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
가 됩니다.
방법 1: 래퍼
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
가 변경되면, 변경된 객체의 메서드를 호출하게 됩니다.
방법 2: bind
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로 고정됩니다.)
- 객체 메서드에 bind를 적용
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
에 전달됩니다.
- 래퍼에 전달된 인수("Hello")는...args
가 됩니다.
6.11 화살표 함수 다시 살펴보기
요약
화살표 함수가 일반 함수와 다른 점은 다음과 같습니다.
- this를 가지지 않습니다.
- arguments를 지원하지 않습니다.
- new와 함께 호출할 수 없습니다.
- 이 외에도 화살표 함수는 `super가 없습니다.
- 화살표 함수는 컨텍스트가 있는 긴 코드보다는 자체 '컨텍스트’가 없는 짧은 코드를 담을 용도로 만들어졌습니다.
- 화살표 함수는 단순히 함수를 ‘짧게’ 쓰기 위한 용도로 사용되지 않습니다. 화살표 함수는 몇 가지 독특하고 유용한 기능을 제공합니다.
- 자바스크립트를 사용하다 보면 저 멀리 동떨어진 곳에서 실행될 작은 함수를 작성해야 하는 상황을 자주 만나게 됩니다.
-arr.forEach(func)
–func
는forEach
가 호출될 때 배열arr
의 요소 전체를 대상으로 실행됩니다.
-setTimeout(func)
–func
는 내장 스케줄러에 의해 실행됩니다. - 그런데 어딘가에 함수를 전달하게 되면 함수의 컨텍스트를 잃을 수 있습니다. 이럴 때 화살표 함수를 사용하면 현재 컨텍스트를 잃지 않아 편리합니다.
화살표 함수에는 'this’가 없습니다
- 화살표 함수 본문에서
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’가 없습니다
- 화살표 함수는 일반 함수와는 다르게 모든 인수에 접근할 수 있게 해주는 유사 배열 객체
arguments
를 지원하지 않습니다. - 이런 특징은 현재
this
값과arguments
정보를 함께 실어 호출을 포워딩해 주는 데코레이터를 만들 때 유용하게 사용됩니다. - 아래 예시에서 데코레이터
defer(f, ms)
는 함수를 인자로 받고 이 함수를 래퍼로 감싸 반환하는데, 함수f
는ms
밀리초 후에 호출됩니다.
📚 참고 : javascript.info
Author And Source
이 문제에 관하여(6. 함수 심화학습(3)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@protect-me/함수-심화학습3저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)