[모던 자바스크립트 Deep Dive] 12장. 함수

윤상준·2022년 11월 24일
0
post-thumbnail

12.1 함수란?

수학에서의 함수?
입력을 받아 출력을 내보내는 일련의 과정.

프로그래밍 언어에서의 함수
일련의 과정을 문 (Statement)으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것.

// f(x, y) = x + y
function add(x, y) {
  return x + y;
}

// f(2, 5) = 7
add(2, 5); // 7
  • 매개 변수 (Parameter) : 함수 내부로 입력을 전달 받는 변수.
  • 인수 (Argument) : 입력.
  • 반환값 (Retun Value) : 출력.

함수 정의 (Function Definition)

함수를 생성하는 행위.

// 함수 정의
function add(x, y) {
  return x + y;
}

함수 호출 (Functino Call/Invoke)

인수를 매개변수를 통해 함수에 전달하면서 함수의 실행을 명시적으로 지시하는 행위.

// 함수 호출
var result = add(2, 5);

// 함수 add에 인수 2, 5를 전달하면서 호출하면 반환값 7을 반환한다.
console.log(result); // 7

12.2 함수를 사용하는 이유

함수는 코드의 재사용 측면에서 매우 유리.

  • 유지보수의 편의성 향상.
  • 코드의 신뢰성 향상.

함수 이름

함수의 내부 코드를 이해하지 않아도 그 역할을 파악할 수 있도록 적절한 네이밍이 중요.

12.3 함수 리터럴

function 키워드, 함수 이름, 매개 변수 목록, 함수 몸체로 구성.

// 변수에 함수 리터럴을 할당
var f = function add(x, y) {
  return x + y;
};

구성 요소

함수 이름

  • 함수 이름은 식별자.
  • 생략 가능.
    • 이름이 있는 함수 : 기명 함수. (Named Function)
    • 이름이 없는 함수 : 무명/익명 함수. (Anonymous Function)

매개변수 목록

  • 함수 몸체 내에서 변수와 동일하게 취급.

함수 몸체

  • 함수 호출 시 일괄적으로 실행될 문들이 정의된 코드 블록.

함수는 객체 (Object)
일반 객체와 다르게 호출이 가능하다.

12.4 함수 정의

12.4.1 함수 선언문

// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 참조
// console.dir은 console.log와는 달리 함수 객체의 프로퍼티까지 출력한다.
// 단, Node.js 환경에서는 console.log와 같은 결과가 출력된다.
console.dir(add); // ƒ add(x, y)

// 함수 호출
console.log(add(2, 5)); // 7

함수 선언문은 함수 리터럴과는 다르게 함수 이름을 생략할 수 없다.

// 함수 선언문은 함수 이름을 생략할 수 없다.
function (x, y) {
  return x + y;
}
// SyntaxError: Function statements require a function name

함수 선언문은 표현식이 아닌 문이기 때문에 그냥 실행하면 undefined가 출력된다.

12.4.2 함수 표현식

일급 객체
값의 성질을 갖는 객체.

자바스크립트의 함수는 일급 객체이므로 값처럼 자유롭게 사용 가능.

따라서 함수 리터럴로 생성한 함수 객체는 변수에 할당 가능.

// 함수 표현식
var add = function (x, y) {
  return x + y;
};

console.log(add(2, 5)); // 7

함수 이름은 생략 가능하며 함수 표현식의 함수 리터럴은 함수 이름을 생략하는 것이 일반적.

// 기명 함수 표현식
var add = function foo (x, y) {
  return x + y;
};

// 함수 객체를 가리키는 식별자로 호출
console.log(add(2, 5)); // 7

// 함수 이름으로 호출하면 ReferenceError가 발생한다.
// 함수 이름은 함수 몸체 내부에서만 유효한 식별자다.
console.log(foo(2, 5)); // ReferenceError: foo is not defined

12.4.3 함수 생성 시점과 함수 호이스팅

함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수는 생성 시점이 다르다.

// 함수 참조
console.dir(add); // ƒ add(x, y)
console.dir(sub); // undefined

// 함수 호출
console.log(add(2, 5)); // 7
console.log(sub(2, 5)); // TypeError: sub is not a function

// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 표현식
var sub = function (x, y) {
  return x - y;
};

함수 선언문으로 정의한 함수

런타임 이전에 함수 객체가 먼저 생성.

자바스크립트 엔진은 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 함수 객체를 할당.

따라서 런타임에는 이미 함수 객체가 생성되었고 이름이 동일한 식별자에 할당까지 완료된 상황.

함수 선언문은 함수 호이스팅이 발생한다.

함수 표현식으로 정의한 함수

함수 리터럴이 변수에 할당되므로 변수 선언문과 동일하게 동작.

변수 선언은 런타임 이전에 실행되어 undefined로 초기화.

변수 할당문의 값은 할당문이 실행되는 런타임 시점에 평가.

따라서 함수 리터럴도 할당문이 실행되는 런타임 시점에 평가되어 함수 객체가 된다.

함수 표현식은 변수 호이스팅이 발생한다.

12.4.4 Function 생성자 함수

Function 생성자 함수에 new 연산자와 매개 변수 목록, 함수 몸체를 전달하여 함수 객체를 생성할 수 있다.

new 연산자는 생략 가능.

var add = new Function('x', 'y', 'return x + y');

console.log(add(2, 5)); // 7

하지만 이러한 방식은 함수 선언문, 함수 표현식으로 생성한 함수와 다르게 동작하므로 권장하지 않는다.

12.4.5 화살표 함수

ES6부터 등장.

function 키워드 대신 화살표를 통해 더 간략하게 함수 선언 가능.

항상 익명 함수로 정의.

// 화살표 함수
const add = (x, y) => x + y;
console.log(add(2, 5)); // 7

12.5 함수 호출

함수 호출은 함수 식별자와 함수 호출 연산자로 호출한다.

함수 호출 시 현재 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴 후, 매개 변수 인수가 순차적으로 할당되고, 함수 몸체의 문들이 실행.

12.5.1 매개변수와 인수

함수 실행에 필요한 값을 함수 외부에서 전달할 경우 매개변수 (Parameter)를 통해 인수 (Argument)를 전달.

// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 호출
// 인수 1과 2는 매개변수 x와 y에 순서대로 할당되고 함수 몸체의 문들이 실행된다.
var result = add(1, 2);

함수가 호출되면 함수 몸체 내에서 암묵적으로 매개변수가 생성.

일반 변수와 마찬가지로 undefined로 초기화되고 인수가 순서대로 할당.

매개 변수의 스코프는 함수 내부에 한정.

function add(x, y) {
  console.log(x, y); // 2 5
  return x + y;
}

add(2, 5);

// add 함수의 매개변수 x, y는 함수 몸체 내부에서만 참조할 수 있다.
console.log(x, y); // ReferenceError: x is not defined

인수가 할당되지 않은 매개변수의 값은 undefined.

function add(x, y) {
  return x + y;
}

console.log(add(2)); // NaN

매개변수보다 인수가 더 많을 경우 arguments 객체의 프로퍼티로 보관.

function add(x, y) {
  console.log(arguments);
  // Arguments(3) [2, 5, 10, callee: ƒ, Symbol(Symbol.iterator): ƒ]

  return x + y;
}

add(2, 5, 10);

12.5.2 인수 확인

  • 자바스크립트 함수는 매개변수와 인수의 개수 일치 여부를 확인하지 않는다.
  • 자바스크립트는 동적 타입 언어이므로, 함수는 매개변수의 타입을 사전에 지정할 수 없다.

따라서 다음의 함수는 문제 없이 실행되어 예상과 다른 값을 반환.

function add(x, y) {
  return x + y;
}

console.log(add(2));        // NaN
console.log(add('a', 'b')); // 'ab'

자바스크립트의 경우 다음과 같이 확인을 해주는 것이 좋다.

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에서부터 도입된 매개변수 기본값을 사용하면 정확성을 더 챙길 수 있다.

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

함수 외부에서 객체를 전달할 경우, 함수 내부에서 그 객체를 변경하면 원본도 같이 변경된다는 점을 주의해야 한다.

12.5.3 매개변수의 최대 개수

이상적인 함수는 한 가지 일만 해야하며 가급적 작게 만들어야 한다.

12.5.4 반환문

반화문을 통해 실행 결과를 함수 외부로 반환할 수 있다.

function multiply(x, y) {
  return x * y; // 반환문
}

// 함수 호출은 반환값으로 평가된다.
var result = multiply(3, 5);
console.log(result); // 15
  1. 함수 반환문은 함수 실행을 중단하고 빠져나간다.
function multiply(x, y) {
  return x * y; // 반환문
  // 반환문 이후에 다른 문이 존재하면 그 문은 실행되지 않고 무시된다.
  console.log('실행되지 않는다.');
}

console.log(multiply(3, 5)); // 15
  1. return 키워드 뒤에 오는 표현식을 평가해 반환한다.
function foo () {
  return;
}

console.log(foo()); // undefined
function foo () {
  // 반환문을 생략하면 암묵적으로 undefined가 반환된다.
}

console.log(foo()); // undefined
function multiply(x, y) {
  // return 키워드와 반환값 사이에 줄바꿈이 있으면
  return // 세미콜론 자동 삽입 기능(ASI)에 의해 세미콜론이 추가된다.
  x * y; // 무시된다.
}

console.log(multiply(3, 5)); // undefined

반환문은 함수 몸체 내부에서만 사용 가능.

전역에서 사용 시 문법 에러가 발생.

<!DOCTYPE html>
<html>
<body>
  <script>
    return; // SyntaxError: Illegal return statement
  </script>
</body>
</html>

12.6 참조에 의한 전달과 외부 상태의 변경

원시 타입의 인수는 값 자체가 복사되어 매개변수로 전달.

함수 내부에서 변경해도 원본에는 영향이 없다.

참조 타입의 인수는 참조 값이 복사되어 매개변수로 전달.

함수 내부에서 변경하면 참조 값에 의해 원본이 변경된다.

객체를 불변 객체 (Immutable Object)로 만들면 비용은 들지만 원시 값처럼 변경 불가능한 값으로 동작하게 할 수 있다.

객체의 방어적 복사 (Defensive Copy)
깊은 복사 (Deep Copy)를 통해 원본 객체와 같은 새로운 객체를 생성하고 재할당을 통해 교체.

함수형 프로그래밍
순수 함수를 통해 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 패러다임.

12.7 다양한 함수의 형태

12.7.1 즉시 실행 함수

함수 정의와 동시에 즉시 호출되는 함수

// 익명 즉시 실행 함수
(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

그룹 연산자로 감싸야 한다.

function () { // SyntaxError: Function statements require a function name
  // ...
}();
// 즉시 실행 함수도 일반 함수처럼 값을 반환할 수 있다.
var res = (function () {
  var a = 3;
  var b = 5;
  return a * b;
}());

console.log(res); // 15

// 즉시 실행 함수에도 일반 함수처럼 인수를 전달할 수 있다.
res = (function (a, b) {
  return a * b;
}(3, 5));

console.log(res); // 15

12.7.2 재귀 함수

자기 자신을 호출하는 재귀 호출을 수행하는 함수.

function countdown(n) {
  if (n < 0) return;
  console.log(n);
  countdown(n - 1); // 재귀 호출
}

countdown(10);
// 팩토리얼(계승)은 1부터 자신까지의 모든 양의 정수의 곱이다.
// n! = 1 * 2 * ... * (n-1) * n
function factorial(n) {
  // 탈출 조건: n이 1 이하일 때 재귀 호출을 멈춘다.
  if (n <= 1) return 1;
  // 재귀 호출
  return n * factorial(n - 1);
}

console.log(factorial(0)); // 0! = 1
console.log(factorial(1)); // 1! = 1
console.log(factorial(2)); // 2! = 2 * 1 = 2
console.log(factorial(3)); // 3! = 3 * 2 * 1 = 6
console.log(factorial(4)); // 4! = 4 * 3 * 2 * 1 = 24
console.log(factorial(5)); // 5! = 5 * 4 * 3 * 2 * 1 = 120

함수 이름은 함수 몸체 내부에서만 유효하기 때문에, 함수 내부에서 자기 자신의 이름을 사용해서 자기 자신을 호출할 수 있다.

// 함수 표현식
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

재귀 함수는 탈출 조건을 반드시 만들어야 한다.

그렇지 않으면 무한 실행되어 스택 오버플로우 에러가 발생한다.

function factorial(n) {
  if (n <= 1) return 1;

  var res = n;
  while (--n) res *= n;
  return res;
}

console.log(factorial(0)); // 0! = 1
console.log(factorial(1)); // 1! = 1
console.log(factorial(2)); // 2! = 2 * 1 = 2
console.log(factorial(3)); // 3! = 3 * 2 * 1 = 6
console.log(factorial(4)); // 4! = 4 * 3 * 2 * 1 = 24
console.log(factorial(5)); // 5! = 5 * 4 * 3 * 2 * 1 = 120

12.7.3 중첩 함수

중첩 함수 (Nested Function) / 내부 함수 (Inner Function)
함수 내부에 정의된 함수.

외부 함수 (Outer Function)
중첩 함수를 포함하는 함수.

function outer() {
  var x = 1;

  // 중첩 함수
  function inner() {
    var y = 2;
    // 외부 함수의 변수를 참조할 수 있다.
    console.log(x + y); // 3
  }

  inner();
}

outer();

12.7.4 콜백 함수

콜백 함수 (Callback Function)
함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수.

// 외부에서 전달받은 f를 n만큼 반복 호출한다.
function repeat(n, f) {
  for (var i = 0; i < n; i++) {
    f(i); // i를 전달하면서 f를 호출
  }
}

var logAll = function (i) {
  console.log(i);
};

// 반복 호출할 함수를 인수로 전달한다.
repeat(5, logAll); // 0 1 2 3 4

var logOdds = function (i) {
  if (i % 2) console.log(i);
};

// 반복 호출할 함수를 인수로 전달한다.
repeat(5, logOdds); // 1 3

고차 함수 (High-Order Function, HOF)
매개변수를 통해 함수 외부에서 콜백 함수를 전달받은 함수.

고차 함수는 콜백 함수를 자신의 일부분으로 합성.

// 익명 함수 리터럴을 콜백 함수로 고차 함수에 전달한다.
// 익명 함수 리터럴은 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
// 콜백 함수를 사용한 이벤트 처리
// myButton 버튼을 클릭하면 콜백 함수를 실행한다.
document.getElementById('myButton').addEventListener('click', function () {
  console.log('button clicked!');
});

// 콜백 함수를 사용한 비동기 처리
// 1초 후에 메시지를 출력한다.
setTimeout(function () {
  console.log('1초 경과');
}, 1000);

콜백 함수는 배열 고차 함수에서도 사용된다.

// 콜백 함수를 사용하는 고차 함수 map
var res = [1, 2, 3].map(function (item) {
  return item * 2;
});

console.log(res); // [2, 4, 6]

// 콜백 함수를 사용하는 고차 함수 filter
res = [1, 2, 3].filter(function (item) {
  return item % 2;
});

console.log(res); // [1, 3]

// 콜백 함수를 사용하는 고차 함수 reduce
res = [1, 2, 3].reduce(function (acc, cur) {
  return acc + cur;
}, 0);

console.log(res); // 6

12.7.5 순수 함수와 비순수 함수

순수 함수 (Pure Function)
부수 효과가 없는 함수.
어떤 외부 상태에도 의존하지 않으며 외부 상태를 변경하지도 않는 함수.

비순수 함수 (Impure Function)
부수효과가 있는 함수.

var count = 0; // 현재 카운트를 나타내는 상태

// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
function increase(n) {
  return ++n;
}

// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count);
console.log(count); // 1

count = increase(count);
console.log(count); // 2

함수가 외부 상태를 변경하면 상태 변화 추적이 어려워지므로 순수 함수를 사용하는 것이 좋다.

함수형 프로그래밍은 순수 함수를 통해 부수 효과를 최대한 억제하여 프로그램의 안정성을 높이려는 노력의 일환.

profile
하고싶은건 많은데 시간이 없다!

0개의 댓글