[Javascript]Decorators and forwarding

Suyeon·2020년 9월 8일
0

Javascript

목록 보기
17/31

Decorator

Decorator is a special function that takes another function and alters its behavior.

Benefits

  • Decorator is reusable. We can apply it to another function.
  • The logic is separate, it doesn't increase the complexity of slow itself.
  • We can combine multiple decorators if needed.

Example (caching)

Let’s say we have a function slow(x) which is CPU-heavy, but its results are stable. In other words, for the same x it always returns the same result.

If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations.

But instead of adding that functionality into slow() we’ll create a wrapper function, that adds caching.

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);

alert( slow(1) ); // slow(1) is cached
alert( "Again: " + slow(1) ); // the same

alert( slow(2) ); // slow(2) is cached
alert( "Again: " + slow(2) ); // the same as the previous line
  • cachingDecorator is a decorator.
  • The wrapped slow function still does the same. It just got a caching aspect added to its behavior.

Using “func.call” for the context

The caching decorator mentioned above is not work with object methods. Because this = undefined.

We can fix it using "func.call".

  • func.call(context, arg1, arg2, ...)
  • func.call allows to call a function explicitly setting this.
  • It runs func providing the first argument as this, and the next as the arguments.
function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

Example

Now we can use decorator.

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + 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" is passed correctly now
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // now make it caching

alert( worker.slow(2) ); // works
alert( worker.slow(2) ); // works, doesn't call the original (cached)

✔️ func.apply vs func.call

The only syntax difference between call and apply is that call expects a list of arguments, while apply takes an array-like object with them.

So, where we expect an iterable, call works, and where we expect an array-like, apply works.

  • The spread syntax ... allows to pass iterable args as the list to call.
  • The apply accepts only array-like args.

Call Forwarding

Passing all arguments along with the context to another function is called call forwarding.

let wrapper = function() {
  return func.apply(this, arguments);
};

Borrowing a method

// Error
function hash() {
  alert( arguments.join() ); 
}

// Working
function hash() {
  alert( [].join.call(arguments) ); // 1,2
}
  • arguments object is both iterable and array-like, but not a real array. Array-like can't use the array methods(join, slice...).

Going multi-argument (Decorator)

let worker = {
  slow(min, max) {
    alert(`Called with ${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() {
  return [].join.call(arguments);
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
profile
Hello World.

0개의 댓글