전역 객체, call&apply와 데코레이터

100pearlcent·2021년 9월 8일
0

JavaScript

목록 보기
11/22
post-thumbnail

전역 객체

전역 객체를 사용하면 어디서나 사용 가능한 변수나 함수를 만들 수 있다

alert('Hi');
window.alert('Hi');

전역 객체의 모든 프로퍼티는 아래와 같이 직접 접근할 수 있다

var gVar = 5;
alert(window.gVar); // 객체 window의 프로퍼티 gVar

브라우저에서 var로 선언한 전역 함수나 전역 변수는 전역 객체의 프로퍼티가 된다 > 모듈을 사용하는 모던 자바스크립트는 이 방식을 지원하지 않으므로 실제로는 사용 ❌


let gLet = 6;
alert(window.gLet); // undefined

var대신 let을 사용하면 위 예시와는 달리 전역 객체를 통해 변수에 접근 불가능



// 모든 스크립트에서 현재 사용자에게 접근할 수 있게 전역객체에 추가함
window.currentUser = {
	name: 'Jinju'
};

// 아래와 같은 방법으로 currentUser에 접근 가능
alert(currentUser.name); // Jinju

// 지역 변수 'currentUser'가 있다면
// 지역 변수와 충돌 없이 전역 객체 windwo에서 이를 명시적으로 가져올 수 있음
alert(window.currentUser.name);

전역 변수는 되도록 사용하지 않는것이 좋다
함수를 만들 땐 외부 변수나 전역 변수를 사용하는 것보다 인풋 변수를 받고 이를 이용해 아웃풋을 만들어내게 하는 편이 좋다



call/apply와 데코레이터, 포워딩

- 코드 변경 없이 캐싱 기능 추가하기

function slow(x) {
	// CPU 집약적인 코드
  alert(`slow(${x}을/를 호출함)`);
  return x;
}

// DECORATOR
function cachingDecorator(func) {
	let cache = new Map();
  
  	return function(x) {// WRAPPER STARTS HERE
    	if(cache.has(x)) {	// cache에 해당 key가 있으면
            return cache.get(x); // 대응하는 값을 cache에서 읽어온다
        }
      
      let result = func(x);	// 그렇지 않은 경우에는 func를 호출하고
      cache.set(x, result);	// 그 결과를 캐싱(저장)한다
      
      return result;
    };	// WRAPPER ENDS HERE
}

slow = cachingDecorator(slow);

alert(slow(1));	// slow(1)이 저장되었습니다
alert('다시 호출' + slow(1)); // 동일한 결과

alert(slow(2));	// slow(2)가 저장되었습니다
alert('다시 호출' + slow(2)); // 윗줄과 동일한 결과
  • decorator: 인수로 받은 함수의 행동을 변경시켜주는 함수
  • caching wrapper: 모든 함수를 대상으로 decorator를 호출 할 수 있는데 이 때 반환되는 것
  • slow 본문을 수정하는 것보다 독립된 래퍼 함수 cachingDecorator를 사용할 때 생기는 이점
  • cachingDecorator를 재사용 할 수 있다 = 원하는 함수 어디든 cachingDecorator를 적용할 수 있다
  • 캐싱 로직이 분리되어 slow자체의 복잡성이 증가하지 않는다
  • 필요 시 여러 개의 데코레이터를 조합해서 사용가능



- func.call을 사용해 컨텍스트 지정하기

// worker.slow에 캐싱 기능 추가하기
let worker = {
	someMethod() {
    	return 1;
    },
  
  slow(x) {
  	// CPU 집약적인 코드
    alert(`slow(${x}을/를 호출함)`);
    return x * this.someMethod(); // (1)
  }
};

// 이전과 동일한 코드
function cachingDecorator(func) {
	let cache = new Map();
  	return function(x) {
    	if(cache.has(x)) {
        	return cache.get(x);
        }
      	let result = func(x); // (2)
      	cache.set(x, result);
      	return result;
    };
}

alert(worker.slow(1)); // 기존 메서드는 잘 동작
worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용
alert(worker.slow(2)); // Error: Cannot read property 'someMethod' of undefined

(1)줄에서 this.someMethod() 접근에 실패해서 에러가 발생했다
👉 (2)줄에서 wrapper가 기존 함수 func(x)를 호출하면 thisundefined가 되기 때문
= wrapper가 기존 메서드 호출 결과를 전달하려 했으나 this의 컨텍스트가 사라져서 에러 발생


이를 해결하기 위해서,
this를 명시적으로 고정해 함수를 호출할 수 있게 해주는 내장 함수 메서드

func.call(context, arg1, arg2, ...)

메서드를 호출하면 메서드의 첫 번째 인수가 this, 이어지는 인수가 func의 인수가 된 후, func이 호출된다

아래 함수와 메서드를 호출하면 거의 동일한 일이 발생한다

func(1, 2, 3);
func.call(obj, 1, 2, 3);

둘 다 인수로 1, 2, 3을 받지만 func.call에서는 thisobj로 고정된다는 점이다


wrapper 안에서 call을 사용해 컨텍스트를 원본 함수로 전달하면 에러가 발생하지 않는다

let worker = {
	someMethod() {
    	return 1;
    },
  
  	slow(x) {
    	alert(`slow(${x}을/를 호출함)`);
      	return x * this.someMethod();	// (1)
    }
};

function cachingDecorator(func) {
	let cache = new Map();
  	return function(x) {
    	if(cache.has(x)) {
        	return cache.get(x);
        }
      	let result = func.call(this, x); // !!
      	cache.set(x, result);
    };
}

worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용

alert(worker.slow(2)); // 제대로 동작
alert(worker.slow(2)); // 제대로 동작하나 원본 함수가 호출되지 않고 캐시 된 값이 출력됨 

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


- 여러 인수 전달하기

복수 인수를 가진 메서드 캐싱하는 법

let worker = {
	slow(min, max) {
    	return min + max;	// CPU 많이 쓰는 작업이라고 가정
    }
};

// 동일한 인수를 전달했을 때 호출 결과를 기억할 수 있어야 한다
worker.slow = cachingDecorator(worker.slow);

해결할 수 있는 방법들

  • 복수 key를 지원하는 map과 유사한 자료 구조 구현하기 (서드파티 라이브러리 등을 사용해도 된다)
  • 중첩 map을 사용 (map, result)쌍 저장은 cache.set(min)으로, resultcache.get(min).get(max)을 사용해서 얻는다
  • 두 값을 하나로 합치기. map의 key로 문자열 "min,max"를 사용한다. 여러 값을 하나로 합치는 코드는 해싱함수에 구현해서 유연성을 높인다

3번째 방법을 이용 + func.call(this, x)func.call(this, ...arguments)로 교체해서 코드를 수정해본다면

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); // (1)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (2)

    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) ); // 동일한 결과 출력 (캐시된 결과)

개선 후 바뀐 점
👉 (1)줄에서 hash가 호출되면서 arguments를 사용한 단일 키가 만들어짐. (3, 5) > "3,5"
👉(2)줄에서 func.call(this, ...arguments)를 사용해서 컨텍스트(this)와 래퍼가 가진 인수 전부 (...arguments)를 기존함수에 전달


func.apply

그런데 func.call(this, ...arguments) 대신 func.apply(this, arguments)를 사용해도 된다

내장 메서드 func.apply

func.apply(context, args) // syntax

applyfuncthiscontext로 고정해주고, 유사 배열 객체인 args를 인수로 사용할 수 있게 해준다
callapply의 문법적 차이는 call이 복수 인수를 따로 받는 대신 apply는 인수를 유사 배열 객체로 받는다는 점

func.call(context, ...args); // 전개 문법을 사용해서 인수가 담긴 배열을 전달하는 것과
func.apply(context, args); // call을 사용하는 것은 동일

하지만 약간의 차이점은 존재한다

  • 전개 문법 ...은 iterable args을 분해해서 call에 전달할 수 있도록 해준다
  • apply는 오직 유사 배열 형태의 args만 받는다

위 두 차이점을 빼면 두 메서드는 완전히 동일하게 동작하므로

👉 인수가 iterable 형태라면 call을, 유사배열 형태라면 apply를 사용한다

0개의 댓글