본문 바로가기
책정리/자바스크립트

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

by 난파선 2017. 1. 14.

 '바인딩', 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"}


댓글