함수는 필요할 때 여러번 반복할 수 있다. 즉 실행시점을 개발자가 결정할 수 있고 몇번이든 재사용이 가능하다.
동일한 작업을 반복해야 한다면 함수는 몇번이고 호출할 수 있으므로 코드의 재사용이라는 측면에서 매우 유용하다.
함수를 사용하지 않고 같은 코드를 중복해서 여러 번 작성하면 그 코드를 수정해야 할 때 중복된 횟수만큼 그 코드를 수정해야 한다. 그리고 이 과정에서 실수가 발생할 확률이 높아진다. 따라서 함수를 사용하면 코드의 중복을 억제하고 유지보수의 편의성을 높이고 실수를 줄여 코드의 신뢰성을 높이는 효과가 있다
함수는 객체 타입의 값이다. 따라서 이름(식별자)을 붙일 수 있다. 적절한 함수 이름은 함수의 내부 코드를 이해하지 않고도 함수의 역할을 파악할 수 있게 돕는다. 이는 코드의 가독성을 높인다.
function add () {
// ...생략
return;
}
var bada = function () {
// ...생략
return;
};
var add = new function ('x', 'y', 'return x+y');
var add = (x, y) => return x+y;
```
// 그룹 연산자의 피연산자는 값으로 평가될 수 있는 평가식이어야 한다.
// 따라서 bar는 함수 선언문으로 해석되지 않고 함수 리터럴 표현식으로 평가된다.
(function bar() { console.log('bar')})
bar(); // ReferenceError: bar is not defined
var add = () => {
// ...생략
return;
}
함수는 함수를 가리키는 식별자와 한 쌍의 소괄호인 함수 호출 연산자로 호출한다.
함수를 호출하면 현재의 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴다. 이때 매개변수에 인수가 순서대로 할당되고 함수 몸체의 문들이 실행되기 시작한다.
매개변수와 인수
인수 확인
function add(x, y) {
if (typeof x !== 'number' || typeof y !== 'number) {
// 매개변수를 통해 전달한 인수의 타입이 부적절한 경우 에러를 발생시 킨다.
throw new TypeError('인수는 모두 숫자 값이어야 합니다.');
}
return x + y;
}
console.log(add(2)); // 인수는 모두 숫자 값이어야 합니다.
console.log(add('2', '3')); // 인수는 모두 숫자 값이어야 합니다.
// 매개변수 primitive는 원시 값을 전달받고,
//매개변수 obj는 객체를 전달받는다.
function changeVal(ptimitive, obj) {
primitive += 100;
obj.name = 'kim';
}
// 외부 상태
var num = 100; // 100
var person = { name: 'Lee'}; // { name: 'Lee'}
console.log(num); // 100
console.log(person) // { name: 'Lee'}
changeVal(num, person);
// 원시 값은 원본이 훼손되지 않는다
console.log(num); // 100
// 객체는 원본이 훼손된다.
console.log(person) // { name: 'Kim'}
changeVal 함수는 매개변수를 통해 전달받은 원시 타입 인수와 객체 타입 인수를 함수 몸체에서 변경한다.
원시 값은 변경 불가능한 값이므로 직접 변경할 수 없기 때문에 재할당을 통해 할당된 원시 값을 새로운 원시 값으로 교체했고, 객체 타입 인수를 전달받은 매개변수 obj의 경우, 객체는 변경 가능한 값이므로 직접 변경할 수 있기 때문에 재할당 없이 직접 할당된 객체를 변경했다.
이때 원시 타입 인수는 원본이 훼손되지 않는다. 즉 함수 외부에서 함수 몸체 내부로 전달한 원시 값의 원본을 변경하는 어떠한 부수 효과도 발생하지 않는다.
객체 타입 인수는 함수 몸체에서 참조 값을 통해 객체를 변경할 경우 원본이 훼손된다. 즉 함수 외부에서 함수 몸체 내부로 전달한 참조 값에 의해 원본 객체가 변경되는 부수효과가 발생한다.
함수가 외부 상태(예제의 경우, 객체를 할당한 person 변수)를 변경하면 상태 변화를 추적하기 어려워진다. 이는 코드의 복잡성을 증가시키고 가독성을 해치는 원인이 된다.
객체의 변경을 추적하려면 옵저버 패턴 등을 통해 객체의 참조를 공유하는 모든 이들에게 변경 사실을 통지하고 이에 대처하는 추가 대응이 필요하다.
이러한 문제의 해결 방법 중 하나는 객체를 불변 객체로 만들어 사용하는 것이다. -> 객체를 마치 원시 값처럼 변경 불가능한 값으로 동작하게 만드는 것 -> 객체의 방어적 복사(깊은 복사)를 통해 외부 상태가 변경되는 부수 효과를 없앨 수 있다.
즉시 실행 함수
// 익명 즉시 실행 함수
(function () {
var a = 3;
var b = 5;
return a * b;
})
즉시 실행 함수는 함수 이름이 없는 익명 함수를 사용하는 것이 일반적이다.
기명 즉시 실행 함수도 사용할 수 있다. 그룹 연산자 내의 기명 함수는 함수 선언문이 아니라 함수 리터럴로 평가되며 함수 이름은 함수 몸체에서만 참조할 수 있는 식별자이므로 즉시 실행 함수를 다시 호출할 수는 없다.
// 기명 즉시 실행 함수
(function foo () {
var a = 3;
var b = 5;
return a * b;
})
foo() // ReferenceError: foo is not defined
익명 즉시 실행 함수를 그룹연산자로 감싸지 않으면 syntaxError: function statements require a function name 발생
기명 즉시 실행 함수를 그룹 연산자로 감싸지 않으면 syntaxError: Unexpected token ')' 발생 -> 원인: 함수 코드 블록의 닫는 중괄호 뒤에 ";"이 암묵적으로 추가되기 때문
그룹 연산자의 피연산자는 값으로 평가되므로 기명 또는 무명 함수를 그룹 연산자로 감싸면 함수 리터럴로 평가되어 함수 객체가 된다. 즉, 그룹 연산자로 함수를 묶은 이유는 먼저 함수 리터럴을 평가해서 함수 객체를 생성하기 위해서다. -> 따라서 함수 객체를 생성할 수 있는 다른 방법을 사용해도 된다
즉시 실행 함수에서 인수를 전달할 수 있다
var res = (function (a, b) {
return a*b;
}(3, 5));
console.log(res); // 15
재귀함수
// 함수 표현식
var factorial = function foo(n) {
// 탈출 조건: n이 1 이하일 때 재귀 호출을 멈춘다.
if (n <= 1) return 1;
// 함수를 가리키는 식별자로 자기 자신을 재귀 호출
return n * factorial(n - 1);
// 함수 이른으로 자기 자신을 재귀 호출할 수도 있다
// console.log(factorial === foo); // true
// return n * foo(n - 1);
};
console.log(factorial(5)); // 5! = 5 * 4 * 3 * 2 * 1 = 120
```
중첩 함수
콜백 함수
함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수라고 한다.
매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수라고 한다.
매개변수를 통해 함수를 전달받거나 반환값으로 함수를 반환하는 함수를 함수형 프로그래밍 패러다임에서 고차함수라 한다.
중첩 함수는 고정되어 있어서 교체하기 곤란하지만 콜백 함수는 함수 외부에서 주입하기 때문에 자유롭게 교체할 수 있다는 장점이 있다.
즉, 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다.
고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해서 호출한다. 다시 말해, 콜백 함수는 고차 함수에 의해 호출되며 이때 고차 함수는 필요에 따라 콜백 함수에 인수를 전달할 수 있다. 따라서 고차 함수에 콜백 함수를 전달할 때 콜백 함수를 호출하지 않고 함수 자체를 전달해야 한다.
// 익명 함수 리터럴을 콜백 함수로 고차 함수에 전달한다.
// 익명 함수 리터럴은 repeat 함수를 호출할 때마다 평가되어 함수 객체를 생성한다.
repeat(5, function (i) {
if (i % 2) console.log(i);
}) // 1 3
// logOdds 함수는 단 한 번만 생성된다.
var logOdds = function (i) {
if (i % 2) console.log(i);
};
// 고차 함수에 함수 참조를전달한다.
repeat(5, logOdds); // 1, 3
콜백 함수가 고차 함수 내부에만 호출된다면 콜백 함수를 익명 함수 리터럴로 정의하면서 곧바로 고차 함수에 전달하는 것이 일반적이다.
이때 콜백 함수로서 전달된 함수 리터럴은 고차 함수가 호출될 때마다 평가되어 함수 객체를 생성한다. 따라서 콜백 함수를 다른 곳에서도 호출할 필요가 있거나, 콜백 함수를 전달받는 함수가 자주 호출된다면 함수 외부에서 콜백 함수를 정의한 후 함수 참조를 고차 함수에 전달하는 편이 효율적이다.
위 예제의 logOdds 함수는 단 한 번만 생성된다. 하지만 콜백 함수를 익명 함수 리터럴로 정의 하면서 곧바로 고차 함수에 전달하면 고차 함수가 호출될 때마다 콜백 함수가 생성된다.
순수 함수와 비순수 함수
순수 함수: 어떤 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과가 없는 함수
비순수 함수: 외부 상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수. 함수 내부에서 외부 상태를 직접 참조하거나, 매개변수를 통해 객체를 전달받으면 비순수함수가 된다.
cf) 함수형 프로그래밍이란?
순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성을 지향하는 프로그래밍 패러다임. 로직 내에 존재하는 조건문과 반복문을 제거해서 복잡성을 해결하며, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다. 함수형 프로그래밍은 결국 순수 함수를 통해 부수 효과를 최대한 억제해 오류를 피하고 프로그램의 안정성을 높이려는 노력의 일환이다. 자바스크립트는 멀티 패러당미 언어이므로 객체지향 프로그래밍뿐만 아니라 함수형 프로그래밍을 적극적으로 활용하고 있다.