개요
- 자바스크립트의 함수는 다음과 같은 특징을 가지는 객체이다.
- 런타임, 즉 프로그램 실행 중에 동적으로 생성할 수 있다.
- 변수에 할당할 수 있고 다른 변수에 참조를 복사할 수 있으며, 확장 가능하고 몇몇 특별한 경우를 제외하면 삭제할 수 있다.
- 다른 함수의 인자로 전달할 수 있고, 다른 함수의 반환 값이 될 수 있다.
- 자기 자신의 프로퍼티와 메서드를 가질 수 있다.
- 함수가 객체라는 사실은 다음과 같은 new Function() 생성자의 동작을 보면 명백해진다.
// 안티패턴
// 데모의 목적으로만 사용한다
var add = new Function('a, b', 'return a + b');
add(l, 2); // 3을 반환한다
- 자바스크립트의 함수는 유효 범위를 제공한다.
- 자바스크립트에서는 중괄호 지역 유효범위가 없다.
- 블록이 유효범위를 만들지 않으며, 함수 유효범위가 있을 뿐이다.
- if 조건문이나 for문, while 문 내에서 var로 정의해도 if나 for문의 지역변수가 되지 않는다.
- 해당 블럭을 감싸는 함수가 있을 때만 지역변수가 된다.
- 전역 변수를 최소화하는 것이 좋기 때문에 변수의 유효범위를 관리하기 위해서 함수는 매우 중요하다.
- 자바스크립트에서는 중괄호 지역 유효범위가 없다.
용어 정리
기명 함수 표현식(named function expression)
var add = function add(a, b) {
return a + b;
};
무명 함수 표현식(unnamed function expression)
- 함수 표현식(function expression), 익명 함수(anonymous function)라는 말로도 쓰인다.
// 함수 표현식 (또는 익명 함수)
var add = function (a, b) {
return a + b;
};
함수 선언문
- 함수 표현식의 결과를 변수에 할당하지 않을 경우이다. (콜백 패턴이라고 한다)
- 세미콜론이 붙는지 여부에 따라 문법적인 차이점이 있다.
- 선언문은 없어도 되지만 표현식은 있어야 함.
function foo() {
// 함수 본문
}
선언문 vs 표현식
// 함수 표현식을 callMe 함수의 인자로 전달한다.
callMe(functior) () {
// 이 함수는 무명 함수(익명 함수) 표현식이다.
});
// 기명함수표현식을 callMe 함수의 인자로 전달한다.
callMe(function me() {
// 이 함수는 "me”라는 기명 함수 표현식이다.
});
// 함수 표현식을 객체의 프로퍼티로 저장한다.
var myobject = {
say: function () {
// 이 함수는 함수 표현식이다.
}
};
- 함수 선언문은 전역 유효범위나 다른 함수의 본문 내부, 즉 프로그램 코드에서만 쓸 수 있다.
- 변수나 프로퍼티에 할당할 수 없고 함수 호출 시 인자로 함수를 넘길 때도 사용할 수 없다.
함수 선언문보다 표현식을 선호하는 이유는 표현식을 사용하면 함수가 다른 객체들과 마찬가지로 객체의 일종이며, 어떤 특별한 언어 구성요소가 아니라는 사실이 드러나기 때문이다.
함수 호이스팅
- 함수 선언문과 기명 함수 표현식은
호이스팅(hoisting) 동작
에서 차이점을 보인다. - 모든 변수는 함수 본문의 어느 부분에서 선언(declaration)되더라도 내부적으로 함수 맨 윗부분으로 끌어올려(hoist)진다.
- 함수 또한 결국 변수에 할당되는 객체이기 때문에 동일한 방식이 적용된다.
- 하지만
함수 선언문을 사용하면 변수 선언 뿐만아니라 함수 정의(definition) 자체도 호이스팅
되기 때문에 자칫 오류를 만들어내기 쉽다.
// 안티패턴 : 설명을 위해 사용한다.
// 전역 함수
function foo() {
alert('global foo');
}
function bar() {
alert('global bar');
}
function hoistMe() {
console.log(typeof foo); // "function"
console.log(typeof bar); // ⭐⭐⭐ "undefined" ⭐⭐⭐
foo(); // "local foo"
bar(); // TypeError: bar is not a function
// 함수 선언문:
// 변수 ‘foo’와 정의된 함수 모두 호이스팅된다.
function foo() {
alert('local foo');
}
// 함수 표현식:
// 변수 ‘bar'는 호이스팅 되지만 정의된 함수는 호이스팅되지 않는다.
var bar = function () {
alert('local bar’);
};
}
hoistMe();
- hostMe() 함수 내에서 foo와 bar를 정의하면 실제 변수를 정의한 위치와 상관없이 끌어올려져 전역 변수인 foo와 bar를 덮어쓴다.
- 그런데 지역변수 foo()는 나중에 정의되어도 상단으로 호이스팅되어 정상 동작하는 방면, bar()의 정의는 호이스팅 되지 않고 선언문만 호이스팅 된다.
콜백 패턴
- a라는 함수의 인자에 b가 전달되면 a는 어느 시점에 b를 실행할 것이다.
- 이때 b를 콜백 함수 또는 콜백이라고 부른다.
function a (callback) {
// ...
}
function b () {
// ...
}
a(b);
- 콜백 함수는 괄호를 붙이지 않고 전달한다.
- 괄호를 붙이면 함수가 바로 실행되기 때문이다.
콜백 예제
- 예제는 콜백 없이 시작하여 리팩터링을 한다.
- 복잡한 작업을 수행 후 그 결과로 대용량 데이터셋을 반환하는 범용 함수가 있다고 하자.
- DOM 트리를 탐색해 필요한 엘리먼트의 배열을 반환한다.
var findNodes = function () {
var i = 100000, // 긴 루프
nodes = [], // 결과를 저장할 배열
found; // 노드 탐색 결과
while (i) {
i -= 1;
// 이 부분에 복잡한 로직이 들어간다.
nodes.push(found);
}
return nodes;
};
- findNodes는 범용으로 쓸 수 있도록 실제 엘리먼트에는 어떤 작업도 하지 않고 단지 DOM 노드의 배열을 반환하기만 하도록 유지한다.
- 노드를 수정하는 로직은 다른 함수를 만든다. hide()
var hide = function (nodes) {
var i = 0, max = nodes.length;
for (; i < max; i += 1) {
nodes[i].style.display = "none";
}
};
// 함수를 실행한다.
hide(findNodes());
- findNodes()에서 반환된 노드의 배열에 대해 hide()가 다시 루프를 돌아야 하기 때문에 비효율적이다.
-
findNodes()에서 노드를 선택하고 바로 숨긴다면 재차 루프를 돌지 않아 효율적이겠지만, findNodes()는 범용적인 함수이기 때문에 탐색과 수정 로직이 결합될 경우 범용 함수의 의미가 퇴색된다…
→ 이럴 때 콜백 패턴을 사용할 수 있다.
- 노드를 숨기는 로직의 실행을 콜백 함수에 위임하고, 이 함수를 findNodes()에 전달한다.
// findNodes()가 콜백을 받도록 리팩터링한다.
var findNodes = function (callback) {
var i = 100000, nodes = [],
found;
// 콜백 함수를 호출할 수 있는지확인한다 .
if (typeof callback !== "function") {
callback = false;
}
while (i) {
i -= 1;
// 이곳에 복잡한 로직을 구현한다.
// 여기서콜백을실행한다.
if (callback) {
callback(found);
}
nodes.push(found);
}
return nodes;
};
- findNodes()에는 콜백 함수가 추가되었는지 확인하고 있으면 실행하는 작업만 추가되기 때문에 직관적이다.
// 콜백 함수
var hide = function (node) {
node.style.display = "none";
};
// 노드를 찾아서 바로 숨긴다.
findNodes(hides);
- hide()처럼 이미 존재하는 함수를 콜백 함수로 쓸 수도 있지만 findNodes() 함수를 호출할 때 익명 함수를 생성하여 쓸 수도 있다.
// 익명 함수를 콜백으로 전달한다.
findNodes(function (node) {
node.style.display = "block";
});
콜백과 유효범위
callback(parameter);
방식으로 실행 시 대부분 훌륭하게 작동하지만,- 콜백이 일회성의 익명 함수나 전역 함수가 아니고 객체의 메서드인 경우,
- this를 사용하면 예기치 않게 동작할 수 있다.
var myapp = {};
myapp.color = "green";
myapp.paint = function (node) {
node.style.color = this.color;
};
var findNodes = function (callback) {
// ...
if (typeof callback === “function") {
callback(found);
}
// ... };
- findNodes(myapp.paint)를 호출하면 this.color가 정의되지 않아 예상대로 동작하지 않는다.
- findNodes()는 전역 함수이기 때문에 객체 this는 전역 객체를 참조한다.
- findNodes()가 dom이라는 객체의 메서드라면, 콜백 내부의 this는 예상과는 달리 myapp이 아닌 dom을 참조하게 된다.
이 문제를 해결하기 위해서는 콜백 함수와 함께 콜백이 속해있는 객체를 전달하면 된다.
findNodes(myapp.paint, myapp);
- 전달받은 객체를 바인딩하도록 findNodes() 또한 수정한다.
var findNodes = function (callback, callback_obj) {
//...
if (typeof callback === "function") {
callback.call(callback_obj, found);
}
// ...
};
- 콜백으로 사용할 메서드와 객체를 전달할 때 메서드를 문자열로 전달할 수도 있다.
- 명령을 이렇게 바꿀 수 있다.
findNodes(myapp.paint, myapp);
→**findNodes("paint", myapp);**
- 두 가지 방법에 모두 대응하는 함수를 다음과 같이 만들 수 있다.
var findNodes = function (callback, callbackobj) {
if (typeof callback === "string") {
callback = callback obj[callback];
}
//...
if (typeof callback === "function") {
callback.call(callback_obj, found); }
// ...
};
비동기 이벤트 리스너
- 페이지의 엘레먼트에 이벤트 리스너를 붙이는 것도 실제로는 이벤트가 발생했을 때 호출될 콜백 함수의 포인터를 전달하는 것이다.
- 다음 에제는 document의 click 이벤트 리스너로 console.log() 콜백함수를 전달하는 예제이다.
document.addEventListener("click", console.log, false);
- 대부분 클라이언트측 브라우저 프로그래밍은 이벤트 구동(event-driven)방식이다.
- 페이지의 로딩이 끝나면 load 이벤트를 발생시킨다.
- 사용자는 페이지에 click, kepress, mouseover, mousemove와 같은 다양한 이벤트를 발생시킨다.
- 자바스크립트가 이벤트 구동형 프로그래밍에 특히 적합한 이유는 프로그램이 비동기적으로, 달리 말하면 무작위로 동작할 수 있게 하는 콜백 패턴 덕분이다.
타임아웃
- 또 다른 콜백 패턴의 실전 예제는 브라우저 window 객체에 의해 제공되는 타임아웃 메서드들인 setTimeout()과 setInterval()이다.
- 이 메서드들도 콜백 함수를 받아 실행시킨다.
var thePlotThickens = function () {
console.log('500ms later...');
};
setTimeout(thePlotThickens, 500);
라이브러리에서의 콜백
- 라이브러리 메서드는 핵심 기능에 집중하고 콜백의 형태로 연결고리(hook)을 제공하라.
- 콜백 함수를 활용하면 쉽게 라이브러리 메서드를 만들고 확장하고 가다듬을 수 있다.
함수 반환하기
- 예제의 setup()은 초기화 작업 수행 후 실행 가능한 함수를 반환 값으로 하는 함수이다.
var setup = function () {
alert(1);
return function () {
alert(2);
};
};
// setup 함수를 사용
var my = setup(); // alert으로 1이 출력된다.
my(); // alert으로 2가 출력된다.
- setup()은 반환된 함수를 감싸고 있기 때문에 클로저를 생성한다.
클로저
는 반환되는 함수에서는 접근할 수 있지만, 코드 외부에서는 접근할 수 없기 때문에 비공개 데이터 저장을 위해 사용할 수 있다.
자기 자신을 정의하는 함수
- 새로운 함수를 만들어 이미 다른 함수를 가지고 있는 변수에 할당한다면, 새로운 함수가 이전 함수를 덮어쓰게 된다.
- 어떤 면에서는 이전의 함수 포인터가 새로운 함수를 가지도록 재사용하는 것이다.
- 함수 본문에서 이런 일을 하는 예제를 확인 해보자.
var scareMe = function () {
alert("Boo!");
scareMe = function () {
alert("Double boo!");
};
};
// 자기 자신을 정의하는 함수를 사용
scareMe(); // Boo!
scareMe(); // Double boo!
함수가 초기화 준비 작업을 단 한번만 수행할 경우에 유용하다.
- 이 패턴의 단점은 자기 자신을 재정의한 이후에는 이전에 원본 함수에 추가했던 프로퍼티들을 모두 찾을 수 없게 된다는 점이다.
- 또한 함수가 다른 이름으로 사용된다면, 예를 들어 다른 변수에 할당되거나 객체의 메서드로써 사용되면 재정의된 부분이 아니라 원본 함수의 본문이 실행된다.
-
scareMe() 함수를 다음과 같이 일급 객체로 사용하는 예제를 확인 해보자.
-
새로운 프로퍼티가 추가된다.
-
함수 객체가 새로운 변수에 할당된다.
-
함수는 메서드로써도 사용된다.
// 1. 새로운 프로퍼티를 추가한다. scareMe.property = "properly"; // 2. 다른 이름으로 할당한다. var prank = scareMe; // 3. 메서드로 사용한다. var spooky = { boo: scareMe }; // 새로운 이름으로 호출한다. prank(); // "Boo!" prank(); // "Boo!" console.log(prank.property); // "properly" // 메서드로 호출한다. spooky.boo(); // "Boo!" spooky.boo(); // "Boo!" console.log(spooky.boo.property); // "properly" // 자기 자신을 재정의한 함수를 사용한다. scareMe; // Double boo! scareMe(); // Double boo! console.log(scareMe.property); // undefined
-
즉시 실행 함수
- 함수가 선언되자마자 실행되도록 하는 문법
(function () {
alert('watch out!');
})();
- 이 패턴은 초기화 코드에 유효범위 샌드박스(sandbox)를 제공한다는 점에서 유용하다.
즉시 실행 함수의 매개변수
- 즉시 실행함수에 인자를 전달할 수도 있다.
// 출력결과:
// I met Joe Black on Fri Aug 13 2010 23:26:59 GMT-0800 (PST)
(function (who, when) {
console.log("I met " + who + “ on " + when);
}("Joe Black", new Date()));
- 즉시 실행 함수는 객체 프로퍼티를 정의할 때도 사용할 수 있다.
- 어떤 객체의 프로퍼티가 객체의 생명주기동안 값이 변하지 않고,
- 처음 값을 정의할 때 적절한 계산을 위한 필요하다고 가정했을 때
- 작업을 즉시 실행 함수로 감싼 후 반환 값을 프로퍼티 값으로 할당하면 된다.
함수 프로퍼티 - 메모이제이션(Memoization) 패턴
- 함수에 프로퍼티를 추가하여 결과(반환 값)를 캐시하면 다음 호출 시점에 복잡한 연산을 반복하지 않을 수 있다.
- 이런 활용 방법을
메모이제이션 패턴
이라고 한다.
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ... 비용이 많이 드는 수행 ...
myFunc.cache[pa ram] = result;
}
return myFunc.cache[param];
};
// 캐시 저장공간
myFunc.cache = {};
- 예시는 param이라는 단 하나의 매개 변수를 받는다고 가정하고 매개변수는 문자열과 같은 원시 데이터 타입이라고 가정한다.
- 객체 인자를 JSON 문자열로 직렬화하고 문자열을 cache 객체에 키로 사용할 수 있다.
var myFunc = function () {
var cachekey = JSON.stringify(
Array.prototype.slice.call(arguments)), result;
if (!myFunc.cache[cachekey]) {
result = {};
// ... 비용이 많이 드는 수행 ...
myFunc.cache[cachekey] = result;
}
return myFunc.cachefcachekey];
};
// 캐시 저장공간
myFunc.cache = {};
커리(Cury)
- 커링(currying)과 부분적인 함수 적용에 대해 알아보자
함수 적용
- 순수한 함수형 프로그래밍 언어에서 함수는 불려지거나 호출된다고 표현하기 보다 적용(apply)된다고 표현한다.
- 자바스크립트에서도
Function.prototype.apply()
를 사용하면 함수를 적용할 수 있다.
// 함수를 호출한다.
sayHi(); // "Hello"
sayHi('world'); // "Hello, world!"
// 함수를 적용(apply)한다.
sayHi.apply(null, ["hello"]); // "Hello, hello!"
- 함수 적용과 함수 호출은 같은 결과를 보여준다.
- apply()는 두 개의 매개변수를 받는데,
- 첫 번째는 이 함수 내에 this와 바인딩할 객체이고,
- 두 번째는 배열 또는 인자(arguments)로 함수 내부에서 배열과 비슷한 형태의 arguments 객체로 사용하게 된다.
- 첫번째 매개변수가 null이면 this는 전역객체를 가리킨다.
- 함수를 특정 객체의 메서드가 아닌, 일반적인 함수로 호출할 때와 같다.
- 함수가 객체의 메서드일 때는 null을 전달하지 않는다.
var alien = {
sayHi: function (who) {
return"Hello"+(who?","+who: "")+"!";
}
};
alien.sayHi('world'); // "Hello, world!"
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
- 함수 호출이라는 것은 사실 함수 적용을 가리키는 문법 설탕(syntatic sugar)이다.
- apply()와 비슷한 call()이라는 메서드 또한 마찬가지이다.
- call() 메서드는 전달하는 매개변수가 배열이 아닌 하나의 요소일 뿐이다.
// 배열을 만들지 않는 두 번째 방법이 더 효과적이다.
sayHi.apply(alien, ["humans"]); // "Hello, humans!"
sayHi.call(alien, "humans"); // "Hello, humans!"
부분적인 적용
- 함수의 호출이 실제로는 인자의 묶음을 함수에 적용하는 것임을 알게 되었다.
- 인자 전부가 아니라 일부 인자만 전달하려면 어떻게 할까?
- 수학 함수를 직접 계산할 때 쓰는 방법과 비슷하다.
// 설명을 위한 목적으로 사용되었다.
// 자바스크립트에서는 유효하지 않다.
// 이런 함수가 있고
function add(x, y) {
return x + y;
}
// 인자들을 알고있다.
add(5, 4);
// 1단계 -- 하나의 인자를 대체한다.
function add(5, y) {
return 5 + y;
}
// 2단계 -- 나머지 인자를 대체한다.
function add(5, 4) {
return 5 + 4;
}
-
부분적인 적용을 실행하면 결과가 나오는 대신 또다른 함수가 나온다.
-
다음은 부분적인 적용을 실행한 결과를 함수로 만들고, 이 함수에 다른 인자 값을 적용하여 호출할 수 있는 partialApply() 메서드의 사용법을 알아보자.
var add = function (x, y) { return x + y;
};
// 모든인자를적용한다.
add.apply(null, [5, 4]); // 9
// 인자를 부분적으로만 적용한다.
var newadd = add.partialApply(null, [5]);
// 새로운 함수에 인자를 적용
newadd.apply(null, [4]); // 9
- 사실 partialApply() 메서드는 존재하지 않고, 자바스크립트의 함수 또한 이렇게 작동하지 않는다.
- 하지만 자바스크립트는 굉장히 동적이기 때문에 이렇게 동작하도록 만들 수 있다.
함수가 부분적인 적용을 이해하고 처리할 수 있도록 만드는 과정을 커링(Curring)이라고 한다.
커링(Curring)
- 수학자 하스켈 커리(Haskell Curry)로 유래되었다.
- 하스켈 프로그래밍 언어도 그의 이름에서 따온 것이다.
- 다른 함수형 언어에서는 커링 기능이 언어 자체에 내장되어 있어 모든 함수가 기본적으로 커링된다.
- 자바스크립트에서는 add() 함수를 수정하여 부분 적용을 처리하는 커링 함수로 만들 수 있다.
// 커링된 add()
// 부분적인 인자의 목록을 받는다.
function add(x, y) {
var oldx = x, oldy = y;
if (typeof oldy === "undefined") { // 부분적인 적용
return function (newy) {
return oldx + newy;
};
}
// 전체 인자를 적용
return x + y;
}
// 테스트
typeof add(5); // "function"
add(3)(4); // 7
// 새로운 함수를 만들어 저장
var add2000 = add(2000);
add2000(10); // 2010
- 예제는 이해를 돕기 위해 풀어서 쓴 식이며, 조금 더 간략화 할 수 있다.
- oldx와 oldy를 없애는 방법
- 원래의 x는 암묵적으로 클로저에 저장되어 있고
- 이전 예제에서 newy라는 새로운 변수를 만들었던 것과 다르게 지역 변수 y를 재사용한다.
// 커링된 add
// 부분적인 인자의 목록을 받는다.
function add(x, y) {
if (typeof y === "undefined") { // 부분적인 적용
return function (y) {
return x + y;
};
}
// 전체 인자를 적용
return x + y;
}
- 어떤 함수라도 부분적인 매개변수를 받는 함수로 변형할 수 있을까?
- 범용 커링 함수 코드이다.
function schonfinkelize(fn) {
var slice = Array.prototype.slice,
stored_args = slice.call(arguments, 1);
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null, args);
};
}
- arguments가 실제로는 배열이 아니기 때문에 코드가 조금 복잡해진다.
- Array.prototype으로부터 slice() 메서드를 빌려오면 arguments를 배열로 바꿔 사용하기 편리하게 만들 수 있다.
- 예제의 매커니즘은 다음과 같다.
- 지역변수 slice에 slice() 메서드에 대한 참조를 저장한다.
- stored_args에 인자를 저장한다.
- 이 때 첫번째 인자는 커링될 함수이기 때문에 떼어낸다.
- 새로운 함수를 반환한다.
- 반환된 새로운 함수는 호출되었을 때 클로저를 통해 비공개로 저장해둔 stored_args와 slice 참조에 접근할 수 있다.
- stored_args와 새로운 인자 new_args를 합친다.
- 클로저에 저장되어 있는 원래의 함수 fn에 적용한다.
커링 예제
// 일반 함수
function add(x, y) {
return x + y;
}
// 함수를 커링하여 새로운 함수를 얻는다
var newadd = schonfinkelize(add, 5);
newadd(4); // 9
// 반환되는 새로운 함수를 바로 호출할 수도 있다.
schonfinkelize(add, 6)(7); // 13
// 혹은 인자가 여러개일 때...
// 일반 함수
function add(a, b, c, d, e) {
return a+b+c+d+e;
}
// 여러 개의 인자를 사용할 수도 있다.
schonfinkelize(add, 1, 2, 3)(5, 5); // 16
커링을 사용해야 할 경우
- 어떤 함수를 호출할 때 매개변수가 항상 비슷하다면 커링의 적합한 후보이다.
- 매개변수 일부를 적용하여 새로운 함수를 동적으로 생성하면,
- 이런 함수는 반복되는 매개변수를 내부적으로 저장하여 매번 인자를 전달하지 않아도 원본 함수가 기대하는 전체 목록을 미리 채워놓을 것이다
.
요약
함수의 중요한 특징
- 함수는 일급 객체다. 값으로 전달될 수 있고, 프로퍼티와 메서드를 확장할 수 있다.
- 함수는 지역 유효범위를 제공한다. 다른 중괄호 묶음은 그렇지 않다. 로컬 변수의 선언은 로컬 유효범위의 맨 윗부분으로 호이스팅 된다.
함수의 여러 가지 패턴
- API 패턴 : 함수에 더 좋고 깔끔한 인터페이스를 제공할 수 있게 도와준다.
- 콜백 패턴 : 함수를 인자로 전달한다.
- 설정 객체 : 함수에 많은 수의 매개변수를 전달할 때 통제를 벗어나지 않도록 해준다.
- 함수 반환 : 함수의 반환 값이 또다시 함수일 수 있다.
- 커링 : 원본 함수와 매개변수 일부를 물려받는 새로운 함수를 생성한다.
- 초기화 패턴 : 전역 네임스페이스를 어지럽히지 않고 임시 변수를 사용해 좀더 깨끗하고 구조화된 방법으로 수행한다.
- 즉시 실행 함수
- 즉시 객체 초기화
- 초기화 시점의 분기
- 성능 패턴 : 코드의 실행 속도를 높인다.
- 메모이제이션 패턴
- 자기선언 함수