목표
12장 함수
12-1. 함수란?
- 일련의 과정을 문(statement)으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것이다.
- 함수는 함수 정의(function definition)를 통해 생성한다.
function add(num1, num2) {
return num1 + num2;
}
- 이 때, 함수 내부로 입력을 전달받는 변수를 매개변수(parameter)라고 한다.
- 그리고 return 을 통해 출력하고자 하는 값을 반환값(return value)이라고 한다.
var result = add(1, 2);
console.log(result);
3
- 입력 값인 인수(argument)와 함께 함수의 실행을 명시적으로 지시해야 한다. 이를 함수 호출(function call)이라고 한다.
12-2. 함수는 왜 사용할까?
- 가장 큰 이유는 필요할 때 여러 번 호출 할 수 있다는 점이다.
- 즉, 실행 시점을 개발자가 결정할 수 있고 몇 번이든 호출할 수 있다.
- 동일한 작업을 반복적으로 수행해야 한다면 같은 코드를 여러 번 정의하는 것보다 미리 정의된 함수를 사용하는 것이 효과적이다.
- 이는 코드의 재사용, 유지보수의 편의성 측면에서 유용하며 코드의 신뢰성을 높인다.
12-3. 함수 리터럴
- 자바스크립트의 함수는 객체 타입의 값이다. 따라서 숫자 리터럴, 객체 리터럴 처럼 함수도 함수 리터럴로 생성할 수 있다.
- 함수 리터럴은 function 키워드, 함수 이름, 매개변수, 함수 몸체로 구성된다.
var f = function add (num1, num2) {
return num1 + num2;
}
12-4. 함수 정의
- 함수를 호출하기 이전에 인수를 전달받을 매개변수와 실행할 문들 그리고 반환할 값을 지정하는 것을 말한다.
- 정의된 함수는 자바스크립트 엔진에 의해 평가되어 함수 객체가 된다.
12-4-1. 함수 선언문
function add(num1, num2) {
return num1 + num2;
}
- 자바스크립트 엔진은 함수 선언문을 해석해 함수 객체를 생성한다.
- 또한, 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 거기에 함수 객체를 할당한다.
- 따라서, 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출한다.
12-4-2. 함수 표현식
var add = function add(num1, num2) {
return num1 + num2;
}
- 자바스크립트의 함수는 객체 타입의 값이다. 변수에 할당할 수도 있으며 프로퍼티의 값이 될 수도 있다.
- 이처럼 함수를 값처럼 자유롭게 사용할 수 있기에 함수는 일급 객체이다.
- 위에서 자바스크립트 엔진에 의해 식별자가 암묵적으로 생성된 것과 유사하다고 생각될 수 있다.
- 하지만, 함수 선언문은 표현식이 아닌 문이고 함수 표현식은 표현식인 문이다. 다음 코드를 보면,
console.log(add(1, 2));
console.log(sub(1, 2));
function add(num1, num2) {
return num1 + num2;
}
var sub = function(num1, num2) {
return num1 - num2;
}
3
"Uncaught TypeError: sub is not a function"
- 함수 선언문으로 정의한 함수는 정상적으로 호출되었고 함수 표현식으로 정의한 함수는 에러가 발생했다.
- 이는 함수 선언문과 함수 표현식으로 생성된 함수의 생성 시점이 다르기 때문이다.
- 즉, 함수 선언문으로 정의한 함수는 한 줄씩 실행되는 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행되어 함수 객체가 먼저 생성되고 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 객체를 할당한다.
- 이를 함수 호이스팅(function hoisting) 이라고 한다.
- 함수 표현식은 var 키워드를 사용한 변수 선언문과 동일하게 동작해 런타임 이전에 자바스크립트 엔진에 의해 식별자를 생성한다는 것은 함수 선언문과 동일하다.
- 하지만, var 키워드를 선언된 변수는 undefined로 초기화된다. 그 후 런타임에 평가되므로 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 된다.
- 즉, 변수 호이스팅(variable hoisting)이 발생한다.
✏️ 참고
- 함수 호이스팅은 함수를 호출하기 전 반드시 함수를 선언해야 한다는 당연한 규칙을 무시한다. 이 때문에, JSON을 창안한 더글라스 크락포트는 함수 표현식을 사용할 것을 권장한다.
12-4-3. Function 생성자 함수
var add = new Function('num1', 'num2', 'return num1 + num2');
console.log(add(1, 2));
3
- 자바스크립트가 기본적으로 제공하는 빌트인 함수인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환한다.
- new 연산자를 사용하지 않아도 결과는 같다.
- Function 생성자 함수로 함수를 생성하는 방식은 일반적이지 않으며 바람직하지도 않다.
- 클로저(Closure)를 생성하지 않는 등 함수 선언문, 함수 표현식과 다르게 동작한다.
12-4-4. 화살표 함수
const add = (num1, num2) => num1 + num2;
console.log(add(1, 2));
3
- ES6에서 도입된 화살표 함수(arrow function) 는 function 키워드 대신에 화살표 => 를 사용해 좀 더 간략한 방법으로 함수를 선언할 수 있다.
- 화살표 함수는 항상 익명함수로 정의한다.
익명함수(Anonymous function)
- 쉽게 말해, 함수명 대신 변수명에 함수 코드를 저장하는 구현 방식을 말한다.
- 화살표 함수는 기존의 함수 선언문 또는 함수 표현식을 완전히 대체 하는 것은 아니다.
- 기존의 함수보다 표현만 간략한 것이 아닌 내부 동작도 간략화되어 있기 때문이다.
12-5. 함수 호출
- 함수는 함수 호출 연산자를 통해 호출한다. 함수 호출 연산자는 0개 이상의 인수를 쉼표로 구분해서 나열한다.
- 함수를 호출하면 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴다.
12-5-1. 매개변수와 인수
- 함수를 실행하기 위해 필요한 값을 외부에서 내부로 전달할 필요가 있는 경우 매개변수(parameter)를 통해 인수(argument)를 전달한다.
- 매개변수는 함수 몸체 내부에서 변수와 동일하게 취급된다. 즉, 함수가 호출되면 암묵적으로 매개변수가 생성되고 undefined로 초기화 된 이후 인수가 할당된다.
- 인수는 값으로 평가될 수 있는 표현식이어야 하며 개수와 타입에 제한이 없다.
function add(num1, num2) {
console.log(num1, num2);
return num1 + num2;
}
add(2, 5);
console.log(num1, num2);
2, 5
"ReferenceError: num1 is not defined"
- 매개변수는 함수 내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다. 즉, 매개변수의 스코프는 함수 내부이다.
function add(num1, num2) {
return num1 + num2;
}
console.log(add(1));
console.log(add(1, 2, 3));
NaN
3
- 함수는 매개변수와 인수의 개수가 동일한지 체크하지 않는다.
- 매개변수보다 인수의 개수가 적어 인수가 할당되지 않은 매개변수의 값은 undefined이다. 위의 예에서는 num2에 undefined가 할당되고 1 + undefined 연산이 이루어져 NaN을 반환한다.
- 매개변수보다 인수의 개수가 많을 경우 초과된 인수는 무시된다.
✏️ 참고
- 인수는 함수 내부에 암묵적으로 arguments 객체의 프로퍼티로 보관된다.
function add(num1, num2) {
console.log(arguments);
return num1 + num2;
}
add(1, 2, 3);
12-5-2. 인수 확인
function add(num1, num2) {
return num1 + num2;
}
console.log(add(1));
console.log(add("1", "2"));
NaN
"12"
- 위 코드는 자바스크립트 문법상으로는 어떠한 문제도 없기 때문에 자바스크립트 엔진은 코드를 실행할 것이다.
- 이런 문제가 발생한 이유는 다음과 같다.
- 자바스크립트 함수는 매개변수와 인수의 개수가 일치하는지 확인하지 않는다.
- 자바스크립트는 동적 타입의 언어이다. 따라서, 매개변수의 타입을 미리 지정할 수 없다.
- 따라서, 자바스크립트의 경우 함수를 정의할 때 적절한 인수인지 확인해야 한다.
function add(num1, num2) {
if(typeof num1 !== "number" || typeof num2 !== "number") {
throw new TypeError("숫자 값이 아닌 인수가 존재합니다.");
}
return num1 + num2;
}
console.log(add(1));
console.log(add("1", "2"));
"Uncaught TypeError: 숫자 값이 아닌 인수가 존재합니다."
✏️ 참고
- 최근에는 타입스크립트(Typescript)와 같은 정적 타입을 선언할 수 있는 자바스크립트의 상위 확장을 도입해서 컴파일 시점에 부적절한 호출을 방지할 수 있다.
- 타입 외에 인수가 잘 안들어왔을 때에는 매개변수에 기본 값을 할당하거나 인수가 전달되지 않은 경우 단축 평가를 이용해 매개변수에 기본 값을 할당하는 방법이 있다.
function add(num1 = 0, num2 = 0) {
return num1 + num2;
}
function add(num1, num2) {
num1 = num1 || 0;
num2 = num2 || 0;
return num1 + num2;
}
12-5-3. 매개변수의 최대 개수
- ECMAScript 사양에서는 매개변수의 최대 개수에 대해 명시적으로는 제한하고 있지는 않지만 자바스크립트 엔진마다 물리적 한계는 존재한다.
- 매개 변수가 많으면 코드를 이해하는데 많이 방해가 되며 인수와 매개변수의 순서를 신경써야 하므로 매개변수의 개수는 적을수록 좋다.
- 매개변수를 많이 사용해야 상황이라면 하나의 매개변수를 선언하고 객체로 인수로 전달하는 것도 하나의 방법이다.
function add(num) {
return num.num1 + num.num2 + num.num3 + num.num4;
}
var sum = add({
num4: 4,
num2: 2,
num3: 3,
num1: 1,
});
console.log(sum);
10
- 이 방법은 jQuery의 ajax 메서드를 사용할 때 유용하게 쓰인다.
$.ajax({
method: "POST",
url: "/user",
data: {
id: 1,
name: "Lee",
job: "programmer",
address: "Seoul",
},
cache: false
});
- 객체를 인수로 사용할 경우 프로퍼티 키만 정확히 지정하면 매개변수의 순서를 신경쓰지 않아도 된다는 장점이 있다.
- 하지만, 함수 외부에서 함수 내부로 전달한 객체를 변경하면 함수 외부의 객체가 변경되는 부수효과가 발생하므로 주의해야 한다.
12-5-4. 반환문
- 함수는 return 키워드를 사용해 자바스크립트에서 사용 가능한 모든 값을 반환할 수 있다.
- 반환문은 두 가지 역할을 한다.
- 함수의 실행을 중단하고 몸체를 빠져나온다. 즉, 반환문 뒤의 다른 문들은 실행되지 않는다.
- return 키워드 뒤에오는 표현식을 평가해 반환한다.
- return 키워드에 표현식을 명시해주지 않으면 undefined를 반환한다.
function func() {
return;
}
console.log(func());
undefined
12-6. 참조에 의한 전달과 외부 상태의 변경
- 원시 값은 값에 의한 전달, 객체는 참조에 의한 전달 방식으로 동작한다. (자세한 내용은 원시 값과 객체 값을 참고하면 된다.)
- 매개변수도 함수 몸체 내부에서 변수와 동일하게 취급되므로 타입에 따라 다르게 동작한다.
function changeValue(primitive, object) {
primitive += 1;
object.name = "Kim";
}
var num = 1;
var person = { name: "Lee" };
console.log(num);
console.log(person);
console.log("---함수 호출---");
changeValue(num, person);
console.log(num);
console.log(person);
1
{name: 'Lee'}
"---함수 호출---"
1
{name: 'Kim'}
- changeValue 함수는 매개변수를 통해 전달받은 원시 타입 인수와 객체 타입 인수를 함수 내부 몸체에서 변경한다.
- 하지만, 결과를 보면 객체 타입인 person 변수는 값이 변했지만 원시 타입인 num 변수는 값이 변하지 않았다.
- 원시 값은 변경 불가능한 값이기 때문에 직접 변경할 수 없다. 따라서, 매개변수 primitive 같은 경우 재할당을 통해 새로운 원시 값으로 교체한다. 즉, 원본이 훼손되지 않는다.
- 하지만, 객체 타입 인수는 참조 값이 복사되어 매개변수에 전달하기 때문에 함수 몸체에서 참조 값을 통해 객체를 변경할 경우 원본이 훼손된다.
- 이러한 문제를 해결하는 방법 중 하나는 객체를 불변 객체(immutable object) 로 만들어 사용하는 것이다.
- 예를 들어, 객체의 상태 변경이 필요한 경우 깊은 복사(deep copy)를 통해 새로운 객체를 생성하고 재할당을 통해 교체한다.
12-7. 함수의 종류
12-7-1. 즉시 실행 함수
- 함수 정의와 동시에 즉시 실행되는 함수이다. 그룹 연산자 ( ) 를 사용하고 단 한번만 호출되며 다시 호출할 수 없다.
(function () {
var num1 = 1;
var num2 = 2;
return num1 + num2;
}());
- 즉시 실행 함수 는 함수 이름이 없는 익명 함수를 사용하는 것이 일반적이다. 함수 이름이 있는 기명 즉시 실행 함수도 사용할 수 있지만 그룹 연산자 내에 있는 함수로 평가되어 다시 호출할 수 는 없다.
(function add() {
var num1 = 1;
var num2 = 2;
return num1 + num2;
}());
add();
"ReferenceError: add is not defined"
- 즉시 실행 함수도 일반 함수처럼 값을 반환할 수 있고 인수를 전달할 수도 있다.
var add = (function (num1, num2) {
return num1 + num2;
}(3, 5));
console.log(add);
console.log(add + 3);
8
11
12-7-2. 재귀 함수
- 함수가 자기 자신을 호출하는 것을 재귀 호출(recursive call)이라 한다. 재귀 함수(recursive function) 는 재기 호출을 수행하는 함수를 말한다.
var sum = 0;
function add(n) {
if (n < 0) return;
sum += n;
add(n - 1);
}
add(5);
console.log(sum);
15
- 재귀 함수는 자신을 무한으로 호출하기 때문에 재귀 함수를 사용할 때에는 반드시 재귀 호출을 멈출 수 있는 탈출 조건이 필요하다. 탈출 조건이 없으면 스택 오버플로우(stack overflow) 에러가 발생한다.
12-7-3. 중첩 함수
- 함수 내부에 정의된 함수를 중첩 함수(nested function) 또는 내부 함수(inner function)이라고 한다. 그리고 중첩 함수를 포함하는 함수를(outer function)이라고 한다.
function outer() {
var x = 1;
function inner() {
var y = 2;
console.log(x + y);
}
inner();
}
outer();
12-7-4. 콜백 함수
- 조건에 맞는 모든 수를 출력하는 함수와 조건에 맞는 모든 수 중 홀수만 출력하는 함수가 있다.
function allNumberPrint(n) {
for(var i = 1; i < n; i++) {
console.log(i);
}
}
function oddNumberPrint(n) {
for(var i = 1; i < n; i++) {
if(i % 2 !== 0) {
console.log(i);
}
}
}
allNumberPrint(5);
console.log("------");
oddNumberPrint(5);
1
2
3
4
-----
1
3
- 위 두 함수는 반복하는 일은 변하지 않고 공통적으로 수행한다. 즉, 함수의 일부분만이 다르기 때문에 매번 함수를 정의해야하고 비효율적이다.
- 이 문제는 함수의 변하지 않는 공통 로직은 미리 정의해두고 변경되는 로직은 추상화해 함수 외부에서 내부로 전달하는 것으로 해결할 수 있다.
function print(n, callback) {
for(var i = 1; i < n; i++) {
callback(i);
}
}
var allNumber = function (i) {
console.log(i);
}
var oddNumber = function (i) {
if(i % 2 !== 0) {
console.log(i);
}
}
print(5, allNumber);
console.log("------");
print(5, oddNumber);
1
2
3
4
-----
1
3
- 위 print 함수는 경우에 따라 변경되는 일을 callback으로 추상화했고 이를 외부에서 전달받는다.
- 이처럼 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수(callback function) 라고 한다.
- 또한, 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수(HOF, Higher-Order function)라고 한다.
- 콜백 함수는 비동기 처리(이벤트 처리, ajax 통신, 타이머 함수 등)에 자주 활용된다.
document.getElementById("button").addEventListener("click", function () {
console.log("button click");
});
setTimeout(function () {
console.log("1초");
}, 1000);
- 뿐만 아니라, 배열 고차 함수에도 콜백함수가 자주 활용된다.
var result = [1, 2, 3].map(function (item) {
return item * 2;
});
console.log(result);
12-7-5. 순수 함수와 비순수 함수
- 함수형 프로그래밍에서는 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과가 없는 함수를 순수 함수(pure function) 라 하고, 외부 상태에 의존하거나 변경하는 함수를 비순수 함수(impure function) 라고 한다.
var count = 0;
function increase(n) {
return ++n;
}
increase(count);
console.log(count);
count = increase(count);
console.log(count);
0
1
- 순수 함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환하는 함수다.
- 즉, 순수 함수는 외부 상태에 전혀 의존하지 않고 매개변수로 전달된 인수에게만 의존해 반환값을 만든다.
var count = 0;
function increase(n) {
return ++count;
}
increase(count);
console.log(count);
count = increase(count);
console.log(count);
1
2
- 비순수 함수는 외부 상태를 직접 참조하기 때문에 외부 상태에 의존하게 되어 반환값이 변할 수 있다.
- 함수 내부에서 외부 상태를 직접 참조하지 않더라도 매개변수에 객체를 전달받으면 비순수 함수가 된다.
- 함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태의 변경을 최소화해서 불변성(Immutability)을 지향하는 프로그래밍 패러다임이다.
- 로직 내에 존재하는 조건문과 반복문을 제거해서 복잡성을 해결하며, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다.