UI Laboratory

UI 개발을 위한 레퍼런스

INDEX

1장. 자바스크립트에 익숙해지기


[ITEM1] 어떤 자바스크립트를 사용하는지 알아야 한다.

ES5에는 다른 버전에 대한 대안으로 스트릭트 모드가 새로 추가되었다. 이 기능은 옵션을 통해 적용할 수 있는데, 특정 버전의 자바스크립트에서는 오류를 일으키기 쉽거나 문제를 일으킬만한 기능들을 사용할 수 없게 만들 수 있다. 또한 문법의 하위 호환성이 유지되어, 스트릭트 모드 확인을 구현하지 않은 환경에서도 엄격한(스트릭트 모드의) 코드를 실행할 수 있게 했다.

스트릭트 모드는 프로그램의 맨 처음 부분에 다음과 같은 특별한 고정 문자열을 추가하면 활성화된다.

			"use strict";
		

유사한 방법으로 함수의 본문 처음에 다음과 같이 명령어를 추가하여 함수내에서 스트릭트 모드를 활성화 시킬 수 있다.

			function f(x){
				"use strict";
				//....
			}
		

문자 리터럴을 명령어로 사용하는 문법은 하위 호환성에 이점을 가진다. ES3 엔진은 이 명령어를 문자열로 평가하고 그 값을 곧바로 제거한다. 하지만 오래된 엔진은 스트릭트 모드에 대한 어떠한 확인도 하지 않아 코드를 정상적으로 실행하지만 ES5 환경에서는 다음과 같이 제대로 실행되지 않는 코드를 작성할 가능서이 높다.

			function f(x){
				"use strict";
				var agments = [];	// 오류 : arguments를 재정의함
			}
		

스트릭트 모드에서는 arguments 변수의 재정의를 허용하지 않는다. 하지만 스트릭트 모드 확인을 구현하지 않는 환경(구형 자바스크립트엔진)에서는 이 코드를 허용할 것이다. 따라서 스트릭트 모드로 작성한 코드는 항상 ES5를 완전히 지원하는 환경에서 테스트해야 한다.

스트릭트 모드를 사용할 때 조심해야 할 함정 중 하나는, "use strict" 명령어가 스크립트나 함수의 상단에 선언되었을 때만 인식되고, 이 때문에 스크립트 병합에 민감해진다는 점이다. 여러 개의 분리된 스크립트 파일을 병합할 때 스트릭트 모드로 작성된 파일을 일반 모드로 작성한 파일 앞에 두고 병합한다면 합쳐진 전체 파일은 스트릭트 모드로 동작할 것이다. 반대로 일반모드로 작성된 파일부터 합쳤다면 병합된 파일은 모두 스트릭트 모두로 동작하지 않을 것이다.

다양한 코드들과도 잘 병합될 수 있는 견고한 코드를 작성하고 싶다면 몇가지 대안을 사용해야 한다.

스트릭트 모드와 일반 모드의 파일을 절대 병합하지 마라.

제한은 있지만 최선의 방법은, 스트립트 모드로 작성된 파일과 일반적인 파일, 즉 두개의 분리된 파일로 배포하는 것이다.

즉시 실행되는 함수 표현식을 사용해 파일들의 본문을 감싸라.

각 파일의 내용을 함수로 감싸서 독립적으로 해석되게 하는 것이다.

			//스트릭트 모드 명령어 있음
			(function(){
				//file1.js
				"use strict";
				function f(x){
					...
				}
			})();

			//스트릭트 모드 명령어 없음
			(function(){
				//file2.js
				function f(x){
					...
				}
			})();
		
		

각 파일의 내용이 별도의 스코프에 위치하기 때문에, 스트릭트 모드 명령어는 해당 파일의 내용에만 영향을 미친다. 이런 방식은 흔히 사용하는 모듈 시스템과도 비슷하데, 각 모듈의 내용을 자동으로 개별 함수에 위치시킴으로써 여러 파일과의 의존성을 관리한다. 파일들이 지역 스코프에 위치하기 때문에 각 파일이 스트릭트 모드의 사용 여부를 개별적으로 결정할 수 있다.

어떤 모드에 있건 동일하게 동작하도록 코드를 작성하라.

최대의 호환성을 가지도록 코드를 구성하는 가장 간단한 방법은 스트릭트 모드로 작성하되, 스트릭트 모드가 지역적으로 활성화될 수 있게 명시적으로 전체 코드 내용을 함수로 감싸는 것이다. 직접 함수 표현식을 작성해 명시적으로 스트릭트 모드를 선택적으로 적용한다.

			(function(){
				"use strict";
				function f(x){
					...
				}
				...
			})();
		

이 코드는 스트릭트 모드의 코드에 병합되든 일반 모드의 코드에 병합되든, 스트릭트 모드로 처리된다는 것을 알 수 있다.

2장. 변수 스코프


[ITEM8] 전역 객체의 사용을 최소화하라.

전역 변수를 정의하는 것은 모든 사람과 공유하는 공통의 네임스페이스를 더럽히고 뜻하지 않게 이름이 충돌할 만한 가능성을 만든다. 전역 변수는 프로그램의 구분된 요소들 간에 불필요한 결합을 초래하므로 모듈성에 반대되는 성향을 가진다.

라이브러리나 컴포넌트는 프로그램의 다른 부분에서 사용할 수 있도록 전역 변수 이름을 정의해야 한다. 그런 경우가 아니라면, 가능한 한 모든 변수를 지역 변수로 유지하는게 최선이다.

자바스크립트의 전역 네임스페이스는 전역 객체로도 노출되어 있다. 이 전역 객체는 프로그램의 최상단에서 this 키워드로 접근할 수 있다. 웹브라우저에서는 전역 객체가 전역 window 변수에도 바인딩되어 있다. 전역 변수를 추가하거나 수정하면 자동으로 전역 객체가 갱신된다.

			this.foo;		// undefined
			foo = "global foo";
			this.foo;		// "global foo"
		

유사하게, 전역 객체를 갱신하면 자동으로 전역 네임스페이스가 갱신된다.

			var foo = "global foo";
			this.foo = "changed";
			foo;		/"changed"
		

전역변수는 전역 스코프에서 var로 정의하거나, 전역 객체(this)에 추가하면 된다. 두가지 방법 모두 동작하지만, var 선언문이 더 명백하게 프로그램의 스코프에 영향을 준다는 이점과 함께 어떤 전역 변수를 선언했는지 사용자가 이해하기 쉽다.

전역 객체의 사용을 제한하는게 최선이지만, 한가지 특별한 필수적인 사용법이 있다. 전역 객체는 전역 환경을 동적으로 반영하기 때문에, 해당 플랫폼에서 사용 가능한 기능을 탐지하기 위해서 실행 환경에 대한 질의를 하는데 사용할 수 있다. 해당 객체의 존재 여부를 전역 객체에서 확인하고 대체제로 사용할 수 있는 구현체를 제공할 수 있다.

			if (!this.JSON){
				this.JSON = {
					parse: ....,
					stringify: ...
				};
			}
		
기억할 점
  • 전역 변수를 선언하지 마라.
  • 가능하면 변수를 지역적으로 선언하라.
  • 전역 객체에 프로퍼티를 추가하지 마라.
  • 플랫폼의 기능 탐지를 위해 전역 객체를 사용하라.

[ITEM11] 클로저에 익숙해져라.

클로저를 이해하기 위해서는 단지 세가지 기본적인 사실만 이해하면 된다.

첫번째로 자바스크립트는 현재 함수 외부에서 선언된 변수를 참조할 수 있다.

			function makeSandwich(){
				var magicIngredient = "peanut butter";
				function make(filling){
					return magicIngredient + " and " + filling;
				}
				return make("jelly");
			}

			makeSandwich();	//"peanut butter and jelly"
		

내부의 make 함수가 이 함수 바깥에서 선언된, 다시 말하면 makeSandwich 함수에서 선언된 magicIngredient 변수를 참조한다는 사실을 주목하라.

두번째로 함수는 외부 함수가 무언가를 리턴한 이후에도 이 외부 함수에서 선언된 변수를 참조할 수 있다.

			function sandwichMaker(){
				var magicIngredient = "peanut butter";
				function make(filling){
					return magicIngredient + " and " + filling;
				}
				return make;
			}

			var f = sandwichMaker();	
			f("jelly");	// "peanut butter and jelly"
			f("bananas");	// "peanut butter and bananas"
			f("marshmallows");	// "peanut butter and marshmallows"
		

이 예제는 외부 함수 안에서 make("jelly")를 곧바로 호출하는 대신에 sandwichMaker가 make 함수 자체를 리턴하고 있다. f의 값은 내부의 make 함수이고, f를 호출하는 것은 make를 효과적으로 호출하는 셈이다. 하지만 sandwichMaker가 이미 리턴되었더라도, make 함수는 magicIngredient의 값을 기억하고 있다.

자바스크립트 함수는 해당 스코프에서 선언되어 참조할 수 있는 어떤 변수더라도 내부적으로 보관한다. 함수 자신이 포함하는 스코프의 변수들을 추적하는 함수를 클로저라고 한다. make 함수는 magicIngredient와 filling 두개의 외부 변수를 참조하는 클로저다. 언제든 make 함수가 호출되면, 이 두변수가 클로저에 저장되어 있기 때문에 참조할 수 있다.

함수는 파라미터와 외부 함수의 변수뿐만 아니라 해당 스코프 내에 포함된 어떤 변수라도 참조할 수 있다.

			function sandwichMaker(magicIngredient){
				function make(filling){
					return magicIngredient + " and " + filling;
				}
				return make;
			}

			var hanAnd = sandwichMaker("ham");	
			hanAnd("cheese");	// "ham and cheese"
			hanAnd("mustard");	// "ham and mustard"

			var turkeyAnd = sandwichMaker("turkey");	
			turkeyAnd("Swiss");	// "turkey and Swiss"
			turkeyAnd("Provaolone");	// "turkey and Provaolone"
		

이 예제는 hanAnd와 turkeyAnd 두개의 다른 함수를 생성한다. 두 함수가 동일한 make 정의에 의해 만들어짐에도 불구하고, 이들은 두 개의 서로 다른 객체다.

자바스크립트는 클로저를 생성하기 위한 더 편리하고 일반적인 문법을 제공하는데, 함수 표현식이 바로 그것이다.

			function sandwichMaker(magicIngredient){
				return function(filling){
					return magicIngredient + " and " + filling;
				}
			}
		

이 함수표현식은 익명인데 새로운 함수값을 만들기 위해 평가하는 용도로만 만들어졌기 때문에 이름이 필요가 없다.

세번째로 클로저가 외부 변수의 값을 변경할 수 있다는 점이다.

클로저는 실제로 외부 변수의 값을 복사하지 않고 참조를 저장한다.

			function box(){
				var val = undefined;
				return {
					set : function(newVal) {val = newVal;},
					get : function() { return val;},
					type : function() { return typeof val;}
				};
			}
			var b = box();
			b.type();	// "undefined"
			b.set(98.6);
			b.get();	// 98.6
			b.type();	// "number"
		

이 예제는 세개의 클로저, 즉 set, get, type 프로퍼티들을 포함하는 객체를 생성한다. 각 클로저는 val 변수를 공유하여 접근한다. set 클로저로 val의 값을 변경하고, 그 이후에 get과 type을 호출해 변경에 대한 결과를 확인한다.

기억할 점
  • 함수는 외부 스코프에 선언된 변수를 참조할 수 있다.
  • 클로저는 자신을 생성한 함수보다 더 오래 지속된다.
  • 클로저는 내부적으로 외부 변수에 대한 참조를 저장하고, 저장된 변수를 읽고 갱신할 수 있다.

[ITEM12] 변수 호이스팅에 대해 이해하라.

자바스크립트는 블록 단위의 스코프를 지원하지 않는다. 변수 정의는 이를 포함한 가장 가까운 선언문이나 블록으로 스코프가 정해지는 것이 아니라, 자신을 포함하는 함수에 의해 지정된다.

자바스크립트 변수 선언의 동작을 선언과 할당의 두부분으로 나누어서 이해하면 좋다. 자바스크립트는 암묵적으로 둘러싼 함수의 맨 윗부분으로 선언을 끌어올리고(호이스팅) 할당 부분은 그 자리에 그대로 둔다. 달리 말해서 변수는 전체 함수의 스코프 안에 있지만, 실제로는 var 선언문이 나타난 곳에서만 할당되는 것이다.

호이스팅은 변수를 재선언할 때 혼란을 초래할 수 있다. 동일한 함수 내에서 같은 변수를 여러번 정의하는 것은 허용되지 않는다. 이는 여러개의 반복문을 작성할 때 자주 나타난다.

			function trimSections(headr, body, footer){
				for(var i=0, n=header.length; i < n; i++){
					header[i] = header[i].trim();
				}
				for(var i=0, n=body.length; i < n; i++){
					body[i] = body[i].trim();
				}
				for(var i=0, n=footer.length; i < n; i++){
					footer[i] = footer[i].trim();
				}
			}
		

trimSections 함수는 여섯개의 지역변수(i 3개, n 3개)를 선언하지만 호이스팅으로 인해 오직 두 개만이 선언되었다. 달리 말해서 호이스팅된 이후에는, trimSections 함수는 다음과 같이 다시 쓰여진 셈이다.

			function trimSections(headr, body, footer){
				var i, n;

				for(i=0, n=header.length; i < n; i++){
					header[i] = header[i].trim();
				}
				for(i=0, n=body.length; i < n; i++){
					body[i] = body[i].trim();
				}
				for(i=0, n=footer.length; i < n; i++){
					footer[i] = footer[i].trim();
				}
			}
		

재선언은 별도의 변수를 나타내기 때문에, 효과적으로 모호함을 줄이기 위해 변수들을 직접 호이스팅하여 함수의 맨 윗 부분에 모든 var 선언문을 위치시키는 방식을 선호한다. 이런 스타일이 마음에 들지 않더라도, 자바스크립트의 스코프 규칙을 이해하는 것은 코드를 읽고 쓰는데 중요하다.

자바스크립트에서 블록 스코프가 지원되는 예외 상황중 하나는 바로 exception이다. try...catch는 exception을 잡아 변수로 바인딩하고, 해당 변수는 catch 블록 안에서만 스코프가 적용된다.

			function test(){
				var x = "var", result = [];
				result.push(x);

				try{
					throw "exception";
				}catch(x){
					x = "catch";
				}

				result.push(x);
				return result;
			}

			test();		// ["var", "var"]
		
기억할 점
  • 블록 내에서의 변수 선언은 암묵적으로 그 변수를 포함하는 함수의 맨 윗부분으로 호이스팅된다.
  • 변수의 재선언은 하나의 변수처럼 처리된다.
  • 혼란을 막기 위해 지역 변수 선언을 직접 호이스팅하는 것을 고려하라.

[ITEM13] 지역 변수 스코프를 만들기 위해 즉시 실행 함수 표현식을 사용하라

다음 프로그램은 어떤 계산을 할까?

			function wrapElements(a){
				var result = [], i, n;
				for(i=0, n=a.length; i < n; i++){
					result[i] = function(){
						return a[i];
						//console.log(i);
					};
				}
				return result;
			}

			var wrapped = wrapElements([10, 20, 30, 40, 50]);
			var f = wrapped[0];
			f();
		

아마도 10 이라는 값을 계산할 의도로 코드를 작성했겠지만, 실제로는 undefined 값이 만들어진다.

이 예제를 제대로 동작하도록 하기 위해서는 바인딩과 할당의 차이점을 이해해야 한다. 런타임시 스코프에 진입하면 해당 스코프에 있는 변수들은 바인딩하기 위해 메모리에 '슬롯'을 할당한다. wrapElements 함수는 세 지역 변수 result, i, n을 바인딩한다. 따라서 이 함수가 호출되면 wrapElements 함수는 이 세 변수들을 위한 슬롯을 할당한다. 반복문을 순회할 때마다, 반복문의 본문은 감싸는 함수를 위한 클로저를 할당한다. 이 프로그램의 버그는 감싸진 함수가 생성되는 시점에 그 함수가 i의 값을 명백히 저장하고 있다고 기대하기 때문에 발생한다. 하지만 사실은 i로의 참조를 포함할 뿐이다. i의 값이 매번 함수가 생성되고 난 뒤 변하기 때문에, 내부의 함수는 결국 i의 마지막 값을 바라보게 된다. 이게 바로 클로저의 키 포인트다. 클로저는 외부 변수의 값이 아니라 참조를 저장한다.

따라서 wrapElements에 의해 생성된 모든 클로저들은 반복문 이전에 i를 위해 생성된 하나의 공유 슬롯을 참조한다. 반복문을 순회할 때마다 i값은 배열의 마지막에 도달할 때까지 증가하고, 클로저 i를 실제로 호출할 때에는, 배열의 다섯째 인덱스를 찾게 되어 undefined를 리턴한다.

var 선언을 for 반복문의 머리 부분에 두더라도 wrapElements는 완전히 동일하게 동작한다는 점을 주목하라.

			function wrapElements(a){
				var result = [];
				for(var i=0, n=a.length; i < n; i++){
					result[i] = function(){
						return a[i];
					};
				}
				return result;
			}

			var wrapped = wrapElements([10, 20, 30, 40, 50]);
			var f = wrapped[0];
			f();		// undefined
		

항상 변수 선언은 반복문의 맨 윗부분으로 호이스팅된다. 따라서 마찬가지로 변수 i를 위한 하나의 슬롯만 할당된다.

다음과 같이 감싸진 함수를 만들어 강제로 지역 스코프를 만들고 즉시 실행하는 방법으로 이 문제를 해결할 수 있다.

			function wrapElements(a){
				var result = [];
				for(var i=0, n=a.length; i < n; i++){
					(function(){
						var j = i;
						result[i] = function(){
							return a[j];
						};
					})();
				}
				return result;
			}

			var wrapped = wrapElements([10, 20, 30, 40, 50]);
			var f = wrapped[0];
			f();		
		

이 방법은, 즉시 실행 함수 표현식이라고 부르며, 자바스크립트의 블록 스코프 지원을 위한 필수적인 차선책이다.

대안으로 사용할 수 있는 변형으로는 다음과 같이 지역 변수를 즉시 실행 함수 표현식의 파라미터로 바인딩하고 그 값을 인자로 전달하는 방법이 있다.

			function wrapElements(a){
				var result = [];
				for(var i=0, n=a.length; i < n; i++){
					(function(j){
						result[i] = function(){
							return a[j];
						};
					})(i);
				}
				return result;
			}
		

하지만 지역 스코프를 생성하기 위해 즉시 실행 함수 표현식을 사용할 때에는, 함수 안에 블록으로 감싸는 것이 블록에 어떤 이상한 변화를 만들기 때문에 조심해야 한다. 첫째로, 블록 안에서는 블록 바깥으로 뛰쳐나가기 위해 break나 continue 명령어를 사용할 수 없다. 두번째로, 블록에서 this나 특별한 arguments 변수를 참조하며, 즉시 실행 함수 표현식은 이를 다르게 해석한다.

기억할 점
  • 바인딩과 할당의 차이점을 이해하라.
  • 클로저는 외부 변수의 값이 아닌 참조를 저장한다.
  • 지역 스코프를 만들기 위해 즉시 실행함수 표현식을 사용하라.

[ITEM15] 블록-지역 함수 선언문의 스코프에 주의하라

다음과 같이 다른 함수의 맨 윗부분에 함수 선언문을 넣는 방법은 관례적이고 완벽히 정확하다.

			function f(){return "global";}
			function test(x){
				function f(){return "local";}
				var result = [];
				if(x){
					result.push(f());
				}
				result.push(f());
				return result;
			}
			test(true);			//["local", "local"]
			test(false);		//["local"]
		

하지만 f를 지역 블록 안으로 이동시키면 완전히 다른 이야기가 된다.

			function f(){return "global";}
			function test(x){
				var result = [];
				if(x){
					function f(){return "local";}
					result.push(f());
				}
				result.push(f());
				return result;
			}
			test(true);			// ?
			test(false);		// ?
		

내부의 f가 if 블록에 지역적으로 나타나기 때문에, 첫번째 test 함수를 호출할 때는 배열 ["local", "global"]을 반환하고, 두번째 호출에는 ["global"]을 반환하리라 예상할 것이다. 하지만 몇몇 자바스크립트 실행환경(최신 크롬브라우저)은 ["local", "local"]과 ["local"]을 반환한다. 하지만 모든 실행 환경이 그런 것은 아니다! 다른 실행환경에서는 런타임시 조건적으로 어떤 포함된 블록이 실행되는지에 따라서 내부 f를 바인딩한다.(이는 코드를 이해하기 어렵게 만들 뿐 아니라, 성능을 떨어뜨린다.)

반면에, 어떤 실행 환경에서도 올바르게 동작하는 함수를 작성하기 위한 최선의 방법은 함수 선언문을 지역 블록이나 하위 명령에 절대 두지 않는 것이다. 감싸진 함수 선언문을 작성하고 싶다면, 부모 함수의 가장 바깥에 두는 것이다. 만약 함수들을 조건에 따라 선택할 필요가 있다면, 최선의 방법은 var 선언문과 함수 표현식을 사용하는 것이다.

			function f(){return "global";}
			function test(x){
				var g = f, result = [];
				if(x){
					g = function(){return "local";}
					result.push(g());
				}
				result.push(g());
				return result;
			}
			test(true);    //["local", "local"]
			test(false);   //["global"]
		

내부 변수(여기서는 변수 g)는 무조건 지역 변수로 바인딩되고 할당만 조건적으로 실행된다. 따라서 결과 값은 명백하며 어떤 환경에서든 동일하다.

3장. 함수 사용하기


[ITEM18] 함수, 메서드, 생성자 호출의 차이를 이해하라.

함수와 메서드, 생성자는 단지 하나의 생성자 function을 사용하는 세 가지 서로 다른 패턴일 뿐이다.

			function hello(username){
				return "hello, " + username;
			}
			hello("Keyser Soze");		//"hello, Keyser Soze"
		

함수호출은 보이는 그대로 동작할 뿐이다. hello 함수를 호출하고 name 파라미터를 주어진 인자에 바인딩한다.

자바스크립트에서 메서드는 함수로 동작하는 객체의 프로퍼티일 뿐이다.

			var obj = {
				hello : function(){
					return "hello, " + this.username;
				},
				username : "Hans Gruber"
			};
			obj.hello();		// "hello, Hans Gruber"
		

hello 함수가 obj의 프로퍼티에 접근하기 위해 this를 참조한다. 하지만 다음과 같이 또 다른 객체에서 같은 함수로의 참조를 복사하면 다른 결과를 얻게 된다.

			var obj = {
				hello : function(){
					return "hello, " + this.username;
				},
				username : "Hans Gruber"
			};
			var obj2 = {
				hello : obj.hello,
				username : "Boo Radley"
			};
			obj2.hello();	// "hello, Boo Radley"
		

메서드 호출시 실제로는 호출 표현식 스스로가 수신자 객체 this로의 바인딩을 결정한다. obj.hello() 표현식은 obj의 hello 프로퍼티를 찾고, 수신자 객체 obj로 호출한다. obj2.hello() 표현식은 obj2의 hello 프로퍼티를 찾고 이는 obj.hello와 동일한 함수지만 수신자 객체는 obj2가 된다.

일반적인 함수도 this를 참조하지 못할 이유가 없다. 하지만 메서드가 아닌 함수를 호출하면 전역 객체가 그 수신자가 되고, 이 경우 username 프로퍼티가 없어서 undefined 값을 만들어 내므로 오히려 해가 된다.

			function hello(){
				return "hello, " + this.username;
			}
			hello();		//"hello, undefined"
		

셋째로 함수를 생성자로써 사용할 수 있다.

			function User(name, password){
				this.name = name;
				this.password = password;
			}
		

다음과 같이 new 연산자로 User를 실행하면 생성자처럼 처리된다.

			var u = new User("sfalken", "123456");
			u.name;		//"sfalken"
		

함수나 메서드 호출과 다르게, 생성자 호출은 this의 값으로 새로운 객체를 전달하고, 암묵적으로 이 객체를 결과로 반환한다. 생성자 함수의 주요한 역할은 객체를 초기화하는 것이다.

[ITEM19] 고차 함수에 익숙해져라.

고차 함수는 다른 함수를 인자로 받거나 그 결과로 함수를 반환하는 함수다. 인자로 받는 함수(흔히 콜백함수)는 특히 강력하고 표현력 높으며 자바스크립트에서 자주 쓰이는 코딩 관례다.

배열의 표준 정렬 메서드 sort를 생각해 보자. 모든 배열에서 동작할 수 있도록, sort 메서드는 호출자에 의존하여 배열안의 두 요소를 어떻게 비교할지 결정한다.

			function compareNumbers(x, y){
				if(x < y){
					return -1;
				}
				if(x > y){
					return 1;
				}
				return 0;
			}
			[3,1,4,1,5,9].sort(compareNumbers);		// [1, 1, 3, 4, 5, 9]
		

이 예제는 익명함수를 통해 더 간단하게 만들 수 있다.

			[3,1,4,1,5,9].sort(function(x, y){
				if(x < y){
					return -1;
				}
				if(x > y){
					return 1;
				}
				return 0;
			});		// [1, 1, 3, 4, 5, 9]
		

고차 함수의 사용법을 익혀 두면 지루한 상용문을 제거하고 코드를 더 간단하게 만들 수 있다.

문자열로 된 배열을 변환하는 간단한 동작에 대해 생각해 보자. 일반적인 반복문으로 작성한 코드는 다음과 같다.

			var names = ["Fred", "Wilma", "Pebbles"];
			var upper = [];
			for(var i=0, n = names.length; i < n; i++){
				upper[i] = names[i].toUpperCase();
			}
			upper;	// ["FRED", "WILMA", "PEBBLES"]
		

배열의 간단한 map 메서드를 이용하면, 반복문의 세부사항을 완전히 제거할 수 있고, 각 요소의 변환을 지역 함수 내에 구현하기만 하면 된다.

			var names = ["Fred", "Wilma", "Pebbles"];
			var upper = names.map(function(name){
				return name.toUpperCase();
			});
			upper;	// ["FRED", "WILMA", "PEBBLES"]
		

비슷하거나 중복된 코드를 자주 보게 된다면 이는 숨길 수 없는 고차 함수 추상의 신호다.

예를 들어, 다음은 알파벳 문자로 문자열을 만드는 프로그램의 일부이다.

			var aIndex = "a".charCodeAt(0);
			var alphabet = "";
			for(var i=0; i < 26; i++){
				alphabet += String.fromCharCode(aIndex + i);
			}
			alphabet;	// "abcdefghijklmnopqrstuvwxyz"
		

다음은 숫자값을 포함하는 문자열을 생성한다.

			var digits = "";
			for(var i=0; i < 10; i++){
				digits += i;
			}
			digits;	// "0123456789"
		

다음은 임의의 글자로 문자열을 만든다.

			var random = "";
			for(var i=0; i < 8; i++){
				random += String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
			}
			random;	// "tckarlls"
		

각 예제는 서로 다른 문자열을 생성하지만, 모두 공통의 로직을 공유한다. 공통 부분을 추출하고 하나의 유틸리티 함수로 옮기면 다음과 같은 코드를 만들 수 있다.

			function buildString(n, callback){
				var result = "";
				for(var i=0; i < n; i++){
					result += callback(i);
				}
				return result;
			}
		

buildString 함수에서 반복문을 순회하는 횟수는 변수 n이 되고, 문자열을 생성하는 부분은 callback 함수를 호출하게 되었다. 이제 buildString을 이용하면 이전의 세 예제를 다음과 같이 간단하게 구현할 수 있다.

			var alphabet = buildString(26, function(i){
				return String.fromCharCode(aIndex + i);
			});
			alphabet;	// "abcdefghijklmnopqrstuvwxyz"
		
			var digits = buildString(10, function(i){
				return i;
			});
			digits;	// "0123456789"
		
			var random = buildString(8, function(i){
				return String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
			});
			random;	// "eimemube"
		

[ITEM20] 지정된 수신자 객체로 함수를 호출하기 위해 call 메서드를 사용하라.

보통 함수나 메서드의 수신자 객체(즉, 특수 키워드 this에 바인딩되는 값)는 이를 호출하는 호출자의 문법에 의해 결정된다. 하지만 간혹 수신자 객체를 특정 객체로 지정해 함수를 호출할 필요가 있을 수도 있다. 물론 다음과 같이 그 메서드를 해당 객체에 새로운 프로퍼티로써 추가활 수도 있다.

			obj.temporay = f;
			var result = obj.temporary(arg1, arg2, arg3);
			delete obj.temporary;
		

하지만 이런 접근 방법은 좋지 않으며 위험할 수도 있다. 다행히도 함수는 call이라는 내장 메서드를 제공하는데, 이를 통해 수신자 객체를 지정할 수 있다.

			f.call(obj, arg1, arg2, arg3);
		

첫번째 인자로 명시적인 수신자 객체를 전달하는 것을 제외하고는 함수를 직접 호출하는 것과 비슷하게 동작한다.

call 메서드는 고차 함수를 정의하는데 유용하다. 고차 함수를 위한 일반적인 코딩 관례 중 함수를 호출하기 위한 수신자 객체를 부가적인 인자로 받는 방법이 있다. 예를 들어, 키-값 바인딩의 테이블을 표현하는 객체는 다음과 같은 forEach 메서드를 제공할 수 있다.

			var table = {
				entries : [ ],
				addEntry : function(key, value){
					this.entries.push({key : key, value : value});
				},
				forEach : function(f, thisArg){
					var entries = this.entries;
					for(var i=0, n=entries.length; i < n; i++){
						var entry = entries[i];
						f.call(thisArg, entry.key, entry,value, i);
					}
				}
			};
		

이 코드는 객체를 사용하는 사람이 table.forEach의 콜백 함수 f로 메서드를 사용할 수 있도록, 메서드를 위한 수신자 객체를 받을 수 있게 해 준다. 이를 이용해, 다음 예제와 같이 한 테이블의 내용을 다른 테이블로 간편하게 복사할 수 있다.

			table.forEach(table2.addEntry, table2);
		

4장. 객체와 프로토타입


[ITEM33] 생성자가 new와 관계 없이 동작하게 만들어라

User 함수와 같은 생성자를 만들 때, 호출자는 반드시 new 연산자를 통해 호출해야 함을 기억해야만 한다.

			function User(name, passwordHash){
				this.name = name;
				this.passwordHash = passwordHash;
			}
		

호출자가 new 키워드를 깜빡한다면, 함수의 수신자 객체는 전역객체가 된다.

			var u = User("baravelli", "d8b74658f789078cd990");
			u;		// undefined
			this.name;		// "baravelli"
			this.passwordHash;	// "d8b74658f789078cd990"
		

함수가 불필요하게 undefined를 반환할 뿐만 아니라, 전역 변수 name과 passwordHash를 생성한다.

User 함수가 ES5 스트릭트 코드로 정의되었다면, 수신자 객체는 디폴트로 undefined가 되어 이 경우, 잘못된 호출로 인해 즉시 오류가 발생하여 버그를 빨리 발견하고 고칠 수 있다.

견고한 접근 방법은 어떻게 호출되더라도 생성자처럼 동자하는 함수를 제공하는 것이다. 이를 구현하는 쉬운 방법은 수신자 객체의 값이 User의 적절한 인스턴스인지 확인하는 것이다.

			function User(name, passwordHash){
				if (!(this instanceof User)){
					return new User(name, passwordHash);
				}
				this.name = name;
				this.passwordHash = passwordHash;
			}
		

이 방법을 사용하면, 함수로 호출되든지 생성자로 호출되든지 상관없이 User를 호출한 결과는 User.prototype을 상속한 객체가 된다.

			var x = User("baravelli", "d8b74658f789078cd990");
			var y = new User("baravelli", "d8b74658f789078cd990");
			x instanceof User;		//true
			y instanceof User;		//true