책정리/자바스크립트

함수형 자바스크립트 - 3장 변수 스코프와 클로저

난파선 2017. 1. 14. 11:22

 '바인딩', var 할당, 함수 인자 사용, this 전달, 프로퍼티 할당 등의 과정에서 자바스크립트의 이름에 값을 할당하는 행위를 가리킨다.

 '스코프'라는 단어는 자바스크립트에서 다양한 의미로 해석된다.
 * this 바인딩값
 * this 바인딩값이 정의하는 실행 컨텍스트
 * 변수 '생명주기'
 * 변수값 해석 방식 또는 어휘적 바인딩

3.1 전역 스코프

  • 스코프의 범위라는 말은 변수의 생명주기를 가리킨다.
aGlobalVariable = 'global';    // var 키워드 없이 변수를 정의하면 전역 스코프가 된다.

aGlobalVariable = 'modify'    // 변수의 속성값을 바로 바꿀 수 있다.
aGlobalVariable //-> modify
  • 전역 변수 선언은 프로그래밍 방법론에서 좋지 않은 패러다임에 속하며 동일한 변수명을 중복해서 선언하는 사이드 이펙트가 발생할 가능성이 크다.
  • 자바스크립트의 변수는 불변성을 가질 수 없다.
  • 프로그램의 어떤 코드에서든 언제든지 값을 고칠 수 있다는 것이 전역 변수의 약점이다.

3.2 어휘 스코프(Lexical Scoping)

  • 자바스크립트의 함수들은 동적이라기보다 어휘적으로 유효 범위가 정해진다.
  • 어휘 스코프란 변수와 값의 유효 범위를 가리킨다.
  • 중첩된 함수가 그 함수를 포함하는 함수의 모든 전달인자와 지역 변수들에 접근할 수 있음을 의미한다.
a = "Outer"

function afun() {
    var a = "Middle";

    return _.map([1,2], function(e) {
        var a = "In";
        return [a, e].join(' ');
    });
}

afun();     //-> ["In 1", "In 2"]

  • 가장 가까운 바인딩 컨텍스트에서 시작해서 바인딩을 찾을 때까지 외부로 변수 탐색이 이루어 진다.

3.3 동적 스코프(dynamic scoping)

  • 가장 저평가되었으면서도 동시에 과하게 남용되는 스코프 중 하나다.
  • 동적 스코핑은 이름을 가진 값들의 전역 테이블 개념을 기반으로 한다.
var globals = {};

function makeBindFun(resolver) {
  return function(k, v) {
    var stack = globals[k] || [];
    globals[k] = resolver(stack, v);
    return globals;
  };
}

var stackBinder = makeBindFun(function(stack, v) {    
// 키와 값을 받아와  키와 관련된 슬롯에 있는 전역 바인딩 맵에 값을 추가 한다.
  stack.push(v);
  return stack;
});

var stackUnbinder = makeBindFun(function(stack) {    // 마지막 값 바인딩을 꺼낸다.
  stack.pop();
  return stack;
});

/* 중간 과정 */
var stackBinder = function(k, v) {
    var stack = globals[k] || [];
    globals[k] = function(stack, v) {stack.push(v);
        return stack;
    }
    return globals;
}

/* stackBinder 을 명령행 프로그래밍으로 한다면 */
var stackBinder = function(k, v) {
    var stack = globals[k] || [];
    stack.push(v)
    globals[k] = stack;
    return globals;
}

var dynamicLookup = function(k) {    // k 키에 해당하는 값의 마지막 값 리턴
  var slot = globals[k] || [];
  return _.last(slot);
};

stackBinder('a', 1);
stackBinder('b', 2);

dynamicLookup('a')
//-> 1

globals;
// {a: [1], b: [2]}



function f() { return dynamicLookup('a'); };
function g() { stackBinder('a', 'g'); return f(); };

f();
//-> 1

g();
//-> 'g'

globals;
// {a: [1, "g"], b: [100]}    // 새로운 값이 추가 되면 [1, "g"]가 된다.
  • 어떤 함수를 호출한 함수가 누군지 알아야만 주어진 변수의 바인딩값을 알 수 있다는 것은 동적 스코핑의 치명적인 약점이다.

3.3.1 자바스크립트의 동적 스코프

  • this 레퍼런스에 동적 스코프가 적용된다.
  • this 레퍼런스가 자신이 처음 만들어졌던 환경과 다른 컨텍스트에서 다른 값을 가질 수 있다.
  • 호출자가 누구냐에 따라 this 레퍼런스의 값이 결정된다.
  • apply나 call이 참조하는 레퍼런스에 따라 this 레퍼런스가 달라진다.
  • this 레퍼런스는 동적으로 스코핑되므로 버튼 클릭과 같은 이벤트 처리함수에서 this를 사용하는 것은 위험하다.
function a() {return this;}
a(); //-> Window 전역객체
a.call('b'); //-> b
a.apply('c', []); //-> c

보충

  • 함수를 호출할 때에 먼저 유효 범위를 함수가 정의될 당시의 효력을 지니는 유효 범위 체인으로 설정한다.
function a() {
// 선언된 모든 변수는 전역 네임스페이스 대신 호출 객체의 프로퍼티가 된다.
}
a(); // 호출하는 함수는 전역객체이다.

(function(){
})();

3.4 함수 스코프

function strangeIdentity(n) {
  // intentionally strange
  for(var i=0; i<n; i++);
  return i;
}

strangeIdentity(138);
//-> 138
  • 한수 바디 안의 모든 var 선언을 암묵적으로 자신이 포함된 함수의 바디 시작 부분으로 이동시킨다.(호이스팅)
  • 함수의 모든 코드는 내부에 정의된 모든 변수에 접근할 수 있다.
function strangerIdentity(n) {
  // this가 해당 함수 스코프가 아닌 전역 스코프 이다.
  for(this['i'] = 0; this['i']<n; this['i']++);
  return this['i'];
}

strangerIdentity.call({}, 100); //-> 빈 객체를 넘겨 전역 스코프를 건드리지 않는 다.

// 언더 스코어에서는 _.clone() 함수를 사용해서 전역 컨텍스트 문제를 해결한다.

3.5 클로져

변수를 '캡처'하는 함수다.

3.5.1 클로저 시뮬레이션

  • 나중에 사용할 목적으로 정의된 스코프에 포함된 외부 바인딩을 캡처하는 함수다.
  • 클로저가 변수를 캡처했다면 생명 주기가 연장된다.
function a() {
    var b = "asdf";

    return function() {
        return b;
    }
}
var c = a()
c() //-> asdf

function createScaleFunction(FACTOR) {
  return function(v) {
    return _.map(v, function(n) {
      return (n * FACTOR);
    });
  };
}

var scale10 = createScaleFunction(10);    // 함수 인자를 캡쳐한다.
/* scale10 아래와 같은 익명함수가 만들어진다. */
function(v) {
    return _.map(v, function(n) {
        return n * 10;
    }
}

scale10([1,2,3]);
//=> [10, 20, 30]

자유 변수

내부 함수를 포함하는 내부에 정의된 모든 변수를 볼 수 있다는 것이 클로저를 유지하는 기본 원칙이다. 이렇게 자유롭게 볼 수 있는 변수를 '자유'변수라고 부른다

function makeAdder(CAPTURED) {
  return function(free) {
    return free + CAPTURED;
  };
}

var add10 = makeAdder(10);

add10(32);
// 외부 함수의 CAPTURED 변수를 선언하지 않고도 사용하므로 변수를 캡처했다라고 할 수 있다.
//makeAdder로 새로운 함수를 만들 때마다 고유한 CAPTURED 인스턴스가 만들어진다.
//=> 42

// 함수캡쳐
function averageDamp(FUN) {
  return function(n) {
    return average([n, FUN(n)]);
  }
}

var averageSq = averageDamp(function(n) { return n * n });    // 함수를 캡쳐한다.
averageSq(10);
//=> 55

셰도잉

x 라는 이름의 변수가 특정 스코프에 정의되어 있는 상태에서 하위스코프에서 같은 이름으로 다른 변수를 정의할 때 셰도잉이 발생한다.

  • 연달아 같은 이름의 변수를 두 번 선언하면 두 번째 선언만 유효하게 된다.
  • '가장 가까운' 변수 바인딩이 우선권을 갖는 다.
var c = 1; var c = 2;
c    //-> 2

var a = 0;

function b(a) {
    return "= " +a;
}
b(1); //-> "= 1";
b(); //->  "= "


function d(e) {
    return function(e) {
        return e + 1;
    }
}
var f = e(100);
f(1); //?

3.5.2 클로저 사용하기

function complement(PRED) {
  return function() {
    return !PRED.apply(null, _.toArray(arguments));
  };
}

function isEven(n) { return (n%2) === 0 }

var isOdd = complement(isEven);

isOdd(2);    //=> false

isOdd(413);    //=> true

function isEven(n) { return false; }    // 새로운 레퍼런스를 만들었다.

isOdd(13) //-> true;
// 클로저에서는 클로저가 만들어질 당시에 캡처한 값의 레퍼런스를 캡처한다.


function a(b) {
    return function() {
        return b;
    }
}

var o = {x:1};
var c = a(o);
c();    // {x:1}
o.z = 100;  // o 변수 수정, 새로 만든 게 아니다.
c();    // {x:1, z:100}    // 변수 o의 레퍼런스가 클로저의 내부와 외부에 모두 존재하므로 경계가 엉키게 된다.



function a() {
    var b = 0;    //private 변수

    return {
        c : function() {
            return b;
        }
    }
}

3.5.3 추상화 도구 클로저

  • 클로저를 이용해서 생성 시에 캡처되는 어떤 '설정'에 따라 다른 함수를 만들 수 있다.
function plucker(FIELD) {
  return function(obj) {
    return (obj && obj[FIELD]);
  };
}

var best = {title: "Infinite Jest", author: "DFW"};

var getTitle = plucker('title');   // title(설정) 필드를 가져오는 함수를 만든다.

getTitle(best);
//=> "Infinite Jest"

var books = [{title: "Chthon"}, {stars: 5}, {title: "Botchan"}];

var third = plucker(2);    // 3번째(설정) 값을 가져오는 함수

third(books);
//=> {title: "Botchan"}