개발계발/Javascript

JavaScript 클로저

냥냥친구 2019. 7. 21. 16:33

해당 포스팅은 <인사이드 자바스크립트> 책 내용을 공부하며 정리한 것입니다.

 

<순서>

1. 클로저 이해

2. 클로저 활용

3. 클로저 주의사항

 

1. 클로저 이해

클로저는 함수 안에 있는 함수이다. 함수 안에 있어서 외부 함수, 내부 함수(클로저)라고 구분해서 부른다. 클로저의 특징은 외부 함수의 변수를 참조한다는 점이다. 예제를 한 번 살펴보자.

function outerFunc(){
	var x = 10;
    var innerFunc = function() {console.log(x);}
    return innerFunc;
    
var inner = outerFunc();
inner()
//result
//10

예제에서 innerFunc() 함수가 클로저이다. inner 변수 객체를 생성한 뒤, inner()를 실행하면 outerFunc()이 끝나고 나서 innerFunc()을 실행시킨다. innerFunc()은 외부 함수인 outerFunc의 변수 x를 참조하여 10을 출력했다. outerFunc() 함수가 끝났음에도 변수 x를 참조하여 10을 출력한 것이다. 어떻게 가능할까?

이 원리를 스코프 체인으로 이해해 보면, innerFunc()의 [[scope]]가 outerFunc의 변수객체와 전역 객체를 가지기 때문이다.

다시 말해, outerFunc 실행 컨텍스트는 사라졌지만 outerFunc 변수 객체는 innerFunc()의 [[scope]]에 계속 남아있으므로 x를 출력할 수 있는 것이다. 그래서 클로저를 이미 생명 주기가 끝난 외부함수의 변수를 참조하는 함수라고 정의할 수 있다. 그리고 x와 같이 클로저로 참조되는 외부 변수를 자유 변수라고 한다.

 

2. 클로저 활용

클로저는 성능 저하나 메모리 부담의 원인이 되기도 한다. 그럼에도 불구하고 고급 기술에서는 클로저가 많이 쓰이기 때문에 제대로 이해하고 활용하는 것이 중요하다. 클로저의 활용 예제를 살펴보자.

 

2-1) 특정 함수에 사용자가 정의한 객체의 메서드 연결하기

먼저 예제 하나를 살펴보자.

function HelloFunc(func){
	this.greeting = "hello";
}

HelloFunc.prototype.call = function(func){
	func? func(this.greeting) : this.func(this.greeting);
}

var userFunc = function(greeting){
	console.log(greeting);
}

var objHello = new HelloFunc();
objHello.func = userFunc;
objHello.call();

//result
hello

예제의 실행 코드부터 보면 HelloFunc() 생성자 함수로 objHello 변수 객체를 생성했다.

objHello에 func 프로퍼티를 userFunc으로 할당했다.

objHello의 call함수를 실행시키면, HelloFunc에는 call함수가 없으므로 HelloFunc  prototype객체에서 찾는데

call 메서드가 있으므로 실행시킨다. call 함수 호출 시, 매개 변수가 없으므로 this.func(this.greeting)이 실행되며 func 프로퍼티는

userFunc이라고 정의했으므로 userFunc(this.greeting)이 실행되고 결과적으로 hello가 출력된다.

 

여기서 call함수가 받는 인자는 greeting하나뿐이다. 만약 call() 함수 변경 없이 인자를 더 넣어서 호출하려면 어떻게 해야 할까?

아래 예제를 보자.

function saySomething(obj, methodName, name){
	return (function(greeting){
    	return obj[methodName](greeting, name);
    });
}

function newObj(obj, name){
	obj.func = saySomething(this, "who", name);
    return obj;
}

newObj.prototype.who = function(greeting, name){
	console.log(greeting + " " + (name || "everyone") );
}

var obj1 = new newObj(objHello, "zzoon");
obj1.call();

이번에는 newObj() 생성자 함수로 obj1 변수 객체를 생성했다.

매개변수 값으로 objHello 객체와 "zzoon" 문자를 전달했다. 그러면 objHello 객체의 func 프로퍼티로 saySomething(this,"who",name);에서 반환되는 함수를 참조한다.

saySomething의 매개변수 값을 살펴보면,

obj = this로, newObject 객체

methodName = "who"

name = "zzoon" 이 된다.

그리고 function(greeting){} 함수를 반환하는 데 이것이 HelloFunc 객체의 func으로 참조된다.

결과적으로 bj1.call()로 실행되는 것은 newObj.prototype.who()가 된다.

 

3. 클로저 주의사항

 

3-1) 자유 변수 값은 변경 가능하다.

function outerFunc(argNum){
	var num = argNum;
    return function(x){
    	num += x;
        console.log('num: ' + num);
        }
 }
 
 var exam = outerFunc(40);
 exam(5);
 exam(-10);
     
//result
//45
//35

자유 변수 num의 값은 계속 변화하기 때문에 주의해야 한다.

 

3-2) 루프 안에서 사용할 때

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

countSeconds(3);
 

1, 2, 3을 1초 간격으로 출력하는 예제이지만 결과는 4가 연속 3번 1초 간격으로 출력된다.

왜냐하면 setTimeout 함수의 인자로 들어가는 함수는 자유 변수 i를 참조한다. 하지만 setTimeout함수가 실행되는 시점은 countSeconds() 함수의 실행이 종료된 이후이고, i 값은 이미 4가 된 상태이다 그러므로 모두 4를 출력하게 된다.

 

원하는 결과를 얻고 싶으면 아래와 같이 하면 된다.

function countSeconds(howMany){
	for (var i = 1; i <= howMany; i++) {
    	(function (currentI) { 
        	setTimeout(function() {
            	console.log(currentI);
            }, currentI * 1000);
        }(i));
    }
};

countSeconds(3);

구현 방법은 (function ( currentI){})라는 함수를 정의함과 동시에 바로 실행되는 즉시 실행 함수(immediate function)로 setTimeout을 감싸는 것이다. 루프 i 값을 currentI에 복사해 사용하면 원하는 결과를 얻을 수 있다.

 

끝 ^_^