6. 함수 심화학습(3)

protect-me·2021년 6월 10일
0
post-thumbnail

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가 어떤 과정을 거쳐 전달되는지 자세히 살펴보겠습니다.

  1. 데코레이터를 적용한 후에 worker.slow는 래퍼 function (x) { ... }가 됩니다.
  2. worker.slow(2)를 실행하면 래퍼는 2를 인수로 받고, this=worker가 됩니다(점 앞의 객체).
  3. 결과가 캐시되지 않은 상황이라면 func.call(this, x)에서 현재 this (=worker)와 인수(=2)를 원본 메서드에 전달합니다.

여러 인수 전달하기

  • 복수 인수를 가진 메서드, worker.slow를 캐싱하려면 어떻게 해야 할지 생각해 봅시다.
  1. 복수 키를 지원하는 맵과 유사한 자료 구조 구현하기(서드 파티 라이브러리 등을 사용해도 됨)
  2. 중첩 맵을 사용하기. (max, result) 쌍 저장은 cache.set(min)으로, result는 cache.get(min).get(max)을 사용해 얻습니다.
  3. 두 값을 하나로 합치기. 맵의 키로 문자열 "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)thiscontext로 고정되고 인수도 고정된 함수 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 메서드는 조금 특별한 방식으로 동작합니다. 인수로 전달받은 함수를 호출할 때, thiswindow를 할당합니다
  • 따라서 위 예시의 this.firstNamewindow.firstName가 되는데, window 객체엔 firstName이 없으므로 undefined가 출력됩니다. 다른 유사한 사례에서도 대부분 thisundefined가 됩니다.

방법 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)'를 반환합니다. 이 객체를 호출하면 thiscontext로 고정된 함수 func가 반환됩니다.
  • 따라서 boundFunc를 호출하면 this가 고정된 func를 호출하는 것과 동일한 효과를 봅니다.
let user = {
  firstName: "John"
};
function func() {
  alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
  • 여기서 func.bind(user)functhisuser로 '바인딩한 변형’이라고 생각하시면 됩니다.
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.sayNowuser를 대상으로 호출됩니다).
    - partial을 호출할 때 받은 인수("10:00")는 ...argsBound에 전달됩니다.
    - 래퍼에 전달된 인수("Hello")는 ...args가 됩니다.


6.11 화살표 함수 다시 살펴보기


요약
화살표 함수가 일반 함수와 다른 점은 다음과 같습니다.

  • this를 가지지 않습니다.
  • arguments를 지원하지 않습니다.
  • new와 함께 호출할 수 없습니다.
  • 이 외에도 화살표 함수는 `super가 없습니다.
  • 화살표 함수는 컨텍스트가 있는 긴 코드보다는 자체 '컨텍스트’가 없는 짧은 코드를 담을 용도로 만들어졌습니다.
  • 화살표 함수는 단순히 함수를 ‘짧게’ 쓰기 위한 용도로 사용되지 않습니다. 화살표 함수는 몇 가지 독특하고 유용한 기능을 제공합니다.
  • 자바스크립트를 사용하다 보면 저 멀리 동떨어진 곳에서 실행될 작은 함수를 작성해야 하는 상황을 자주 만나게 됩니다.
    - arr.forEach(func)funcforEach가 호출될 때 배열 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에 전달되는 함수의 thisundefined 이어서 발생했습니다. alert 함수에서 undefined.title에 접근하려 했기 때문에 얼럿 창엔 에러가 출력됩니다.
  • 그런데 화살표 함수는 this 자체가 없기 때문에 이런 에러가 발생하지 않습니다.

화살표 함수 vs. bind

  • 화살표 함수와 일반 함수를 .bind(this)를 사용해서 호출하는 것 사이에는 미묘한 차이가 있습니다.
  • .bind(this)는 함수의 '한정된 버전(bound version)'을 만듭니다.
  • 화살표 함수는 어떤 것도 바인딩시키지 않습니다. 화살표 함수엔 단지 this가 없을 뿐입니다. 화살표 함수에서 this를 사용하면 일반 변수 서칭과 마찬가지로 this의 값을 외부 렉시컬 환경에서 찾습니다.

화살표 함수엔 'arguments’가 없습니다

  • 화살표 함수는 일반 함수와는 다르게 모든 인수에 접근할 수 있게 해주는 유사 배열 객체 arguments를 지원하지 않습니다.
  • 이런 특징은 현재 this 값과 arguments 정보를 함께 실어 호출을 포워딩해 주는 데코레이터를 만들 때 유용하게 사용됩니다.
  • 아래 예시에서 데코레이터 defer(f, ms)는 함수를 인자로 받고 이 함수를 래퍼로 감싸 반환하는데, 함수 fms 밀리초 후에 호출됩니다.



📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글