이번 글은 '何となくJavaScriptを書いていた人が1歩先に進むための本(아무 생각 없이 JavaScript를 작성하던 사람이 한 걸음 더 나아가기 위한 책)의 Chapter 4. 関数の理解を深めよう!(함수에 대한 이해를 넓혀보자!)'를 바탕으로 작성하였습니다.
함수를 제패하는 자 프로그래밍 세계를 제패한다
JavaScript의 세계에서 객체와 더불어 중요한 키워드는 '함수'이다. 다른 언어와 달리 'JavaScript에서는 함수도 자료형의 일종'이어서 다른 언어를 경험한 사람들은 이 점을 자주 헷갈려한다. 따라서 JavaScript를 이해하는 데 있어 JavaScript만의 독자적인 함수의 기능을 아는 것이 매우 중요하다.
함수의 정의 방법
JavaScript에서 함수는 3가지 방법으로 정의 가능하다.
- function 명령
- function 생성자
- 함수 literal
이 중에서 잘 쓰이지 않는 function 생성자를 제외하고 나머지 방법에 대해 알아보겠다.
우선 function 명령으로 정의하는 법부터 살펴보자. 아마 가장 눈에 익은 방법일 것이다.
function addNumber(num1, num2) {
return num1 + num2;
}
console.log(addNumber(1, 2)); // 3
다른 언어들과 마찬가지로 작성하면 되기 때문에 자세히 알아보지는 않겠다.
다음으로 함수 literal을 사용한 정의 방법이다. 이 방법은 JavaScript만의 독자적인 방법이다.
var addNumber = function(num1, num2) {
return num1 + num2;
};
console.log(addNumber(1, 2)); // 3
예제를 보면 알겠지만 '함수를 변수에 대입'한다. 더욱이 변수에 대입 가능하다는 말은 그 변수를 '다른 함수의 인수나 반환값'으로 쓰는 것도 가능하다는 뜻이다.
변수의 scope
JavaScript의 변수는 정의한 위치에 따라 scope가 정해진다.
- 함수 밖에서 정의 → 전역 변수
- 함수 안에서 정의 → 지역 변수
다음으로 지역 변수의 유효 범위에 대해 살펴보자.
var val = 'Global!';
function getVal() {
console.log(val);* // undefined
var val = 'Local!';
return val;
}
console.log(getVal()); // Local!
console.log(val); // Global!
getVal() 함수의 log 출력값이 undefined인 점에 대해 의문을 가질 수 있다. 왜 이런 결과가 나오는지 알아보자.
- JavaScript의 '지역 변수는 함수 전체에서 유효하다'
- *의 시점에서는 이미 'val' 변수가 '지역 변수로서 해석된다'
- *의 시점에서는 지역 변수 'val'의 'var 명령이 아직 실행되지 않았다'
따라서 출력 결과가 undefined이 된다.
arguments 객체
이제 JavaScript에서 함수의 다양한 활용 방법에 대해 알아보겠다.
function showVal(val) {
console.log(val);
}
showVal(); // undefined
showVal('bar'); // bar
showVal('hoge', 'foo'); // hoge
3종류의 호출 패턴 모두 정상적으로 결과값을 반환한다. 다른 언어를 경험한 사람이 보면 머리가 아파 올 결과물이다. 이 같은 결과물이 나오는 까닭은 JavaScript에서는 '함수를 호출할 때 인수 확인을 수행하지 않기' 때문이다. 하지만 이는 곤란한 경우가 있다. 함수가 인수를 정의할 때 인수들을 사용하여 작업을 수행하는 것을 전제로 하므로 적어도 사전 확인 작업이 필요한 것이다.
이럴 때 도움이 되는 것이 'arguments 객체'이다. arguments 객체는 '호출된 곳에서 전달된 인수를 관리'하는 객체로서, '함수 본체의 스코프 내에서 이용 가능'하다.
함수에 전해지는 인수의 정의 여부나 개수를 불문하고 '함수 실행 시에 전해진 모든 일수를 이 객체에서 관리'한다.
var addFunc = function() {
console.log(arguments[0] + arguments[1]);
}
addFunc(1, 2); // 3
위 예제에서 addFunc 함수 자체에는 인수가 정의되어 있지 않으나 호출할 때 전해진 값이 온전히 arguments 객체에 들어 있음을 볼 수 있다. 특히 이 arguments 객체는 'length 프로퍼티'를 갖고 있어서 다음 예처럼 인수를 확인하는 것도 가능하다.
var addFunc = function(val1, val2) {
if (arguments.length != 2) {
throw new Error('인수가 부정확합니다!');
}
console.log(val1 + val2);
}
addFunc(10, 10); // 20
addFunc(1); // 에러 발생
또한 이 length 프로퍼티를 잘 활용하면 '가변 길이 인수를 가지는 함수'도 간단히 만들 수 있다.
var addFunc = function() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
console.log(result);
}
addFunc(1, 1); // 2
addFunc(1, 2, 3, 4, 5); // 15
이렇듯 arguments 객체 활용에 익숙해지면 자신이 만들 수 있는 함수의 폭이 확연히 넓어진다.
인수에 이름 붙이기
JavaScript에서는 인수에 이름을 붙여두어 호출 시에 명시적으로 지정하는 것도 가능하다. 이름을 붙인 인수를 사용함으로써 인수의 개수가 늘어나도 가독성이 떨어지지 않게 하거나, 인수의 순서도 자유롭게 변경 가능하다.
사용 방법은 다음과 같다.
function showInfo(args) {
var name = args.name;
var age = args.age;
console.log(name + ' ' + age);
}
showInfo({ name: 'iga', age: 35}); // iga 35
showInfo({ age: 37, name: 'tara'}); // tara 37
위 예제처럼 코드가 짧을 경우 인수에 이름을 붙여 활용할 수 있다는 것의 유용함을 깨닫지 못하지만 만약 인수가 10개 이상 되는 경우를 상상해본다면 굉장한 효력을 실감할 수 있을 것이다.
고차함수
고차함수란 '함수 자신을 인수나 반환값으로 취급하는 함수'를 뜻한다. 언뜻 들으면 복잡한 듯하나 JavaScript에서는 '함수도 자료형의 일종'이라는 사실을 떠올려보길 바란다. 자료형의 일종이기에 함수를 인수에 대입 가능한 것이다. 이와 마찬가지로 함수를 다른 함수의 인수나 반환값으로 설정하는 것도 가능하다. 이러한 성질을 갖는 함수를 일컬어 고차함수라고 한다.
function higherOrder (values, fnc) {
for (var val in values) {
fnc(values[val]); // 1, 3, 5를 차례로 반환하여 함수 higherOrder의 두 번째 인수인 함수의 매개변수 val에 할당된다.
}
}
higherOrder([1,3,5], function(val){ console.log(val)}); // 1, 3, 5 순으로 console에 표시된다.
여기서는 함수의 인수에 함수를 지정하였다. 함수의 인수에 함수를 입력할 때의 이점은 인수로서 전해지는 함수를 자유롭게 바꿀 수 있다는 점이다. 인수를 바꿀 수 있다는 것을 전제로 함수를 설계하면 보다 유연하고 범용성이 높은 함수를 만들 수 있다.
스코프 체인(Scope Chain)
우선 스코프 체인이란 'JavaScript가 어떤 순서로 변수나 프로퍼티를 참조하는지 정해놓은 법칙'이라고 생각하자. 그리고 이 스코프 체인을 이해하는 첫걸음으로서 'call 객체'라는 것부터 살펴보자.
call 객체란 '함수를 호출할 때마다 내부적으로 생성되는 객체'로 함수 안의 지역 변수를 관리한다. 보통 코딩할 때 call 객체를 의식할 필요는 없다. 하지만 스코프 체인을 이해하기 위해서는 call 객체의 존재를 확실히 알아둬야 한다. 왜냐하면 '스코프 체인이란 전역 객체와 call 객체를 생성한 순서에 따라 연결한 목록'과 같은 것이기 때문이다.
이 목록과 같은 것은 '맨 처음 생성된 전역 객체를 가장 끝으로 삼아, call 객체가 생성될 때마다 그 앞에 연결하는' 식으로 동작한다고 보면 된다. 그리고 JavaScript의 변수 해결의 구조는 이 '리스트의 선두부터 순서대로 가장 처음 발견된 값을 취하는' 구조로 되어 있다.
/* C */
var x = 'Global';
var y = 'Global';
/* B */
function outerFunc(){
var x = 'Local Outer';
/* A */
function innerFunc(){
var x = 'Local Inner';
// Local Inner 1단계
console.log(x);
// Global 2단계
console.log(y);
// undefined 3단계
console.log(z);
}
// Local Outer 4단계
console.log(x);
innerFunc();
}
// Global 5단계
console.log(x);
outerFunc();
A: innerFuncのCallオブジェクトのスコープ
B: outerFuncのCallオブジェクトのスコープ
C: Globalオブジェクトのスコープ
위 예제에서는 아래와 같은 스코프 체인이 생성된다.
그렇다면 예제의 1 ~ 5를 거치며 각 단계별로 어떻게 변수를 해결하는지 알아보자.
- 1단계는 innerFunc 내에 적혀 있기 때문에 변수 해결은 innerFunc의 call 객체에서 시작한다. 그리고 innerFunc의 call 객체에는 x가 존재한다. 따라서 1단계의 시점에서 x는 'Local Inner'가 된다.
- 2단계도 innerFunc 안에 적혀 있다. 하지만 innerFunc의 call 객체에는 y가 존재하지 않는다. 스코프 체인을 거쳐 다음에 오는 outerFunc의 call 객체를 보아도 y는 없다. 허나 스코프 체인을 거쳐 전역 객체를 보면 y가 존재한다. 따라서 2단계에서 y는 'Global'이 된다.
- 3단계도 innerFunc에 적혀 있다. 하지만 innerFunc의 call 객체, outerFunc의 call 객체, 전역 객체를 살펴보아도 아무 데도 z는 존재하지 않는다. 따라서 3단계에서 z는 존재하지 않으므로 'undefined'가 된다.
- 4단계는 outerFunc에 적혀 있기 때문에 변수 해결은 outerFunc의 call 객체부터 시작한다. 그리고 outerFunc의 call 객체에는 x가 존재한다. 따라서 4단계에서 x는 'Local Outer'가 된다.
- 5단계는 함수 밖에 적혀 있기 때문에 변수 해결은 전역 객체부터 시작한다. 그리고 전역 객체에는 x가 존재한다. 따라서 5단계에서 x는 'Global'이 된다.
call 객체의 존재를 의식할 수 있다면 의외로 쉽게 이해할 수 있을 것이다. 이해가 안 된다면 예제를 나름대로 수정해가면서 이해해보도록 하자.
클로저(Closure)
클로저를 말로써 정의한다면 '지역 변수를 참조하면서 함수 안에 정의된 함수'이다. 말만 들으면 이해가 안 되니 예를 살펴보자.
function closure(initVal) {
var count = initVal;
var innerFunc = function() {
return ++count;
};
return innerFunc;
}
var myClosure = closure(100);
console.log(myClosure()); // 101
console.log(myClosure()); // 102
console.log(myClosure()); // 103
위 예제에서 '지역 변수 = count'이고, 이 'count를 참조하면서 함수 안에 정의된 함수는 innerFunc 함수'이다. 따라서 이 예제에서 클로저에 해당하는 것은 바로 'innerFunct 함수'이다. 그렇다면 왜 myClosure()를 호출할 때마다 결과값이 증가하는지 알아보자.
일반적으로 함수 안에서 정의된 지역 변수는 함수 처리가 끝나면 그 시점에서 파기된다. 하지만 위 예제 코드의 경우 지역 변수(count)를 참조하는 함수(innerFunc)가 반환되기 때문에 innerFUnc 함수 자체는 myClosure에 입력된다. 그리고 myClosure는 전역 변수이므로 전역 객체가 존재하는 한 없어지지 않는다.
요약하자면 이 예제에서 'myClosure가 지역 변수(count)를 계속해서 참조'하게 된다. 그 결과 '지역 변수 count는 myClosure가 유효한 이상 파기되지 않는' 것이다. closure 함수를 호출할 때 전달된 값(100)이 지역 변수(count)에 대입되어 이후 myClosure()가 호출될 때마다 증가되는 것이다.
위 내용을 보다 완벽히 이해하기 위해 위 예제 코드를 '스코프 체인'의 개념을 더해 정리해보자.
이 스코프 체인은 innerFunc 함수가 유효한 이상 계속해서 유지된다. 또한 innerFunc 함수는 myClosure 안에 포함되어 있기 때문에 closure 함수가 종료되어도 파기되지 않는다. (closure 함수의 call 객체도 innerFunc 함수의 call 객체가 계속 참조하고 있기 때문에 역시 파기되지 않는다.) 그 결과 closure 함수가 종료된 후에도 다음과 같은 일이 일어난다.
- 스코프 체인은 파기되지 않는다.
- 그렇다는 말은 innerFunc 함수의 call 객체도 파기되지 않는다.
- 그렇다면 call 객체에서 관리되는 지역 변수(count)도 파기되지 않는다.
- 지역 변수(count)는 파기되지 않으므로 closure 함수를 호출할 때 대입된 값(100)이 남는다.
- 이 과정의 결과 myClosure()가 호출될 때마다 지역 변수(count)가 증가한다 ※ myClosure의 진정한 모습은 innerFunc 함수이며 이 함수는 count를 증가시켜 반환하는 함수다.
'👩💻 Programming > JavaScript' 카테고리의 다른 글
프로토타입(Prototype)을 알아보자! (0) | 2022.04.07 |
---|---|
함수를 좀더 이해해보자! (0) | 2022.04.07 |
'this'를 갈고 닦자! (0) | 2022.04.07 |
함수 장인이 되는 길_함수 호출에 대한 이해 from Secrets of the JavaScript Ninja (0) | 2022.04.06 |
초심자를 위한 함수의 정의와 arguments_3. arguments & parameters from Secrets of the JavaScript Ninja (0) | 2022.04.05 |
댓글