[JS] 모던 Javascript Deep Dive 12장 - 함수
업데이트:
함수는 자바스크립트에서 가장 중요한 개념이다. 이번 장은 자세히 짚고 넘어가보도록 하자.
[01] 함수
함수란?
실제로 수학에서 다루는 함수는 입력과 출력이 존재하는 식을 말한다.
식 f(x, y) = x+y를 자바스크립트로 표현해보면 아래 코드와 같다.
//함수의 정의(함수 선언문)
function add(x, y){
return x+y;
}
//함수 호출
add(2,5); // 7
프로그래밍 언어에서 함수의 구조와 명칭은 이러하다.
함수는 일급객체이기 때문에 값으로 사용되어 변수나 객체, 배열에 저장할 수 있고, 인수, 반환값으로 쓰이기도 한다.
객체 생성, 메소드, 정보 은닉, 클로저, 모듈화 등의 기능을 수행할 수 있다.
함수의 사용이유
🥕 함수는 호출에 의해 실행되고 필요할 때 여러번 호출할 수 있어 동일한 작업을 반복적으로 수행해야 할 때 코드의 재사용을 할 수 있어 유용하다.
🥕 코드의 재사용성을 높여 유지보수의 편의성을 높이고 코드의 신뢰성을 높이는 효과가 있다.
🥕 함수에는 이름을 붙일 수 있어 함수 내부 코드를 다 파악하지 않아도 함수의 역할을 알 수 있다. 이는 코드의 가독성을 향상시킨다.
함수 리터럴
함수는 객체 타입의 값이고, 함수 리터럴로 생성할 수 있다.
🔍 함수 리터럴 구성 요소
일반 객체는 호출 할 수 없지만 함수는 호출할 수 있다.
[02] 함수 정의 방식
함수 선언문
함수 선언문의 경우, 함수명은 재귀적으로 호출하거나 자바스크립트 디버거가 해당 함수를 구분할 수 있는 식별자이기 때문에 생략할 수 없다.
//기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석(이름 생략 불가)
function foo(){console.log('foo')};
//호출 가능
foo();//foo
//함수 리터럴을 괄호안에 넣어 피연산자로 사용하면 함수 리터럴 표현식으로 해석
(function me(){console.log('minhye')});
//호출 불가능
me(); // Uncaught ReferenceError: me is not defined
이름이 있는 함수를 기명함수라 하는데, 자바스크립트 엔진은 문맥에 따라 이 기명함수를 함수 선언문으로 해석하기도 함수 리터럴 표현식으로 해석하기도 한다.
위 코드에서 foo()
는 function 키워드로 선언된 함수 선언문이고 호출할 수 있다. 하지만 함수 선언문은 표현식이 아닌 문이다. 즉, 변수에 할당은 불가능하다.
위 코드에서 me()
는 평가 가능한 함수 리터럴 표현식이다. 함수 선언문은 아니기 때문에 me 식별자는 사실 생략이 가능하고 함수 몸체내에서만 참조할 수 있다. 그러므로 식별자 me로 함수를 호출할 수 없다.
🔍 함수 선언문의 암묵적 해석
함수 이름의 특징에 따르면, foo() 함수도 몸체 내부에서만 유효한 식별자인데 어떻게 외부에서 호출이 가능할까?
사실 자바스립트 엔진은 함수를 호출하기 위해 함수 선언문을 만나면 암묵적으로 식별자를 생성해 함수 객체를 할당한다.
이러한 암묵적 해석 때문에 함수 표현식과 같아보이지만 내부적으로 정확히 동일하게 동작하는 것은 아니라고 한다.
결국 함수 선언문도 함수 표현식과 동일하게 함수 리터럴 방식으로 정의되는 것이다.
함수 표현식
함수 표현식을 사용하면 함수를 값처럼 자유롭게 사용할 수 있다.
함수 표현식에서는 함수명을 생략하는 것이 일반적이다.
🔍 일급객체 함수의 특징
- 무명의 리터럴로 표현이 가능하다.
- 변수나 자료 구조(객체, 배열…)에 저장할 수 있다.
- 함수의 파라미터로 전달할 수 있다.
- 반환값(return value)으로 사용할 수 있다.
// 함수 표현식
var square = function(number) {
return number * number;
};
// 기명 함수
var foo = function multiply(a, b) {
return a * b;
};
console.log(foo(10, 5)); // 50
console.log(multiply(10, 5)); // Uncaught ReferenceError: multiply is not defined
함수 호출시 함수명이 아니라 함수를 가리키는 변수명을 사용하여야 한다.
함수 표현식은 표현식인 문이다.
Function 생성자 함수
함수 선언문과 함수 표현식은 모두 함수 리터럴 방식으로 함수를 정의하는데 이것은 결국 내장 함수 Function 생성자 함수로 함수를 생성하는 것을 단순화시킨 short-hand(축약법)이다.
new Function(arg1, arg2, ... argN, functionBody)
var square = new Function('number', 'return number * number');
console.log(square(10)); // 100
거의 사용하지 않는다고 하니 넘어가도록 하겠다.
화살표 함수
ES6에서 도입된 함수 정의 방식이다. 문법은 아래와 같다.
const add = (x, y) => x + y;
console.log(add(2,5)); // 7
화살표 함수는 26장에서 더 자세히 다룰 예정이다.
[03] 함수 생성 시점과 함수 호이스팅
var res = square(5);
function square(number) {
return number * number;
}
위 코드를 보면 함수 생성 이전에도 호출이 가능하다. 이것은 변수 호이스팅처럼 함수는 function 키워드를 만나면 호이스팅한다.
하지만 함수 표현식으로 함수를 작성했을 때는 과연 어떻게 될까?
var res = square(5); // TypeError: square is not a function
var square = function(number) {
return number * number;
}
위 코드와 같이 타입 에러가 발생한다. 여기서는 함수 호이스팅이 아닌 변수 호이스팅만 일어났기 때문에 변수에 square는 undefined로 초기화 되어 있고 실제 함수의 할당은 런타임에 이루어진다.
함수 선언문으로 함수를 정의하면 사용하기에 쉽지만 대규모 애플리케이션을 개발하는 경우 애플리케이션의 응답속도는 현저히 떨어질 수 있으므로 함수 표현식 사용을 권장한다고 한다.
[04] 함수 호출
매개변수와 인수
함수를 호출하면 매개변수에 인수가 할당되고 함수 몸체의 문들이 실행된다.
매개변수는 함수 몸체 내부에서 변수와 동일하게 암묵적으로 생성되고 undefined로 초기화된다. 그러므로 만약 인수에 아무것도 들어있지 않다면 매개변수에 undefined가 들어있을 것이다.
function add(x,y){
return x+y;
}
console.log(add(2)); //NaN
// y에 인수가 전달되지 않아 2 + undefined로 해석되어 NaN을 반환
console.log(add(2,5,10)); // 7
//초과되어 전달된 인수인 10은 무시되고 arguments객체의 프로퍼티로 보관됨
인수 타입 확인
자바스크립트 함수는 매개변수의 타입을 사전에 지정할 수 없다.
그리고 매개변수의 개수와 인수의 개수가 일치하는지 확인하지도 않는다.
이러한 부작용을 막기 위해 적절한 인수가 전달되었는지 코드를 추가하여 확인해주어야 한다.
🥕 인수의 타입 확인 추가 코드
function add(x, y) {
if (typeof x !== 'number' || typeof y !== 'number') {
// 매개변수를 통해 전달된 인수의 타입이 부적절한 경우 에러를 발생시킨다.
throw new TypeError('인수는 모두 숫자 값이어야 합니다.');
}
return x + y;
}
console.log(add(2)); // TypeError: 인수는 모두 숫자 값이어야 합니다.
console.log(add('a', 'b')); // TypeError: 인수는 모두 숫자 값이어야 합니다.
🥕 ES6에서 도입된 매개변수 기본값 사용
// ES6에서 도입된 매개변수 기본값을 사용
function add(a = 0, b = 0, c = 0) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0
매개변수의 최대 개수
매개변수의 이상적 개수는 0개이고 최대 3개 이상을 넘지 않는 것을 권장한다.
이상적인 함수는 한가지 일만 해야하고 가급적 작게 만들어야 한다고 한다.
반환문
🔍 반환문의 역할 두가지
- 함수의 실행을 중단하고 함수 몸체를 빠져나감
- return 키워드 뒤에 오는 표현식을 평가해 반환
만약 return 키워드 두에 반환문이 지정되어 있지 않으면 undefined가 반환된다.
return 문 뒤에 세미콜론이 없어도 자동 삽입 기능에 의해 세미콜론이 추가된다.
function calculateArea(width, height) {
var area = width * height;
return area; // 단일 값의 반환
}
console.log(calculateArea(3, 5)); // 15
console.log(calculateArea(8, 5)); // 40
function getSize(width, height, depth) {
var area = width * height;
var volume = width * height * depth;
return [area, volume]; // 복수 값의 반환
}
console.log('area is ' + getSize(3, 2, 3)[0]); // area is 6
console.log('volume is ' + getSize(3, 2, 3)[1]); // volume is 18
반환문은 단일값도 반환되지만 복수값도 인덱스를 통해 반환 가능하다.
[05] 참조에 의한 전달과 외부 상태의 변경
함수에 전달되는 인수는 원시값일 수도 있고 객체 값일수도 있다.
이런 경우에 원시값은 함수 외부에서 값을 변경해도 상관없지만 객체 값은 함수 외부에서 함수 몸체 내부로 전달한 참조값에 의해 원본 객체가 변경되는 부수 효과가 발생할 수 있다.
function changeVal(primitive, obj) {
primitive += 100;
obj.name = 'Kim';
obj.gender = 'female';
}
var num = 100;
var obj = {
name: 'Lee',
gender: 'male'
};
console.log(num); // 100
console.log(obj); // Object {name: 'Lee', gender: 'male'}
changeVal(num, obj);
console.log(num); // 100
console.log(obj); // Object {name: 'Kim', gender: 'female'}
이렇게 말이다. 이러한 부수 효과가 발생하는 함수를 비순수 함수라고 한다.
비순수 함수를 최대한 줄이는 것이 프로그램에 안정성을 높이는데 도움이 된다.
결국, 어떤 외부 상태도 변경하지 않는 함수인 순수함수를 사용하는 것이 좋다.
뒤에서 순수함수와 비순수함수의 내용도 다뤄볼 것이다.
[06] 다양한 함수의 형태
즉시 실행 함수
함수의 정의와 동시에 실행되는 함수이다.
한번만 호출되고 재호출은 불가능하다. 초기화 처리에 사용하면 좋다.
// 기명 즉시 실행 함수(named immediately-invoked function expression)
(function myFunction() {
var a = 3;
var b = 5;
return a * b;
}());
// 익명 즉시 실행 함수(immediately-invoked function expression)
(function () {
var a = 3;
var b = 5;
return a * b;
}());
// SyntaxError: Unexpected token (
// 함수선언문은 자바스크립트 엔진에 의해 함수 몸체를 닫는 중괄호 뒤에 ;가 자동 추가된다.
function () {
// ...
}(); // => };();
// 따라서 즉시 실행 함수는 소괄호로 감싸준다.
(function () {
// ...
}());
(function () {
// ...
})();
즉시 실행 함수 내에 처리 로직을 모아 두면 혹시 있을 수도 있는 변수명 또는 함수명의 충돌을 방지할 수 있어 이를 위한 목적으로 즉시실행함수를 사용되기도 한다.
내부 함수(중첩 함수)
함수 내부에 정의된 함수
function parent(param) {
var parentVar = param;
function child() {
var childVar = 'lee';
console.log(parentVar + ' ' + childVar); // Hello lee
}
child();
console.log(parentVar + ' ' + childVar);
// Uncaught ReferenceError: childVar is not defined
}
parent('Hello');
부모함수는 자식함수(내부함수)의 변수에 접근할 수 없고 내부함수는 부모함수의 외부에서 접근할 수 없다.
말이 너무 복잡하니 코드를 통해 이해해 보겠다.
function parent(param) {
var parentVar = param;
function child() {
var childVar = 'lee';
console.log(parentVar + ' ' + childVar); // Hello lee
}
child();
//부모함수는 자식함수의 변수에 접근 불가
console.log(parentVar + ' ' + childVar);
// Uncaught ReferenceError: childVar is not defined
}
parent('Hello');
function sayHello(name){
var text = 'Hello ' + name;
var logHello = function(){ console.log(text); }
logHello();
}
sayHello('lee'); // Hello lee
// 내부함수 logHello의 부모함수 외부에서 접근 불가
logHello('lee'); // logHello is not defined
재귀 함수
자기 자신을 호출하는 함수. 알고리즘 문제로도 많이 등장한다.
재귀함수를 잘 설명하는 대표적인 예시로 피보나치 수열, 팩토리얼 등이 있다.
// 피보나치 수열
// 피보나치 수는 0과 1로 시작하며, 다음 피보나치 수는 바로 앞의 두 피보나치 수의 합이 된다.
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, ...
function fibonacci(n) {
if (n < 2) return n; // 탈출 조건
return fibonacci(n - 1) + fibonacci(n - 2);// 재귀함수
}
console.log(fibonacci(0)); // 0
console.log(fibonacci(1)); // 1
console.log(fibonacci(2)); // 1
console.log(fibonacci(3)); // 2
console.log(fibonacci(4)); // 3
console.log(fibonacci(5)); // 5
console.log(fibonacci(6)); // 8
// 팩토리얼
// 팩토리얼(계승)은 1부터 자신까지의 모든 양의 정수의 곱이다.
// n! = 1 * 2 * ... * (n-1) * n
function factorial(n) {
if (n < 2) return 1; //탈출 조건
return factorial(n - 1) * n; // 재귀함수
}
console.log(factorial(0)); // 1
console.log(factorial(1)); // 1
console.log(factorial(2)); // 2
console.log(factorial(3)); // 6
console.log(factorial(4)); // 24
console.log(factorial(5)); // 120
console.log(factorial(6)); // 720
재귀 함수는 반복 연산을 간단히 구현할 수 있다는 장점이 있지만 무한 반복에 빠질 수 있고, stackoverflow 에러를 발생시킬 수 있으므로 주의하여야 한다.
반복문보다 재귀 함수를 통해 보다 직관적으로 이해하기 쉬운 구현이 가능한 경우에만 한정적으로 적용하는 것이 바람직하다.
콜백 함수
특정 이벤트가 발생했을 때 시스템에 의해 호출되는 함수
대표적인 예 : 이벤트 핸들러 처리
<!DOCTYPE html>
<html>
<body>
<button id="myButton">Click me</button>
<script>
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log('button clicked!');
});
</script>
</body>
</html>
댓글남기기