자바스크립트 완벽가이드 8장 (함수)

함수는 한 번 정의하면 몇 번이든 실행할 수 있고 호출할 수 있는 자바스크립트 코드 블록이다. 함수 호출에는 전달인자 외에도 호출 컨텍스트가 포함되는데, this 키워드의 값이 바로 해당 컨텍스트다.
어떤 객체의 프로퍼티로 할당된 함수를 해당 객체의 메서드라 한다. 어떤 함수를 객체를 대상(on)으로, 또는 객체를 통해서(through) 호출하면, 이 객체는 해당 함수의 호출 컨텍스트, 즉 호출된 함수의 this 값이 된다. 새로 생성된 객체를 초기화하는 데 쓰이는 함수는 생성자(constructor)라고 한다.
자바스크립트 함수는 다른 함수 내에 중첩되어 정의될 수 있고, 중첩된 함수는 해당 함수가 정의된 유효범위 안의 어떤 변수에도 접근할 수 있다. 이는 자바스크립트 함수가 클로저(closure)이며, 클로저가 가능하게 하는 중요하고 강력한 프로그래밍 기법을 자바스크립트도 구사할 수 있음을 뜻한다.

8.1 함수 정의하기

함수는 function 키워드에 의해 정의되며, function 키워드는 함수 정의 표현식 또는 함수 선언문에서 사용된다.

1
2
3
4
5
6
7
8
// 정의된 함수를 변수에 할당할 수 있다.
var square = function(x) { return x*x; }
// 함수 표현식은 이름을 포함할 수 있다. 이러한 이름은 재귀 호출에 유용하게 사용된다.
var f = function fact(x) { if (x <= 1) return 1; else return x*fact(x-1); };
// 함수 표현식은 다른 함수의 전달인자로 사용 가능하다.
data.sort(function(a, b) { return a-b ; });
// 함수 표현식은 정의되는 즉시 호출 가능하다.
var tensquared = (function(x) {return x*x;}(10));

함수 정의 표현식에서 함수 이름은 옵션이다. 함수 선언문이 실제로 하는 일은, 어떤 변수를 정의하고 함수 객체를 그 변수에 할당하는 것이다. 함수 정의 표현식이 이름을 포함하면, 이 함수 몸체의 유효 범위에 해당 함수 객체에 연결된 이름이 포함된다. 사실상 그 함수 이름이 해당 함수의 지역 변수가 되는 것이다. 표현식 형태로 함수를 정의하는 것은 한 번만 사용되는 함수에 특히 적합하다.
함수 선언문은 그 함수를 둘러싼 스크립트나 함수의 맨 위로 끌어올려(hoisted)진다. 따라서 해당 함수는 이 함수가 정의된 위치보다 앞서 나오는 코드로부터 호출될 수 있다. 그러나 표현식으로 정의된 함수는 다르다. 함수를 호출하려면 먼저 호출할 함수를 참조할 수 있어야 하는데, 표현식으로 정의된 함수는 변수에 할당되기 전까지는 참조할 수 없다. 변수 선언은 끌어올려지지만, 변수 할당은 그렇지 않다. 그래서 표현식으로 정의된 함수는 정의되는 지점 위에서는 호출할 수 없다.
함수 대부분은 return문을 포함하고 있다. return 다음에 오는 표현식의 값을 호출자에게 반환하는데 return 다음에 표현식이 없다면 undefined 값을 반환한다. 함수가 return 문을 포함하지 않는다면, 함수 몸체 내의 각 구문을 실행 후 다음 호출자에게 undefined 값이 반환된다.

8.2 함수 호출하기

자바스크립트 함수는 네 가지 방법으로 호출할 수 있다.

  1. 일반적인 함수 형태
  2. 메서드 형태
  3. 생성자
  4. call()과 apply() 메서드를 통한 간접 호출

8.2.1 함수 호출

함수가 호출될 때는 먼저, 각각의 전달인자 표현식(괄호 사이에 있는 것)이 평가되고, 평가 결과 값이 해당 함수의 전달인자가 된다. 이 전달인자 값들은 함수 정의에 등장하는 형식인자 각각에 대응된다.
일반적인 함수 형태로 호출하도록 작성된 함수는 보통 this 키워드를 사용하지 않는다.

8.2.2 메서드 호출

메서드는 객체의 속성으로 저장된 자바스크립트 함수일 뿐이다. 그러나 메서드 호출함수 호출에 비해 한 가지 중요한 부분이 다른데, 바로 호출 컨텍스트다. 메서드 호출 표현식에서는 객체가 호출 컨텍스트가 되므로, 함수 몸체에서 this 키워드를 사용해서 객체를 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
var calculator = {
operand1: 1,
operand2: 1,
add: function() {
// 이 객체를 참조하기 위해 this 키워드를 사용
this.result = this.operand1 + this.operand2;
}
};
calculator.add();
calculator.result // => 2

메서드this 키워드는 자바스크립트 객체 지향 프로그래밍 패러다임의 중심이다. 메서드로 사용되는 함수는 메서드의 호출 대상 객체를 암시적 인자로 전달받는다.

메서드 체이닝
메서드 체이닝은 객체 이름은 한 번만 사용하고 메서드는 여러 번 호출할 수 있는 방식이다. (메서드가 객체를 반환하면, 메서드의 반환 값을 후속 호출의 일부로 사용)

변수와 달리, this 키워드에는 유효범위(scope)가 없고 중첩 함수는 호출자의 this 값을 상속하지 않는다. 만약 중첩 함수가 메서드 형태로 호출되면, 그 함수의 this 값은 그 함수의 호출 대상 객체다. 가장 흔한 실수는, 함수 형태로 호출된 중첩 함수가 바깥쪽 함수의 호출 컨텍스트를 획득하기 위해 this 값을 사용할 수 있다고 가정하는 것이다. 만약 바깥쪽 함수의 this 값에 접근하고 싶다면, 안쪽 함수의 유효범위에 바깥쪽 함수의 this 값을 별도의 변수로 저장해야 한다. 이러한 용도로 보통 self 변수를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
m: function() {
var self = this; // this 값을 변수에 저장
console.log(this === o); // true: this는 객체 o이다.
f(); // 헬퍼 함수 f() 호출
function f() { // 중첩 함수 f()
console.log(this === o); // false: this는 global 객체 또는 undefined이다.
console.log(self === o); // true: self는 바깥쪽 함수의 this 값이다.
}
}
};
o.m();

8.2.3 생성자 호출

함수나 메서드 호출 앞에 new 키워드가 있다면 생성자 호출이다. 생성자 호출은 일반 함수와 메서드 호출에 비해 매개변수, 호출 컨텍스트와 반환 값을 다루는 방식이 다르다.
생성자에 전달인자(매개변수) 목록이 없다면, 자바스크립트 생성자 호출 문법은 전달인자 목록과 괄호를 아예 생략하는 것을 허용한다.

1
2
3
var o = new Object();
var o = new Obejct;
// 위 두개는 같은 코드이다.

생성자를 호출하면 생성자의 prototype 프로퍼티를 상속받은 새로운 빈 객체가 생성된다. 생성자 함수는 객체를 초기화하고, 새로 생성된 이 객체는 생성자 함수의 호출 컨텍스트로 사용된다. 따라서 생성자 함수는 새로 생성된 객체를 this 키워드로 참조할 수 있다. 주의할 것은 생성자 호출이 마치 메서드 호출처럼 보일지라도, 메서드가 속한 객체가 아닌 새로 생성된 객체가 호출 컨텍스트로 사용된다는 점이다. (즉, new o.m()과 같은 표현식에서 o가 호출 컨텍스트로 사용되지는 않는다는 뜻)
생성자 함수는 보통 return 키워드를 사용하지 않는다. 일반적으로 생성자 함수는 새 객체를 초기화하고, 생성자 함수 몸체의 끝에 이르면 암시적으로 그 객체를 반환한다. 새 객체가 생성자 호출 표현식의 값이다.

8.2.4 간접 호출

자바스크립트 함수는 객체이고, 모든 자바스크립트 객체와 같이 함수에도 메서드가 있다. 이 메서드 중 call()apply()는 함수를 간접적으로 호출한다. 두 메서드 모두 호출 때 this 값을 명시적으로 지정할 수 있는데, 이는 어떤 함수든지 특정 객체의 메서드로 호출할 수 있다는 의미다. 심지어 함수가 실제로 그 객체에 속하지 않더라도 말이다.

8.3 함수 전달인자와 매개변수

8.3.1 생략 가능한 매개변수

정의된 것보다 적은 수의 전달인자로 함수가 호출되면, 나머지 매개변수는 undefined 값으로 설정된다.

1
2
3
4
5
function getPropertyNames(o, /* optional */ a) {
if (a === undefined) a = []; // 만약 undefined이면 새 배열을 사용한다.
for(var property in o) a.push(property);
return a;
}

getPropertyNames의 첫줄에는 if문 대신, 관용적으로 || 연산자를 사용하기도 한다.

1
a = a || [];

|| 연산자는 첫 번째 피 연산자가 true이거나 true로 변환될 수 있는 값이면 첫 번째 피 연산잘르 반환하고, 그렇지 않으면 두 번째 피 연산자를 반환한다. 따라서, 두 번째 인자가 생략된다면(또는 null이거나 false혹은 false로 변환될 수 있는 값이라면), 새로 생성된 빈 배열이 대신 사용될 것이다.

8.3.2 가변길이 전달인자 목록: Arguments 객체

함수가 호출될 때 정의된 매개변수보다 더 많은 인자가 전달되면, 매개변수 이름이 붙지 않은 인자 값을 직접적으로 참조할 방법은 없다. Arguments 객체는 이러한 문제에 대한 해결책이다. 함수 몸체 내에서 arguments 식별자는 해당 호출에 대한 Arguments 객체를 참조한다. Arguments 객체는 유사 배열 객체이고, 이름이 아니라 인덱스 숫자를 통해 함수의 전달인자를 얻어올 수 있다. (내장 함수 Math.max()가 Arguments를 사용하여 동작한다.)
arguments는 실제로는 배열이 아니라 Arguments 객체이다. 각 Arguments 객체는 숫자 인덱스가 붙은 배열 원소와 length 프로퍼티를 갖고 있다. 그러나 배열은 아니다. 어쩌다가 숫자로 된 프로퍼티를 갖고 있는 객채이다.
Arguments 객체의 배열 원소와 매개변수의 이름은 동일한 값을 가리키는 다른 두 이름이다. (Arguments 객체가 평범한 배열이라면, arguments[0]과 x는 같은 값을 가질 수 있지만, 하나를 변경하는 작업이 다른 하나에 영향을 미치지는 않을 것이다.)

1
2
3
4
5
function f(x) {
console.log(x); // 전달인자의 초기 값 출력
arguments[0] = null; // 배열 요소를 변경하면 x 또한 변경
console.log(x); // 이제 null을 출력
}

8.3.3 객체의 프로퍼티를 전달인자로 사용하기

어떤 함수에 세 개 이상의 매개변수가 있다면, 이 함수를 호출할때 인자의 올바른 순서를 기억하기가 어렵다. 따라서 전달인자를 순서에 상관없이 이름/값의 쌍으로 함수에 전달하는 편이 효과적일 수 있다. 단일 객체를 전달인자로 받는 함수를 정의하고, 함수의 사용자에게 함수에서 요구하는 이름/값 쌍을 가진 객체를 함수의 인자로 넘기도록 하면 된다.

8.3.4 전달인자 형식

자바스크립트 메서드의 매개변수에는 정의된 형식도 없고, 함수에 전달한 값에 대해서 자료형 검사도 하지 않는다. 한두 번만 사용하고 ‘버릴’함수가 아니라면, 인자 자료형을 검사하는 코드를 추가할 가치가 있다.
자바스크립트는 매우 유연하며 자료형을 느슨하게 처리하는 언어이기에, 때로는 인자 개수와 자료형에 유연한 함수를 작성하는 것이 바람직하다.

8.4 값으로서의 함수

자바스크립트에서 함수는 문법일 뿐만 아니라 값이기도 한데, 이는 함수가 변수에 할당될 수 있고 객체의 프로퍼티나 배열 원소로 저장될 수도 있으며, 다른 함수의 인자로 전달될 수도 있고, 기타 여러 방식으로 사용될 수 있음을 뜻한다.

8.4.1 자시만의 함수 프로퍼티 정의하기

자바스크립트에서 함수는 원시 값이 아니지만 특별한 종류의 객체이고 이는 함수가 프로퍼티를 가질 수 있음을 의미한다. 함수가 여러 번 호출되어도 그 값이 유지되어야 하는 ‘정적’ 변수가 필요할 때는, 전역 변수를 사용하는 것보다 함수의 프로퍼티를 사용하는 것이 편리한 경우가 많다.

1
2
3
4
5
6
7
8
9
10
// 팩토리얼을 계산하고 계산 결과를 함수 자신의 프로퍼티에 캐시한다.
function factorial(n) {
if (isFinite(n) && n>0 && n==Math.round(n)) {
if (!(n in factorial)) // 만약 캐시 해둔 결과가 없다면
factorial[n] = n * factorial(n-1); // 팩토리얼을 계싼하고, 계산 값 캐시
return factorial[n]; // 캐시 결과를 반환
}
else return NaN;
}
factorial[1] = 1; // 캐시를 기본 경우(1)에 대한 값으로 초기화

8.5 네임스페이스로서의 함수

자바스크립트는 함수 단위의 유효범위를 갖는다. 함수 내부에 정의된 변수는 해당 함수 내부(중첩 함수를 포함한)에서는 접근 가능하지만, 그 함수 바깥에는 존재할 수 없다. 함수 밖에서 정의된 변수는 전역 변수이고 자바스크립트 프로그램 전체에서 접근할 수 있다.

1
2
3
(function () {       // 이름이 없는 표현식으로 함수 작성
// 모듈 코드 위치
}()); // 함수 리터럴을 끝내고 바로 호출

단일 표현식으로 함수를 정의하고 호출하는 방식은 관용적으로 자주 사요되는 기법이다. 함수 앞의 시작 괄호는 반드시 필요한데, 만약 시작괄호가 없다면 자바스크립트 인터프리터는 function 키워드를 함수 선언문으로 해석하기 때문이다. 괄호가 있으면 인터프리터는 이것을 표현식 형태의 함수 선언으로 올바르게 인식한다. 괄호가 꼭 필요하지 않은 상황에서도, 정의하자마자 호출할 함수를 괄호로 둘러싸는 건 관용적인 방식이다.

8.6 클로저

자바스크립트는 다은 언어와 마찬가지로 어휘적 유효범위를 사용한다. 함수를 호출하는 시점에서의 변수 유효범위가 아니라, 함수가 정의된 시점의 변수 유효범위를 사용하여 함수가 실행된다는 뜻이다. 이러한 어휘적 유효범위를 구현하기 위해, 자바스크립트 함수 객체는 내부 상태에 함수 자체의 코드뿐만 아니라 현재 유효범위 체인에 대한 참조도 포함하고 있다. 함수 객체와 함수의 변수가 해석되는 유효범위(변수 바인딩의 집합)를 아울러 컴퓨터 과학 문헌에서는 클로저(closure)라고 부른다.(내부함수는 외부함수의 지역변수에 접근 할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있다. 이러한 메커니즘이 클로저이다.)
기술적으로 자바스크립트 함수는 클로저이다. 함수는 객체이고 함수 자신과 관련된 유효범위 체인을 갖고있기 때문이다. 함수 대부분은 함수가 정의되었을 때의 유효범위 체인을 사용하여 호출되고, 클로저가 개입되었는지의 여보는 중요하지 않다.

1
2
3
4
5
6
7
var scope = "global scope";             // 전역 변수
function checkscope() {
var scope = "local scope"; // 지역 변수
function f() { return scope; } // 이 유효범위에 있는 값을 반환
return f;
}
checkscope()()

checkscope()는 중첩 함수를 객체 그 자체를 반환한다. 어휘적 유효범위의 기본적은 규칙을 기억해야한다. 자바스크립트 함수는 함수가 정의되었을 때의 유효범위 체인을 사용하여 실행된다. 중첩 함수 f()가 정의된 유효범위 체인에서 변수 scope는 “local scope”로 바인드되어 있다. f가 어디서 호출되든 상관없이, f가 실행될 때 이 바인딩은 항상 유효하다. 따라서 코드의 제일 마지막 줄은 “global scope”가 아니라 “local scope”를 반환한다. 이것이 클로저의 놀랍고 강력한 특성이다. 클로저는 자신을 정의한 바깥쪽 함수에 바인딩된 지역 변수(그리고 전달인자)를 포착한다.

1
2
3
4
var uniqueInteger = (function() {      // 함수를 정의하고 바로 호출
var counter = 0; // 아래 함수의 내부 상태
return function() { return counter++; };
}());

중첩 함수는 유효범위에 있는 변수에 접근하고, 바깥쪽 함수에 정의된 counter 변수를 사용할 수 있다. 바깥쪽 함수의 실행이 끝나면, 어떤 코드도 counter 변수를 볼 수 없다. 오직 안쪽 함수만 단독으로 counter 변수에 접근할 수 있을 뿐이다. counter와 같은 내부 변수는 여러 클로저가 공유할 수 있다. 즉, 같은 함수 안에 정의된 중첩 함수들은 같은 유효범위 체인을 공유한다.
클로저 기법과 getter/setter 프로퍼티들을 결합할 수 있다. 다음 예제는 내부 상태를 다루는 데 일반 객체 프로퍼티 대신 클로저를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function counter(n) {        // 함수 전달인자 n은 내부 변수다.
return {
// getter 메서드 프로퍼티는 counter 변수를 반환하고 증가시킨다.
get count() { return n++; },
// setter 메서드는 프로퍼티 n 값을 감소시키는 것을 허용하지 않는다.
set count(m) {
if ( m >= n) n = m
else throw Error("count는 오직 더 큰 값으로만 설정될 수 있습니다.");
}
};
}

var c = conter(1000);
c.count // => 1000
c.count // => 1001
c.count = 2000 // => 2000
c.c ount // => 2000
c.count = 2000 // => 에러!

counter() 함수는 지역 변수를 정의하지 않지만, 프로퍼티 접근 메서드들이 공유하는 내부 상태를 보관하기 위해 매개변수 n을 사용한다. 이로써 counter()를 호출하는 쪽에서 내부 변수의 초기 값을 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
// 0-9 값을 반환하는 함수들의 배열을 반환
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++)
funcs[i] = function() { return i; };
return funcs;
}

var funcs = constfuncs();
funcs[5]() // 무엇이 반환될까?

위의 코드는 열 개의 클로저를 생성하고, 생성한 클로저들을 배열에 저장한다. 모든 클로저는 같은 함수 내에서 정의되고, 따라서 클로저들은 변수 i에 대한 접근을 공유한다. constfuncs() 실해잉 끝나면, 변수 i의 값은 10이고, 열 개의 클로저 모두 이 값을 공유한다. 클로저와 연관된 유효범위 체인이 ‘살아 있다’는 사실을 기억해야 한다. 중첩 함수는 유효범위에 대한 내부 사본이나 변수 바인딩의 스냅샷 따위는 만들지 않는다.

더 자세한 내용은 다음을 참고
클로저(MDN)
자바스크립트의 스코프와 클로저

8.7 함수 프로퍼티, 메서드, 생성자

자바스크립트에서 함수는 일종의 값이다. typeof 연산자를 사용하면 “function” 문자열을 얻을 수 있지만, 함수는 정말 독특한 자바스크립트 객체다. 함수는 객체이기 때문에 프로퍼티와 메서드를 가질 수 있으며 Function() 이라는 생성자도 갖고 있다.

8.7.2 prototype 프로퍼티

모든 함수에는 prototype 프로퍼티가 있는데, 이 프로퍼티는 프로토타입 객체를 참조한다. 모든 함수는 서로 다른 프로토타입 객체를 갖고 있고, 함수가 생성자로 사용될 때, 새로 생성된 객체는 함수의 프로토타입 객체로부터 프로퍼티들을 상속받는다.

8.7.3 call()과 apply() 메서드

call()apply()는 어떤 함수를 다른 객체의 메서드인 것처럼 간접적으로 호출할 수 있도록 한다. call()과 apply()의 첫 번째 인자는 호출되는 함수와 관련이 있는 개체다. 이 첫 번째 인자는 호출 컨텍스트고 함수 몸체에서 this 키워드의 값이 된다. 함수 f()를 객체 o의 메서드로 호출하려면 다음과 같이 사용한다.

1
2
f.call(o);
f.apply(o);

call()의 첫 번째 호출 컨텍스트 다음에 있는 모든 인자는 호출되는 함수로 전달된다. apply() 메서드는 call() 메서드와 비슷하지만, 함수에 전달할 인자는 배열 형태여야 한다. apply()는 실제 배열과 마찬가지로 유사 배열 객체와도 잘 작동한다. 특히 arguments 배열을 직접 apply()에 넘김으로써, 다른 함수를 호출할 때 현재 함수에 전달된 인자와 같은 인자를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
// 객체 o의 메서드 m을, 원본 메서드 호출 전후에 로그 메시지를 남긴다.
function trace(o, m) {
var original o[m]; // 원본 메서드를 클로저에 기억
o[m] = function() {
console.log(new Date(), "Entering:", m);
var result = original.apply(this, arguments); // 원본 메서드 호출
console.log(new Date(), "Exiting:", m);
return result;
};
}

8.7.4 bind() 메서드

bind() 메서드는 ECMAScript 5에 추가되었다. bind()의 주요 목적은 함수와 객체를 서로 묶는 것이다. 함수 f의 bind() 메서드를 호출하면서 객체 o를 전달하면, bind() 메서드는 새로운 함수를 반환한다. 반환된 새 함수를 호출하면, 원래 함수 f가 o의 메서드로 호출된다. 새로운 함수에 전달한 모든 인자는 원래 함수에도 전달된다.

1
2
3
4
function f(y) { return this.x + y; }     // 바인드되어야 하는 함수
var o = { x : 1 }; // 바인드 될 객체
var g = f.bind(o); // g(x)를 호출하면 o.f(x)가 호출된다.
g(2) // => 3

ECMAScript 5의 bind() 메서드는 함수를 객체에 바인딩하는 것보다 더 많은 일을 한다. bind()에 전달하는 인자 중 첫 번째 이후의 모든 인자는 this 값과 함께 해당 함수의 인자로 바인딩된다. 이를 커링(currying)이라 부르기도 한다.

1
2
3
4
var sum = function(x, y) { return x + y };
// 첫 번째 인자는 1로 바인딩된다. 새로운 함수는 단지 하나의 인자만 요구한다.
var succ = sum.bind(null, 1);
succ(2) // => 3: x는 1에 바인딩되고 y 인자로 2를 넘긴다.

출처 : “JavaScript: The Definitive Guide, by David Flanagan (O’Reilly). Copyright 2011 David Flanagan, 978-0-596-80552-4”

Author

KimJongMin

Posted on

2017-01-30

Updated on

2021-03-22

Licensed under

댓글