다음 예제를 보자.

function object(a, b){
    var x = 10;
    var innerFunc = function(innerParam){ console.log((a + b) / (innerParam + x))};
    return innerFunc;
}

var pointer = object(2, 4);
pointer(2);

책에 보면 클로저를 설명하기 위해 실행 컨텍스트, 변수 객체, 스코프 체인, 스코프 프로퍼티 등등 처음보면 이해하기 어려운 내용들이 마구마구 나오게 된다.

이런 개념들을 알면 좋지만 몰라도 위 예제를 쉽게 이해할 수 있게 설명해보겠다.

 

전역 컨텍스트에서 object(2, 4)함수를 호출해 pointer 변수에 담고 pointer를 실행하고 있다. 하나 하나씩 나눠서 보도록 하자. 

object(2, 4)는 2와 4를 파라미터로 넘기기까지 한 함수(객체)이고 함수 내부에서 변수 x, innterFunc만 담은 함수(객체)라고 생각하자. 함수도 객체다. 그리고 object(2, 4)을 pointer 변수에 담는데 pointer변수에 위 객체의 메모리 주소를 담는 것이다. C언어를 배운 사람이라면 pointer 변수가 포인터 역할을 한다고 이해할 수 있을 것이다. 

 

pointer 변수에 (2)를 붙여 호출한다. 

결과값이 어떻게 될까?

우선 object 함수 a, b 파라미터는 2, 4로 넘어가고 pointer(2)를 실행할 때 x에는 10, innerParam에 2값을 넘김으로써 console창에는 (2 + 4) / (2 + 10), 즉 0.5가 표시된다.

 

여기서 pointer(2)를 실행하기 전까지 pointerobject() 안에 innerFunc를 계속해서 참조하고 있다. 함수객체니까 리턴값을 참조하고 있는 것이다. 

 

C언어에서 포인터를 배운 사람이라면 클로저를 금방 이해할 수 있을 것이다. 

 

그럼 클로저를 사용할 때 유의해야 할 사항들을 알아보자.

다음 예제를 보자.

function object(param){
    var num = param;
    return function(x){
        num += x;
        console.log('num: ' + num);
    }
}
var pointer = object(40);
pointer(5);
pointer(-10);

pointer 변수는 파라미터로 40을 넘기는 object함수 객체를 가리키고 있다. 같은 곳을 포인터로 가리키고 있는 것이다. 그러므로 pointer 변수를 계속 실행하면 num값이 계속해서 변한다. 

 

다음 유의해야할 경우를 보자.

function object(){
    var x = 1;
    return {
        func1 : function(){ console.log(++x)},
        func2 : function(){ console.log(--x)}
    };
};
var pointer = object();
pointer.func1();
pointer.func2();

반환되는 객체에 두 개의 함수가 정의되어 있는데, 두 함수가 모두 같은 변수 x를 참조한다. 그래서 각각의 함수를 호출할 때마다 x 값이 변화한다. 

 

다음으로 루프 안에서 클로저를 사용할 때 주의할 점이다.

function countingSec(x){
    for (var i = 1; i <= x; i++){
        setTimeout(function(){
            console.log(i);
        }, i + 1000)
    }
}

countingSec(3);

이 함수를 실행하면 1, 2, 3이라고 출력될 것 같다. 하지만 결과는 4만 연속 3번 출력된다. 왜냐하면 setTimeout에 들어가는 함수가 i를 참조하는데 이 함수가 실행되는 시점은 countingSec함수 실행이 종료된 이후이고, i 값은 이미 4가 된 상태이기 때문이다.

그렇다면 1, 2, 3으로 출력되게 하려면 어떻게 해야 할까?

function countingSec(x){
    for (var i = 1; i <= x; i++){
        (function(thisI){
            setTimeout(function(){
                console.log(thisI);
            }, thisI * 1000);
        }(i))
    }
}

countingSec(3);

즉시 실행 함수를 실행시켜 i값을 thisI에 복사해서 setTimeout 함수에 넘겨주면 원하는 결과를 얻을 수 있다.

이번에는 객체 리터럴 방식과 생성자 함수를 통한 객체 생성의 차이에 대해 알아 보겠다.

다음 예제를 보자.

var kim = {
    name : 'kim',
    age : 40,
    gender : 'man'
};
console.dir(kim);

function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender
}

var man = new Person('kim', 40, 'man');
console.dir(man);

var woman = new Person('kim', 35, 'woman');
console.dir(woman);

kim은 리터럴 방식으로 생성한 객체이고 man과 woman은 Person이라는 생성자 함수로 객체를 생성하였다.

 

콘솔창에 나오는 것을 보면 리터럴 방식으로 생성한 객체는 모든 객체의 조상인 Object객체와 연결되어 있다. 반면 생성자 함수로 생성한 객체는 자신을 만든 생성자 객체인 Person.prototype을 참조하고 있음을 알 수 있다.

참고로 자바스크립트 함수는 함수가 생성될 때 constructor 프로퍼티 하나만 있는 객체를 생성해서 함수의 prototype과 연결한다. Person 생성자 함수를 통해서 만든 객체의 constructor 프로퍼티를 보면 Person 객체가 매핑되어 있음을 알 수 있다.

 

 

자바스크립트에서 함수를 호출할 때 인자값과 arguments 객체, this 인자가 내부적으로 전달된다. 이번 시간에는 함수 호출 패턴에 따라 this가 어떤 객체에 바인딩 되는지, 이를 이용한 코딩 방법을 알아보자.

 

1. 객체의 메서드를 호출할 때 this 바인딩

객체의 메서드를 호출할 때, 메서드 내부에 사용된 this는 해당 메서드를 호출한 객체로 바인딩된다.

var kimMan = {
    name : 'kim',
    myNameIs : function(){
        console.log(this.name);
    }
};

var otherGuy = {
    name : 'lee'
};

otherGuy.myNameIs = kimMan.myNameIs;

kimMan.myNameIs(); //kim
otherGuy.myNameIs(); //lee

myNameIs 메서드는 자신의 name 프로퍼티에 있는 값을 출력하는 함수이다. 객체 매서드에서 호출하는 경우 this는 호출한 객체를 가리킨다고 했으므로 kimMan.myNameIs()에서 this는 kimMan객체가 되어 kim이 출력되고, otherGuy.myNameIs()에서 this는 otherGuy 객체가 되어 lee가 출력된다.

 

2. 함수를 호출할 때 this 바인딩

함수를 호출할 때 this는 전역 객체에 바인딩 된다. 브라우저 환경에서 자바스크립트를 실행할 경우 전역 객체는 window가 되고 Node.js와 같은 자바스크립트 런타임 환경에서의 전역 객체는 global 객체가 된다.

다음 예제를 보면 함수를 호출할 때 this는 전역 객체 window에 바인딩 됨을 알 수 있다.

var t = 'this is javascript';
console.log(window.t); // this is javascript

var ref = function(){
    console.log(this.t);// this is javascript
}

ref();

 

 

다음 예제를 보면 내부함수도 함수 호출이므로 내부함수를 호출할 때 this도 전역 객체에 바인딩 된다. 

var name = 'windowMan';

var objectMan = {
    name : 'objectMan',
    func1 : function(){
        console.log('what is your name : ' + this.name); 

        func2 = function(){
            console.log('what is your name : ' + this.name);
        };

        func2();
    }
};

objectMan.func1();

위 함수를 호출하면 func2를 호출할 때 objectMan이 아닌 windowMan이 출력된다. 내부함수 호출도 this가 전역 객체에 바인딩 되기 때문이다. 

func2 호출할 때도 objectMan이 출력되려면 어떻게 해야할까?

var name = 'windowMan';

var objectMan = {
    name : 'objectMan',
    func1 : function(){
        that = this;
        console.log('what is your name : ' + this.name);

        func2 = function(){
            console.log('what is your name : ' + that.name);
        };

        func2();
    }
};

objectMan.func1();

위와 같이 하거나 다음과 같이 코딩하면 원하는 값을 출력할 수 있다.

var name = 'windowMan';

var objectMan = {
    name : 'objectMan',
    func1 : function(){
        console.log('what is your name : ' + this.name);

        func2 = function(){
            console.log('what is your name : ' + this.name);
        };

        func2.apply(objectMan);
    }
};

objectMan.func1();

 

자바스크립트에서 함수는 일반 객체처럼 값으로 취급된다. 

값으로 취급된다는 것은 어떠한 변수에 값을 담을 수 있는 것처럼 함수를 변수에 담고 함수가 값이 된다는 의미이다.

 

자바스크립트에서 다음과 같이 함수를 생성해보자.

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

var sum = function(x, y){
   return x + y;
};

위에 정의된 함수도 sum이라는 자바스크립트 변수에 담겨진 일종의 값이 되는 것이다. 자바스크립트의 함수는 변수에 담을 수 있다는 속성을 가지고 있다는 것인데 이는 다른 언어의 함수와 다른 점이다.

참고로 첫 번째 방법을 함수 선언문 방식이라 하고 두 번째 방법을 함수 표현식이라고 한다.

 

함수 표현식은 보통 함수 이름을 넣지 않는데 함수 이름을 넣는 경우를 보자.

var sum = function plusReturn(x, y){
   return x + y;
}

console.log(sum(1, 2)); // 3
console.log(plusReturn(1, 2));// 에러

함수 참조값을 가지는 sum으로 호출 했을 경우는 제대로 실행되지만 함수 이름으로 호출했을 때는 오류가 발생한다. 함수 표현식에서 사용된 함수이름은 외부에서 접근할 수 없기 때문이다. 

 

외부에서 접근할 수 없기 때문에 보통 함수 표현식에 함수 이름은 넣지 않는다. 그러나 함수 이름이 필요할 때가 있다. 바로 재귀함수다.

var refVal = function factorial(a){
   if(a <= 1){
      return 1;
   }
   return a * factorial(a-1);
};

console.log(refVal(3)); //6

다음 챕터에서는 함수 호출에 대해 자세히 알아보도록 하겠다.

+ Recent posts