5 Closure

5.1 클로저의 원리 및 이해

  • MDN 에서는 클로저는 "함수와 그 함수가 선언될 당시의 lexical environment의 상호관계(combination)에 따른 현상 이라고 표현하고있다. 
  • '선언될 당시의 lexical environment는 실행 컨텍스트중 하나인 outerEnvironmentReference에 해당한다.
    • lexicalEnvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해진다고 이미 설명한 바 있다.
  • 위에서 의미하는 combination이란, 내부함수에서 외부변수를 참조하는 경우에 한해서만 combination, 즉 '선언될 당시의 LexicalEnvironment와의 상호관계' 이다.

 

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner();
}

var outer2 = outer();
console.log(outer2);  // 2
console.log(outer2);  // 2
  • 위의 예제를 살펴보면, outer 내부의 inner function이 outer의 a식별자를 참조하고있다. 그러나 특별한 현상은 안보인다.
  • 그러나 outer 함수의 실행컨텍스트가 종료된 시점에서는 a변수를 참조하는 대상이 없어진다.
  • a, (inner 변수의 값)들은 언젠가 가비지컬렉터에 의해 소멸될 것이다.
  • 만약 outer의 실행 컨텍스트가 종료된 후에도 inner 함수를 호출할 수 있게 만들면 어떻게 될까??

 

 

var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner;   // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}

var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3
  • inner 함수의 실행 결과가 아니라 inner 함수 자체를 반환했다.
  • 결과 또한, a가 2,3 이전 값을 기억 했다.
  • inner 함수의 실행 시점에는 outer 함수는 이미 실행종료된 상태인데, outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있을까?
  • 잘 알다시피, 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그값은 수집대사엥 포함시키지 않는다.
  • inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmnetReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다.

[120page 그림]

(설명: 원래는 LexicalEnvironment 전부를 gc하지 않았으나, 2019년 기준으로 v8엔진에서는 내부함수에서 사용하는 변수만 남겨두고 나머지는 gc)

  • 즉 "어떤함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상" 이란 "외부함수의 LexicalEnvironment가  가비지컬렉팅 되지 않는 현상" 을말하는 것이다.
  • 이를 정리하자면 클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상이다.

 

다른책에서는 Clousre를 이렇게 표현하는데 그 중 가장 이해하기 쉬운걸 소개함

- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수 - 존레식, <자바스크립트 닌자 비급>
- 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수 - 송형주 고현준, <인사이드 자바스크립트>
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행 될 때 사용할 변수들만을 기억하여유지시키는 함수 - 유인동 <함수형 자바스크립트 프로그래밍>

  • 주의할점은 '외부로 전달' 이 곧 retunr 만을 의미하는 것은 아니다.

 

- return 없이도 클로저가 발생하는 경우

(function () {
  var a = 0;
  var intervalId =  null;
  var inner = function() {
    if(++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
}) ();
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListenr('click', function() {
    console.log(++count, 'time clicked');
  });
  
  document.body.appendChild(bttuon);
}) ();
  • 함수내부에서 지역변수를 참조하고 있다. 두상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저 이다.

 

5.2 클로저와 메모리 관리

  • 클로저는 매우 중요한 개녀이나, 일부는 메모리 누수의 위험을 이유로 클로저 사용을 지양해야 한다고 한다.
  • '메모리 누수' 라는 표현은 개발자의 의도와 달리 어떤값의 참조 카운트가 0이 되지않아 GC의 수거대상이 되지 않는 경우 이다.
  • 적절한 사용과 적절한 클로저의 메모리 해제만 있으면 괜찮다.
  • 참조카운트를 0으로 만들면 GC의 수거대상이된다. 식별자에 참조형이 아닌 기본형 데이터 (보통 null이나 undefined)를 할당 하면 된다.
var outer = function () {
  var a = 1;
  var inner = function() {
    return ++a;
  }
  return inner;   // ** 함수의 실행결과가 아닌 함수의 주소 자체를 리턴
}

var outer2 = outer();
console.log(outer2());  // 2
console.log(outer2());  // 3

outer2 = null;             //outer 식별자의 inner 함수 참조를 끊음
(function () {
  var a = 0;
  var intervalId =  null;
  var inner = function() {
    if(++a >= 10) {
      clearInterval(intervalId);
      inner = null;                       //inner 식별자에대하여 참조를 끊음 
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
}) ();
(function () {
  var count = 0;
  var button = document.createElement('button');
  button.innerText = 'click';
  button.addEventListenr('click', function() {
    console.log(++count, 'time clicked');
    if(count >= 10) {
      button.removeEnventListner('click', clickHandler);
      clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음.
    } 
  });
  
  
  document.body.appendChild(bttuon);
}) ();

 

 

5.3 클로저 활용 사례

5.3.1 콜백함수 내부에서 외부 데이터를 얻고자 할때.

5.3.2 정보은닉

 

5.3.3 부분적용함수

 - debounce

  • 기본적인 예제는 인터넷에서도 많으니 실무에서 활용할 수 있는예제 디바운스만 정리해보았다.
  • 디바운스는 짧은 시간동안 동일한 이벤트가 많이 발생할경우, 이를 전부처리 하지않고, 처음 또는 마지막 발생한 이벤트에 대해 한번만 처리하는 것이다. (Front-End performance 개선에 많은 도움이 된다)
  • scroll, wheel, mousemove,resize 그리고 최근에 검색어 자동완성 등에서 사용한 경험이있다.

 

var debounce = function (eventName, func, wait) {
  var timerId = null;
  return function (event) {
    var self = this;
    console.log(eventName, ' event 발생';
    clearTimeout(timerId);
    timerId = setTimeout(func.bind(self, event), wait);
  };
};

var moveHandler = function(e) {
  console.log('move event 처리');
}

documnet.body.addEventListener('mousemove', debounce('move',moveHandler, 500));

 

 

5.3.4 커링 함수

  • 커링 함수 (currying function)란 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인형태로 구성한 것을 말한다 (흡사 build pattern과 비슷하지만, function 으로 사용되니 좀다르긴 한듯)
  • 커링은 하나의 한개의 인자만 전달하는 것을 원칙으로 한다.
var curry3 = function (func) {
  return function (a) {
    return function (b) {
      return func(a,b);
    }
  }
}

var getMaxWith10 = curry3(Math.max)(10);

consoel.log(getMaxWith10(3));  // 3
console.log(getMaxWith10(20)); // 20
  • 위처럼 일부 매개변수는 고정되고, 가변적인 parameter를 넘겨줄때 사용된다.
  • 프로젝트 내부에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고, 일부만 바뀌는 경우에 대해 이를 적용하면 유용하다.

 

var getHttpRequest = function (baseUrl) { 
  return function (path) {
    return function (id) {
      return fetch(baseUrl + path + '/' + id); 
    }
  }
}


//es6로 만들면 가독성이 훨씬 높아진다.
var getHttpRequest = baseUrl => path => id => fetch(baseUrl + path + '/' + id);
  • getHttpRequest라는 공통함수를 만들었다. 이를 기반으로 아래와 같이 코드 중복을 제거할 수있다.
var userInfoUrl = 'http://localhost:1234/';
var fileUrl = 'http://localhost:8574/';

// 유저 직군별 request 준비
var getUserInfo = getHttpRequest(userInfoUrl);   // http://localhost:1234/
var getDeveloperInfo = getUserInfo('developer'); // http://localhost:1234/developer
var getTaInfo = getUserInfo('ta');               // http://localhost:1234/ta

// file 종류별 request 준비
var getFileInfo = getHttpRequest(fileUrl);   // http://localhost:1234/
var getZipInfo = getUserInfo('zip');         // http://localhost:1234/zip
var getImageInfo = getUserInfo('image');     // http://localhost:1234/image


// 실제 get request
var zipfile = getZipInfo(123);    // http://localhost:1234/zip/123
var zipfile2 = getZipInfo(456);   // http://localhost:1234/zip/456

var image = getImageInfo('navi'); // http://localhost:1234/zip/navi
var image2 = getImageInfo('dog'); // http://localhost:1234/zip/dog


// ..... ommitted

 

  • 특히 이런이유로 커링이 광범위 하게 사용되는데, Redux middleware에서도 사용된다.
// Redux middleware 'Logger'
const logger = store => next => action => {
  console.log('dispatching', action);
  console.log('next state', store.getState());
  return next(action);
}


// Redux Middleware 'thunk'
const thunk = store => next => action => {
  return typeof action === 'function'
    ? action(dispatch, store.getState)
    : next(action);
}
  • 두 미들웨어는 공통적으로 store, next, action 순서로 인자를 받는다.
  • store는 한번만 생성되고 바뀌지 않고, dispatch의 의미를 가지는 next도 마찬가지지만, action은 매 요청마다 달라진다.
  • 그러므로 store, next 값이 결정되면 redux 내부에서 logger 또는 thunk에 store,next를 미리 넘겨서 반환된 함수를 저장시켜놓고, 이후에는 action만 받아서 처리할 수 있게끔 한 것이다.

5.4 정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
  • 내부함수를 외부로 전달하는 방법에는 함수를 return하는 경우뿐 아니라 콜백으로 전달하는 경우도 포함된다.
  • 클로저는 그 본질이 메모리를 계속 차지하는 개념ㄴ이므로 더는 사용하지 않게 된 클로저에 대해서는 메모리를 차지하지 도록 관리해줄 필요가 있다.

'To be Developer > JavaScript' 카테고리의 다른 글

Core Javascript 3 - this  (0) 2020.05.14
Core Javascript - 2 Execution Context  (0) 2020.05.13
Core Javascript 1 - Data Type  (0) 2020.05.12
[javascript] Closure와 필요성.  (0) 2019.11.08
[javascript] let 과 var 그리고 hoisting  (0) 2019.11.07

3.this

  • 대부분 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의마한다.
  • 그러나 자바스크립트에서의 this는 어디에서든 사용 가능 하다.
  • 함수와 객체(메서드)의 구분이 느슨한 자바스크립트 에서는 this는 실질적으로 이둘을 구분하는 유일한 기능이다.

 

3.1 상황에 따라 달라지는 this

  • JS에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다.
  • this는 함수를 호출할 때 결정된다. 함수를 어떤 방식으로 호출하느냐에 따라값이 달라진다.

- 전역공간에서 this

  • 전역공간에서 this는 전역 객체를 가르킨다.
//browser 환경에서 전역객체
console.log(this)    // {alert: f() ........}
console.log(window)  // {alert: f() ........}
console.log(this === window) //true
//nodejs 환경에서 전역객체
console.log(this)    // {process: {title: 'node' ........}
console.log(global)  // {process: {title: 'node' ........}
console.log(this === global) //true

 

- 다음 소스코드를 살펴보자.

var a = 1;
console.log(a);         // 1
console.log(window.a);  // 1
console.log(this.a);    // 1
  • 위에서 우리가 확인할 수 있는 사실은 자바스크립트의 모든 변수는 실은 특정객체의 프로퍼티 라는점이다.
  • 특정 객체란 실행 컨텍스트의 LexicalEnvironment 이다.
  • 전역컨텍스트의 L.E는 전역객체를 그대로 참조한다.
  • '전역변수를 선언하면 자동으로 전역객체의 프로퍼티로도 할당한다' 라는 말은 틀린말이다.
  • '전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다'가 올바른 표현이다.

 

- 그럼 전역공간에서는 var로 indentifer를 선언하는 대신, window의 프로퍼티에 직접 할당해도 상관없을까?

var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2

window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4
  • 대부분의 경우에는 이말이 맞다
  • 그러나 전역변수의 선언과 전역객체의 프로퍼티 할당사이에 전혀다른 경우가 있다. 'delete' 이다
var a = 1;
delete window.a;                  // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b;                  // false
console.log(b, window.b, this.b); // 2 2 2
window.c = 3;
delete window.c;                  // true
console.log(c, window.c, this.c); // error !
  • 전역객체의 프로퍼티로 할당한 경우는 삭제가 되는 반면
  • 전역변수로 선언한 경우에는 삭제가 되지않는다.
  • var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보인다.

 

3.1.2 메서드로서 호출할 때 그 메서드 내부에서의 this

 - method vs function

  • 함수와 메서드는 근본적으로 코드뭉치이다.
  • 유일한 차이는 독립성에 있다.
  • 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상객체에 관한 동작을 수행한다.
  • JS에서는 상황별로 this 키워드에 다른값을 부여하게 함으로써 이를 구현했다.

 

  • JavaScript를 처음 접하는 사람들은 흔히 메서드를 '객체의 프로퍼티로 할당된 함수'로 이해하지만 이는 반은맞고 반은 틀리다.
  • 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지않으면 함수로 동작한다.
var func = function (x) {
  console.log(this, x);
}

func(1);  // Window {.....} 1

var obj = {
  method: func
}

obj.method(2); // {methog: f} 2
  • 위의 예제에서 볼 수 있듯이, 원래의 익명함수는 그대로인데 이를 변수에담아 호출한 경우와 obj객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것이다.

 

- 그렇다면 '함수로서 호철'과 '메서드로서의 호출'을 어떻게 구분할까??

  • 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다. (대괄호 표기법 포함)

 

var obj = {
  method: function (x) { console.log(this, x); }
}
obj.method(1);    // { method: f } 1
obj['method'](2); // { method: f } 2
  • 점 표기법이든, 대괄호 표기법이든 어떤 함수를 호출할 때 그 함수이름 앞에 객체가 명시돼 있는 경우는 메서드로써의 호출이다.

 

- 메서드 내부에서의 this

  • this에는 호출한 주체에 대한 정보가 담긴다.
  • 마지막 점 앞에 명시된 객체가 곧 this이다.
var obj = {
  methodA: function() { console.log(this); }
  inner: {
    methodB: function() { console.log(this); }
  }
}

obj.methodA();               // { methodA: f, inner { .... } } { === obj}
obj['methodA']();            // { methodA: f, inner { .... } } { === obj}

obj.inner.methodB();         // { methodB: f } { === obj.inner}
obj.inner['methodB']();      // { methodB: f } { === obj.inner}
obj['inner'].methodB();      // { methodB: f } { === obj.inner}
obj['inner']['methodB']();   // { methodB: f } { === obj.inner}

 

3.1.3 함수로서 호출할 때 그 함수 내부에서의 this

  • fucntion을 함수로서 호출할 경우에는 this가 지정되지 않는다.
  • this란 호출한 주체의 정보이기 때문이다. 그러므로 함수로서의 호출의 this는 window 객체이다
  • 그러나 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적했다.

 

- 메서드의 내부함수에서의 this

  • 앞서 말한 '설계상의 오류' 때문에 실제 동작과 우리의 예측과는 다를 때가있다.
var obj1 = {
  outer: function () {
    console.log(this)              // (1)
    var innerFunc = function() {
      console.log(this)           // (2) (3)
    }
    innerFunc();
    
    var obj2 = {
      innerMethod: innerFunc
    };
  
    obj2.innerMethod();
  }
};

obj1.outer();
  • 정답은 (1): obj1, (2): window, (3): obj2이다.
  • 같은함수임에도 7번째줄에 binding 되는 this와 12번째줄에 의해서 바인딩되는 this가 달라진 것이다.
  • 즉 this란 함수를 실행하는 당시 scope와는 관계없이, function 앞의 object가 가장중요한 것이다.

 

- 메서드의 내부 함수에서의 this를 우회하는 방법

  • 위와같은 상황에서 우리는 this를 확실하게 구분할 수 있지만, 본래 this의 취지와는 달라졌다.
  • 호출주체가 없을때도 주변 환경의 this를 그대로 상속받는게 더 일관적이다.
  • this 역시 현재 컨텍스트에 바인딩 된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이다.
  • 가장 대표적인 방법은 변수를 활용하는 방법이다.
var obj1 = {
  outer: function () {
    console.log(this)             // (1) { outer : f}
    var innerFunc1 = function() {
      console.log(this)           // (2) { window }
    }
    innerFunc1();
    
    
    var self = this;
    var innerFunc2 = function() {
      console.log(self);
    }
  
    innerFunc2();               // (3) { outer: f }
  }
};

obj1.outer();
  • 위처럼 변수에 할당해서 출력한다면 의도한 대로 동작한다.
  • 사람마다 self, _this, that 혹은 다른변수명을 많이쓰지만 self가 가장많이쓰인다.

 

- ES6 에서 this를 바인딩하지 않는 함수

var obj = {
  outer: function () {
    console.log(this);   // (2) { outer: f }
    var innerFunc = () => {
      console.log(this); // (2) { outer: f }
    };
    innerFunc();
  }
};

obj.outer();
  • arrow function에서 실행컨텍스트를 생성할때 this binding 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 
  • react에서 arrow function 사용시에 this를 bind 해주지 않아도 되는 이유도 위와같은 이유때문이다.
    (arrwo function을 쓰지않는다면 this.methodName.bind(this); 이런식으로 해주어야함. 그렇지않으면 null)

 

3.1.4 콜백함수 호출시 그 함수 내부에서의 this

  • 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 한다.
  • A는 함수 B의 내부로직에 따라 실행되며, this 역시 함수 B내부로직에서 정한 규칙에 따라 값이 결정됨.
  • 기본적으로 콜백함수도 함수라 window를 참조하지만, 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

 

setTimeout(function () {console.log(this); }, 300); // (1)

[1,2,3,4,5].forEach( function (x) {                 // (2)
  console.log(this, x);
});

documnet.body.innHTML += '<button id="a"> 클릭 </button>';
documnet.body.querySelector('#a').addEventListener('click', function (e) { console.log(this,(e); });
  • 위는 콜백함수 이다.
  • 1,2의 경우는 따로 this를 지정하지 않아 this는 window 객체가 된다.
  • 그러나 마지막 3번째 콜백함수에서는 제어권을 앞에 지정한엘리먼트에게 수여하기때문에 this 는 a tag가 될 것이다.

 

3.1.5 생성자 함수 내부에서의 this

  • 객체지향 언어에서는 생성자를 class, 클래스를 통해 만든 객체를 instance라고 한다.
  • 프로그래밍 적으로 본다면 생성자는 인스턴스를 만들기위한 틀이다.
  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 만들어질 구체적인 인스턴스이다.
  • 생성자 함수 (new 명령어)를 호출하면 생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 개체를 만들고, 공통 프로퍼티등을 this에 부여합니다.
  •  
var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}

var choco = new Cat('초코',7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

/*
Cat{ bark: '야옹' name: '초코' ..... }
Cat{ bark: '야옹' name: '나비' ..... }

*/

 

 

3.2 명시적으로 this를 바인딩 하는 방법

3.2.1 Call method

  • call method는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var func = function (a,b,c) {
  console.log(this, a, b, c);
}

func(1,2,3);     // Window {...} 1 2 3
func({ x: 1 }, 3,4,5) // { x: 1} 4 5 6
  • 위와같이 call method를 사용하면 임의 객체로 this를 지정 할 수있다.
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var obj = { 
  a: 1,
  method: function (x) {
    console.log(this.a, x, y);
  }
}

obj.method(2,3);             // 1 2 3
obj.method.call({a:4}, 5, 6) // 4 5 6

 

3.2.2 apply Method

  • call method와 거의 비슷하지만, 두번째 이후 인자의 매개변수는 array로 한번에 받는다.
// Function.prototype.call(thisArg[, argsArray])

var obj = { 
  a: 1,
  method: function (x) {
    console.log(this.a, x, y);
  }
}

obj.method(2,3);             // 1 2 3
obj.method.apply({a:4}, [5, 6]) // 4 5 6

 

3.2.3 실 사용 예시

- 생성자 내부에서 다른 생성자 호출

  • 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면, 간단하게 반복을 줄일 수 있다.
  • (마치 상속과 비슷한 느낌이다.)
function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Student(name,gender,school) {
  Person.call(this, name, gender);
  this.school = school;
}

function Employee(name,gender,company) {
  Person.call(this, name, gender);
  this.company = school;
}

var by = new Student('보영', 'female', '한국대');
var jn = new Employee('재난', 'male', '구글');

 

- 여러 인수를 묶어 하나의 배열로 전다할고 싶을때 - apply 활용

var number = [10, 20, 3, 16, 45];
var max = min = numbers[0];
numbers.forEach(function(number) {
  if(number > max) {
    max = number;
  }
  if(number < min) {
    min = number;
  }
});

console.log(max, min);          // 45 3
  • 위와 같이 배열의 가장 큰값 작은값을 구하려면 구현해야된다.
  • 그러나 Math.min, Math.min 과 apply를 사용한다면 아주 간결하게 만들수있다. 
var number = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);          // 45 3
  • es6에서는 sperad 연산자의 등장과 함께, 더욱 쉽게 구현 가능해졌다.
  • 그러나 실무에선 es5환경이 훨씬많으므로, 위와같은 방법을 더 많이쓴다.
var number = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min);          // 45 3

 

3.2.4 bind 메서드

  • bind는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지않고, 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드이다.
// Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

var func = function (a,b,c,d) {
  console.log(this, a, b, c, d);
}

func(1,2,3,4);     // Window {...} 1 2 3 4


var bindFunc1 = func.bind( { x: 1 } );
bindFunc1(3,4,5,6) // { x: 1} , 3,4,5,6

var bindFunc2 = func.bind( { x: 1 }, 4, 5 );
bindFunc2(5,6) // { x: 1}  4 5 5 6
bindFunc2(6,7) // { x: 1}  4 5 6 7

 

- name property

  • bind 메서드를 적용해서 새로만든 함수는 name property에 bound 접두어가 붇는다.
  • 'bound xxx' 라면 xxx원본 함수에 bind 메소드를 적용한 것이다.
  • call 이나 apply 보다 코드를 추적하기에 더 수월하다.
var func = function(a,b,c,d) {
  console.log(this,a,b,c,d);
};

var bindFunc = func.bind({x:1}, 4, 5);
console.log(func.name);       // func
console.log(bindFunc.name);   // bound func

 

-상위 컨텍스트의 this를 내부함수나 콜백함수에 전달하기 call vs bind

var obj1 = {
  outer: function () {
    console.log(this)              
    var innerFunc = function() {
      console.log(this)     
    }
    innerFunc.call(this);
  }
};

obj1.outer();
var obj1 = {
  outer: function () {
    console.log(this)              
    
    var innerFunc = function() {
      console.log(this)     
    }.bind(this);
    
    innerFunc();
  }
};

obj1.outer();

 

- *** callback 함수에 this context 전달

var obj1 = {
  printThis: function() {
    console.log(this);
  },
  printThisLater1: function() {
    setTimeout(this.logThis, 500);
  },
  printThisLater2: function() {
    setTimeout(this.logThis.bind(this), 500);
  },
};

obj1.printThisLater1();  // window { ... }
obj1.printThisLater2();  // obj { logThis: f, ... }

// polling 기능을 만들던중, polling이 좀 비효율적이라 
// long polling으로 기능을 만들고 싶었다.
// react component 내부에서 setTimeOut 함수를 호출 할경우, 
// state를 읽지못하는 이슈가 존재했다. function 실행하는 주체가 global 객체로 변하면서
// state를 읽을수 없는 문제였다.
// 그러나 this context에 대한 개념을 이해한뒤에 bind로 this 값을 전달하면서 
// long polling을 위해 state값을 읽고 해결할수있었다.

 

3.2.5 Arrow Function의 예외 사항

  • ES6에 새롭게 도입된 Arrow Function은 실행 컨텍스트 생성 시 this를 바인딩 하는 과정이 제외되었다.
  • 이 내부에는 this가 아예 없으며, this에 접근하면 스코프체인상 가장 가까운 this에 접근하게 된다.
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    }
    innerFunc();
  }
}

obj.outer();
  • 이렇게 하면 구지 변수로 this를 우회하거나 call/apply/bind를 사용할 필요가 없다. (ES6 한정)

 

3.2.6 별도의 인자로 this를 받는경우 (콜백 함수 내에서의 this)

  • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this를 인자로 지정할 수 있는 경우가 있다.
  • 배열메서드에 많이 포진되어 있으며 ES6의 map, set에서도 일부 존재한다.
var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function() {
    return this.sum / this.count;
  }
}

report.add(60, 85, 95);
console.log(report.sum, report.count, report.average()); // 240 3 80
  • 9번째 줄에서 전달해준 this가 바인딩 되면서, 콜백함수에서도 sum,count를 참조할 수 있게 된다.

- (참고) 콜백함수와 함께 thisArg를 인자로 받는 메서드들

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(arryLike[, callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
Set.prototype.forEach(callback[, thisArg])

 

3.3 정리

  • 다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립합니다. this를 예측하는 연습을 해봅시다.
    1. 전역공간에서 this는 전역객체 (window or global)을 참조한다
    2. 어떤 함수에서 메소드로 this를 호출한경우 호출 주체 (메서드앞 객체) 를 참조한다.
    3. 어떤 함수를 함수로서 호출할경우 this는 전역객체를 참조한다.
    4. 콜백함수 내부에서의 this는 해당 콜백함수의 제어권을 넘겨받은 함수가 정의 한 바에 따르며, 정의하지 않으면 전역객체를 참조
    5. 생성자 함수에서 this는 생성 될 인스턴스를 참조
  • 다음은 명시적 this 바인딩이다. 위 규칙의 부합하지 않을 경우 다음 내용을 바탕으로 this를 예측가능하다.
    1. call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출.
    2. bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듬
    3. 요소를 순회하면서 콜백 함수를 반복호출하는 일부 메서드는 this를 인자로 넘겨 줄 수 있다.

02 실행컨텍스트

1.실행컨텍스트란?

  • 자바스크립트는 어떤 실행 컨텍스트가 활성화 되는 시점에 선언된 변수를 hosting 하고 외부환경정보, this 값을 정하는등의 동작을 수행함.
  • 실행컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하여 이를 Call Stack에 쌓음.
  • es5에서는 하나의 실행 컨텍스트를 구성할 수 있는 방법에는 전역공간, eval() 함수, 함수 등이 있다.
var a = 1; // --------------------- (1)
function outer() {
    function inner() {
        console.log("inner", a); //undefined
        var a = 3; 
    }
    inner();//------------- (2)
    console.log("outer", a); 
}
outer(); // -----------------(3)
console.log("global", a);

// 실행결과
// inner undefined
// outer 1
// global 1

 

순서 1 2 3 4 5 6 7
        inner      
      outer outer outer    
    전역컨텍스트 전역컨텍스트 전역컨텍스트 전역컨텍스트 전역컨텍스트  

 

  • (2) 에선 전역 컨텍스트가 콜스택에 담긴다. 전역컨텍스트 또한 일반적 실행 컨텍스트이다.
  • (3)에서 outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경정보를 수집후 컨텍스트를 콜스택에 올린다. 
  • inner 내부에서 변수 a값을 출력하고 나면, inner 함수가 종료되면서 콜스텍에서 컨텍스트가 제거된다.
  • 나머지 컨텍스트도 순차적으로 Call Stack에서 제거 될 것이다.
  • 즉 스택 맨위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이다.
  • 실행 컨텍스트에 담기는 정보는 다음과 같다.
    1. VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한정보 + 외부환경정보, 선언시점의 LexicaEnvironment의 스냅샷으로, 변경사항은 반영되지 않음
    2. LexicalEnvironment: 처음에는VariableEnvironment와 같지만, 변경사항이 실시간으로 반영됨
    3. ThisBinding: this 식별자가 바라봐야할 대상객체.

 

2 VariableEnvironment

  • VariableEnvironment의 담기는 정보는 LexicalEnvironment와 타지만, 최초 실행시의 스냅샷을 유지한다는 점이 다르다.
  • 주로 실행컨텍스트를 생성할때  VariableEnvironment에 정보를 먼저 담은뒤, 그대로 복사해서 LexicalEnvironment를 만들고 LexicalEnvironment정보를 활용함

3 LexicalEnvironment

  • lexical environment대한 번역은 '어휘적 환경', '정적 환경' 이나 둘다 애매하므로 '사전적인' 이라는 느낌으로 외우면 편하다
  • 예를 들면 a,b,c 와 같은 식별자가 내부컨텍스트에 있고, 그 외 정보는 D를 참조하도록 구성돼 있다 이런식으로 컨텍스트를 구성하는 환경정보들을 사전에서 접하는 느낌으로 모아놓은 것이다.

3.1 envicronmentRecord와 호이스팅

  • environmentRecord 에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됨.
    • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자(전역은 x)
    • 선언한 함수가 있을경우 그 함수 자체
    • var로 선언된 변수의 식별자
    • 컨텍스트 처음부터 끝까지 한줄한줄 읽어나가며 순서대로 수집함.
 전역 실행 컨텍스트는 변수 객체를 생성하는 대신, 구동환경이 별도로 제공하는 객체 즉 global object를 활용한다. browser에 window 또는 nodejs의 global이 이에 해당됨.
 이들은 nativce object가 아닌 host object로 분류됨. 

 

  • 변수정보를 수집하는 과정을 모두 마쳤더라도, 코드가 실행되기 전의 상태이다.
  • 그러므로 자바스크립트 엔진은 이미 해당 환경에 속한 변수명들을 모두 알고있다.
  • '자바 스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 코드를 실행한다'
  • 여기서 호이스팅이라는 개념이 등장한다. (실제로 끌어올리지는 않지만 편의상 끌어올린다 표현)

 

3.2 호이스팅 규칙

  • environmentRecord에는 매개변수의 이름, 함수 선언, 변수명 등이 담긴다고 했다.
fuction a (x) {     // 수집대상 1(매개변수)
    console.log(x); // (1)
    var x;          // 수집대상 2(변수선언)
    console.log(x); // (2)
    var x = 2;      // 수집대상 3(변수선언)
    console.log(x); //(3)
}
a(1)
  • 호이스팅이 되지 않을때 (1) (2) (3) 에서 어떤값이 나올지 예상해보자.
  • (1) 에는 1이 호출되고
  • (2) 에는 undefined에가 출력되고
  • (3) 에는 아마도 2가 호출될 것 같다... 과연 그럴까?!

 

- 설명을 위해 우리가 자바스크립트 엔진이라 생각하고 위의 코드를 Hoisting 되게 바꿔보자. 

fuction a () {     
  var x; // 수집대상 1(매개변수)
  var x; // 수집대상 2(변수선언)
  var x; // 수집대상 2(변수선언)
  
  x = 1; // (매개변수 대입)
  console.log(x); // (1)    
  console.log(x); // (2)
  x = 2;      // 값 대입
  console.log(x); //(3)
}
a(1)
  • 위와 같이 호이스팅 된 결과이다.
  • 호이스팅이 끝나고 코드가 실행될 차례이다 (스코프체인 수집 및 this 할당과정은 이후 단원에서 나옴)
  • 실제로는 1, 1, 2 라는결과가 나왔다. hoisting 이라는 개념 때문이다.

 

- 한가지 호이스팅 되는 예를 더 살펴보자.

fuction a () {     
  console.log(b);  // (1)
  var b = 'bbb';   // 수집대상 1 (변수선언)
  console.log(b);  // (2)
  function b () {} // 수집대상 2 함수선언
  console.log(b);  // (3)
}
a();
  • 호이스팅에 대해 잘 모른다면 결과는 undefined / bbb / fuction... 이 예상된다.

 

- 그러나 실제로는 아래의 예시처럼 될 것이다.

fuction a () {     
  var b;           // 수집대상 1.변수는 선언부만 끌어올림
  var b = function b () { }// 수집대상 2.함수 선언은 저체를 끌어올림
                           // 호이스팅이 긑난 상태에서 함수 선언문은 
                           // 함수명으로 선언한 변수에 함수를 할당했다고 볼 수있다.
  
  console.log(b); // (1)
  b = 'bbb';
  console.log(b); // (2)
  console.log(b); // (3)
}
a();
  •  위와같이 호이스팅이 예상되며, 실행 결과는 b 함수 / 'bbb' / 'bbb' 가 될 것이다.

 

- 함수 선언문과 함수 표현식

  • 함수 선언문: function의 정의부만 존재. 반드시 함수명이 정의되어 있어야함.
  • 함수 표현식: function을 별도의 변수에 할당하는 것. 함수명이 없어도 됨
  • 함수를 정의한 함수 표현식을 '기명 함수 표현식' 이라하며, 일반적으로 함수 표현식을 '익명함수 표현식' 이라고 한다.
// 함수를 정의하는 3가지 방식
function a() {/* .... */}        //함수 선언문 a가 곧 변수명
a() //실행 ok

var b = function () {/* .... */} //함수 표현식 변수명 b가 곧 함수명
b() //실행 ok

var c = function d () {/* .... */} // 기명함수 표현식, 변수명은 c, 함수명은 d.
c(); // 실행 ok
d(); // 실행 error

 

 

- 둘의 차이는 무엇인지 함수 선언문과 함수 표현식의 차이를 알아보자.

console.log(sum(1,2));
console.log(mul(3,4));

//함수 선언문
function sum (a, b) {
  return a+b;
}


//함수 표현식
var mul = function (a, b) {
 return a*b;
}
  • 예상하건데 우리가 호이스팅에서 배웠듯이 결과는 3 / 12가 나올것같다.
  • 그러나 함수 표현식은 호이스팅이 다르다.

 

//함수 선언문은 전체를 호이스팅 함.
var sum = function (a, b) {
  return a+b;
}

//함수 표현식은 변수 선언부만 끌어올린다.
var mul 

console.log(sum(1,2));
console.log(mul(3,4));


//함수 표현식
mul = function (a, b) {
 return a*b;
}
  • 함수 표현식의 호이스팅 방식때문에 위의 결과는 3 / mul is not function 이라는 결과가 나올것이다.
  • 함수 선언문은 아무 문제없이 잘 실행되지만, 반대로 큰 혼란의 원인이 되기도 한다.

 

- 혼란 상황을 가정해보자. 회사에서 협엽을 하는데 A대리가 이미 SUM이라는걸 만들었다. 근데 만약 B라는 사원이 logging을 위해서 다음과 같이 SUM을 재정의 한 것이다.

console.log(sum(3,4)); // (1)

var sum = function (a, b) {
  return a+b;
}

.......

var a = sum(1,2);    //(2)

.......

function sum (x,y) {
  return x + "+" + y + " : " + x+y;
}

var c = sum(1,2);
console.log(c);     //(3)
  • 첫번째 console. 함수의 결과는 문자열을 반환 할 것이다.
  • 두번째 var a도 원래 의도는 number type의 값이 들어가야 맞지만, string 값이 들어가 있을것이다. (자바스크립트는 형변환이 없기에)
  • 결국, 소스코드의 오류는 없지만 전혀 다른 기대값이 들어가므로 굉장히 찾기 어려운 오류를 야기 할 것이다.
  • 그러므로 되도록 함수 표현식을 사용하는게 낫다.

 

3.3 스코프, 스코프 체인, outerEnvironmentReference

  • 스코프란 식별자(idnetifier)에 대한 유효범위 이다.
  • 어떤 함수 A의 외부에서 선언한 변수는 외부 scope 뿐만 아니라 A내부에서도 접근 가능하다.
  • 반면 A의 scope 내에서 선언된 변수는 A안에서만 접근 가능하다.
  • ES5는 function scope 이다. (Es6 부터는 bracket 스코프도 가능)
  • 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색하는 것을 Scope Chain 이라한다.
  • 이게 가능한 이유는 LexicalEnvironment의 두번째 수집자료인 outerEnvironmentRefrence 이다.

- Scope Chain

  • outerEnvironmentRefrence는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다.
  • '선언될 당시' 라는 점을 유의해야한다.
  • '선언하다' 라는 행위가 일어나는 시점은 Call Stack 상에서 실행 컨텍스트가 활성화 된 상태일 때뿐이다.

 

var a = 1;
var outer = function() {
  var inner = function () {
    console.log(a);
    var a = 3;
  }
  inner();
  console.log(a);
}

outer();
console.log(a);
  • 위의 예에서 풀어서 이야기해보자.
    1. Start: 전역컨텍스트가 활성화 된다. 전역컨텍스트의 environmentRecode에는 {a, outer} 식별자를 저장한다. 전역컨텍스트는 선언시점이 없으므로 outerEnvironmentReference에는 아무것도 담기지않음 (ths: 전역객체)
    2. 2번째줄 outer 실행 컨텍스트의 environmentRecord에 { inner } 식별자를 저장함. outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 담긴다. outer는 전역공간에 선언 되었으므로, 전역공간의 LexicalEnvironment를 참조복사한다. 이를 [ GLOBAL, { a , outer } ] 라고 표현하자 (this: 전역객체)
    3. 3번째줄 inner 실행 컨텍스트의 environmentRecord에 { a } 에 식별자를 저장함. outerEnvironmentRefernce에는 outer 함수의 LexicalEnvironment 즉, [ outer, { inner }] 를 참조복사 할 것이다 (this: 전역객체)
  • inner의 outerEnvironmentReference는 함수 outer의 LexcialEnvironment를 참조한다.
  • 함수 outer의 LexicalEnvironment 내부에 있는 outerEnvironmentReference는 전역 LexicalEnvironment를 참조한다.
  • 즉 무조건 스코프 체인상에서 가장 먼저 발견된 식별자에만 접근가능하다.
  • 위의 코드의 결과는 스코프체인의 특성때문에 undefined / 1 / 1 이라는 결과가 나온다.

 

코어 자바스크립트 57page 그림

 

  • 주의 해야할점은 스코프체인상에 있는 변수라고 해서 무조건 접근 가능한것은 아니다.
  • 위의 코드에서 inner 스코프의 LexicalEnvironment의 a라는 식별자가 존재하므로, 스코프체인 검색을 더이상 진행하지않고, undefined를 print 한것처럼 말이다. 이를 변수 은닉화 (variable shadowing) 라 한다.

 

참고: 크롬 브라우저 환경에서는 스코프체인중 현재 실행컨텍스트를 제외한 상위 스코프 정보들을 개발자 도구의 콘솔을 통해 간단하게 확인 할 수있다.

 

var a = 1;
var outer = function () {
  var b = 2;
  var inner = function () {
    console.dir(inner);
  }
  inner();
};
outer();

 

 

디버거를 이용하면 좀 더 제대로 된 정보를 확인 할 수 있다. 이는 모든브라우저에서 가능하다. (사파리가 가장 자세하다고함)
var a = 1;
var outer = function () {
  var b = 2;
  var inner = function () {
    console.dir(b);
    debugger;
  }
  inner();
};
outer();

 

 

 

3.4 전역변수 (global variable) 와 지역변수(local variable)

  • 여기 까지 왔다면 전역변수와 지역변수는 이해가 가능 할 것이다.
  • 전역객체에 선언된 outer, a는 전역변수이다
  • outer의 함수 스코프 내부에서 선언된 b와 inner는 지역변수이다.
  • 위에 sum이라는 함수의 재표기가 문제가 되는 경우는, 전역변수안에서 선언 했기 때문에 문제가 된것이다.
  • 코드의 안정성을 위해, 전역변수 사용을 최소하 해야한다.

 

3.5 this

  • 실행 컨텍스트가 활성화 당시에 this가 지정되지 않은 경우는 this는 전역객체를 가리키게 된다.
  • 그 밖에는 함수를 호출하는 방법에 따라서 this에 저장되는 대상이 다르다.

 

3.6 정리

  • 실행 컨텍스트는 객체 활성화되는 시점에 VariableEnvrionment, LexicalEnvironment, ThisBinding의 세가지 정보를 수집한다.
  • VariableEnvironment와 LexicalEnvironment는 동일한 내용으로 구성되지만, LexicalEnvironment는 함수 실행도중에 변경되는 사항이 즉시 반영되지만, VariableEnvironment는 초기상태를 유지한다
  • L.E 에는 매개변수명, 변수의 식별자, 선언한 함수의 수명등을 수집하는 environmentRecord와 바로 직전의 컨텍스트의 lexicalEnvironmnet정보를 참조하는 otuerEnvironmentReference로 구성된다.
  • 스코프는 변수의 유효범위를 만한다. outerEnvironmentReference는 해당 함수가 선언된 위치의 LexicalEnvironmnet를 참조한다. 코드상에서 변수에 접근하려고 할때 현재 컨텍스트 LexicalEnvironment에서 찾아보고 없으면 outerEnvironmentRecord에 담긴 LexicalEnvironment에서 찾는다. Scope Chain 에서 계속 못찾게 된다면 undefined를 반환 한다.

'To be Developer > JavaScript' 카테고리의 다른 글

Core Javascript - 5 Closure  (0) 2020.05.15
Core Javascript 3 - this  (0) 2020.05.14
Core Javascript 1 - Data Type  (0) 2020.05.12
[javascript] Closure와 필요성.  (0) 2019.11.08
[javascript] let 과 var 그리고 hoisting  (0) 2019.11.07

+ Recent posts