Web/모던 자바스크립트 Deep Dive

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

동띵 2023. 8. 29. 14:03

함수란?


  • 일련의 과정을 **문**으로 구현하고 **코드 블록**으로 감싸서 **하나의 실행 단위**로 정의한 것
  • 입력을 전달받는 변수 : 매개변수
  • 입력 : 인수
  • 출력 : 반환값
  • 함수는 함수 정의를 통해 생성됨
    • 정의만으로 실행되는 것은 아님 → **함수 호출**을 해야됨

함수를 사용하는 이유


  • **재사용** 가능
  • 함수 재사용 시 유지보수의 편의성을 높이고 코드의 신뢰성을 높임
  • 함수는 객체 타입의 값이라 이름(식별자)을 붙일 수 있음 ⇒ 코드의 가독성 형상

함수 리터럴


  • 함수는 객체 타입의 값이라 함수 리터럴을 통해 생성 가능
  • 함수 리터럴 : **function** 키워드, **함수 이름**, **매개변수** 목록, **함수** 몸체로 구성
    • 함수 이름
      • 식별자 (식별자 네이밍 규칙에 준수)
      • 이름 생략 가능 (기명 함수/익명, 무명 함수로 나뉨)
    • 매개변수 목록
      • 소괄호로 감싸고 쉼표로 구분
      • 함수 호출 시 지정한 인수가 **순서대로** 할당됨
      • 식별자 네이밍 규칙 준수
    • 함수 몸체
      • 함수 호출 시 일괄적으로 실행될 문들을 **하나의 실행 단위**로 정의한 코드 블록
      • 함수 몸체는 함수 호출에 의해 실행됨
  • 함수는 객체지만 일반 객체와 다른 점 : 호출 가능하고, 함수 객체만의 고유한 프로퍼티를 갖고 있음

함수 정의


  • 함수 정의 : 함수 호출 전 **매개변수**와 **실행할 문**들, **반환 값**을 지정하는 것
  • 함수를 정의하는 방법
    • 함수 선언문 function add(x, y) { return x+y; }
    • 함수 표현식 var add = function (x, y) { return x+y; }
    • Function 생성자 함수 var add = new Function('x', 'y', 'return x+y');
    • 화살표 함수 (ES6) var add = (x,y) => x+y;

함수 선언문

  • 함수 선언문은 함수 리터럴과 형태가 동일 but **함수 이름 생략 X** (함수 리터럴은 함수 이름 생략 가능)
    • SyntaxError 발생 function (x,y) { return x+y; }
  • 함수 선언문은 표현식이 아닌 문 ⇒ 함수 실행 시 크롬 콘솔에 완료 값 undefined
  • 함수 선언문은 표현식이 아닌 문이라 변수에 할당 불가능 but 코드 문맥에 따라 기명 함수 리터럴이 함수 선언문이나 함수 리터럴 표현식으로 해석될 수 있음 ⇒ 함수 선언문이 변수에 할당되는 것처럼 보이기도 함
  • 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성 후 함수 객체 할당
    • 함수는 함수 이름으로 호출하는 것이 아니라 **함수 객체를 가리키는 식별자로 호출** ⇒ 자바스크립트 엔진이 암묵적으로 생성한 식별자를 호출하는 것

⇒ 함수 선언문 : 표현식이 아닌 문 (변수에 할당 불가)

함수 표현식

  • 자바스크립트 함수는 일급 객체 ⇒ 함수를 **값처럼 자유롭게 사용** 가능
    • 일급 객체 : 값처럼 변수에 할당할 수 있고, 프로퍼티 값이 될 수 있으며 배열의 요소가 될 수 있는 값의 성질
  • 함수 리터럴로 생성한 **함수 객체를 변수에 할당** 가능 ⇒ 이러한 함수 정의 방식 : **함수 표현식**
// 함수 리터럴은 이름 생략 가능하여 아래같은 함수를 익명 함수라 함
var add = function(x, y) {
	return x+y;
}
  • 함수 표현식의 함수 리터럴은 함수 이름을 생략하는 것이 일반적
    • **기명 함수**를 사용해도 함수 이름은 함수 몸체 내부에서만 유효한 식별자이므로 **함수 이름으로 호출 X** (함수 객체를 가리키는 **식별자를 사용해 호출**해야 됨)
var add = function foo(x, y) {
	return x+y;
};

console.log(add(2,5)) // 7
console.log(foo(2,5)) // ReferenceError (함수 이름은 함수 내부에서만 호출 가능하여 참조 에러)

⇒ 함수 표현식 : 표현식인 문 (변수에 할당 가능)

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

  • **함수 선언문**으로 정의한 함수는 함수 선언문 **이전에 호출 가능**
  • **함수 표현식**으로 정의한 함수는 함수 표현식 **이전에 호출 X**

⇒ 함수 선언문과 표현식의 생성 시점이 다름

  • 함수 선언문이 코드 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징 : 함수 호이스팅
    • 변수 호이스팅은 undefined가 할당되어 변수 선언 전에 참조하면 undefined를 출력하지만, 함수 호이스팅은 함수 객체로 초기화되어 함수 선언 이전에 호출해도 사용 가능
    ⇒ 함수 선언문은 변수 선언과 할당을 한 번에 기술한 축약 표현과 동일하게 동작
// 함수 참조
console.dir(add); // f add(x,y)
console.dir(sub); // undefined

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

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

// 함수 표현식
var sub = function(x, y) {
	return x-y;
};
  • 함수 표현식으로 함수 정의 시 함수 호이스팅이 아닌 변수 호이스팅이 발생 ⇒ 함수 표현식 이전에 함수 참조 시 undefined로 평가 (이때 함수 호출하면 TypeError)

Function 생성자 함수

  • 자바스크립트가 기본 제공하는 빌트인 함수
  • 매개변수 목록과 함수 몸체를 **문자열**로 전달하면서 **new 연산자**와 함께 호출하면 함수 객체를 생성하여 반환 (new 연산자 없어도 결과는 동일)
var add = new Function('x', 'y', 'return x+y');
  • Function 생성자 함수로 생성한 함수는 **클로저를 생성하지 X** ⇒ 함수 선언문이나 함수 표현식으로 생성한 함수와 다르게 동작하여 일반적이지 않고 바람직하지 않음

화살표 함수

  • ES6에서 도입
  • function 키워드 대신 화살표를 사용해 좀 더 간략하게 함수 선언 가능
  • **항상 익명 함수**로 정의
const add = (x,y) => x+y;
  • 화살표 함수는 생성자 함수로 사용 X
  • 기존 함수와 this 바인딩 방식 다름
  • prototype 프로퍼티가 없으며 arguments 객체 생성 X

함수 호출


  • 함수는 함수를 가리키는 **식별자**와 소괄호인 **함수 호출 연산자**로 호출
    • 함수 호출 연산자 내에는 0개 이상의 인수를 쉼표로 구분해서 나열

매개변수와 인수

  • 함수 실행 시 필요한 값을 함수 **외부에서 내부로** 전달할 경우, 매개변수(인자)를 통해 인수 전달
    • 인수는 값으로 평가될 수 있는 표현식이어야 하며, 함수 호출 시 지정하고 개수와 타입에 제한 X
  • 매개변수는 함수 정의 시 선언하며, 함수 몸체 내부에서 **변수와 동일하게 취급**
    • 함수 호출 시 함수 몸체 내에서 암묵적으로 매개변수가 생성되고 undefined로 초기화된 이후 인수가 순서대로 할당됨
    • 매개변수의 스코프는 함수 내부여서 함수 외부에서 사용 X
  • 함수는 매개변수 개수와 인수의 개수가 일치하는지 체크 X ⇒ 인수가 부족해 할당되지 않은 매개변수는 undefined가 되어 에러 발생 X
    • 만약 매개변수보다 인수가 더 많을 경우 초과된 인수는 무시됨 (arguments 객체의 프로퍼티로 보관됨)

인수 확인

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

⇒ 함수 정의 시 적절한 인수가 전달되었는지 확인할 필요가 있음

  • 인수가 전달되지 않은 경우 **단축 평가**를 통해 매개변수에 기본값 할당 가능
function add(a, b, c) {
	a = a || 0;
	b = b || 0;
	c = c || 0;
	return a+b+c;
}
  • ES6의 경우 **매개변수 기본값**을 사용하면 함수 내에서 인수 체크 및 초기화를 간소화할 수 있음
    • 매개변수 기본값은 매개변수에 인수를 전달하지 않았을 경우와 undefined를 전달할 경우에만 유효
function add(a=0, b=0, c=0) {
	return a+b+c;
}

매개변수의 최대 개수

  • 매개변수의 최대 개수는 정해져있지 않지만 **매개변수의 순서에는 의미가 있어** 개수가 많아진다면 그만큼 **유지보수성**이 나빠짐
  • 매개변수는 최대 3개 이상을 넘지 않는 것을 권장
    • 그 이상의 매개변수가 필요하다면 **객체를 인수로 전달**
      • 객체를 인수로 전달 시 프로퍼티 키만 정확히 지정하면 **매개변수 순서 신경 X**
      • but 함수 외부에서 내부로 전달한 객체를 함수 내부에서 변경하면 함수 외부 객체가 변경되는 **부수 효과 (side effect) 발생**

반환문

  • **return 키워드**와 **반환값**으로 이뤄진 반환문을 사용해 실행 결과를 함수 외부로 반환 가능
function mul(x, y) {
	return x*y; // 반환문
}
// 함수 호출은 반환값으로 평가
const result = mul(3, 5)

⇒ 함수 호출은 표현식임

  • 반환문 역할
    • 함수의 실행을 중단하고 함수 몸체를 빠져나감
    • return 키워드 뒤에 오는 표현식을 평가해 반환 (명시적으로 지정하지 않으면 undefined 반환 ⇒ 즉, 생략 가능)
  • 반환문은 함수 몸체 내부에서만 사용 가능

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


  • 원시 값은 값에 의한 전달 / 객체 값은 참조에 의한 전달 방식으로 동작함
  • 매개변수도 함수 몸체 내부에서 변수와 동일하게 취급하므로 타입에 따라 값에 의한 전달, 참조에 의한 전달 방식을 따름 ⇒ 값에 의한 호출, 참조에의한 호출
function changeVal(primitive, obj) {
	primitive += 10;
	obj.name = 'Kim';
}

var num = 100;
var person = {name: 'Lee'}

console.log(num) // 100
console.log(person) // {name: 'Lee'}

// primitive는 원시 타입이므로 변경 X (재할당 필요)
// obj는 객체 타입이므로 재할당 없이 직접 변경 가능
changeVal(num, person)

console.log(num) // 100 원본 훼손 X
console.log(person) // {name: 'Kim'} 원본 훼손
  • **객체 타입** 인수는 **참조 값이 복사되어 매개변수에 전달**되므로 함수 몸체에서 참조 값을 통해 객체 변경할 경우 **원본 훼손** ⇒ 함수 외부에서 함수 몸체 내부로 전달한 참조 값에 의해 원본 객체가 변경되는 부수 효과 발생
  • 객체를 **불변 객체**로 만들어 사용하면 부수 효과 발생 X
    • **객체의 복사본을 생성**하여 객체를 마치 원시 값처럼 변경 불가능한 값으로 동작하게 함 ⇒ **깊은 복사**를 통해 새로운 객체를 생성하고 재할당을 통해 교체

다양한 함수의 형태


즉시 실행 함수

  • 함수 정의와 동시에 즉시 호출되는 함수
  • 단 한 번만 호출되며 다시 호출 불가능
// 익명 즉시 실행 함수
(function () {
	var a = 3;
	var b = 5;
	return a*b;
}());
  • 즉시 실행 함수는 익명 함수가 일반적
    • 그룹 연산자 (…) 내의 기명 함수는 함수 리터럴로 평가되며 함수 이름은 함수 몸체에서만 참조할 수 있는 식별자이므로 다시 호출 불가능
  • 즉시 실행 함수는 반드시 그룹 연산자 (…)로 감싸야 함
    • 그룹 연산자로 감싸지 않고 아래처럼 작성하면 자바스크립트 엔진이 중괄호 뒤에 ‘;’을 자동으로 추가해 에러 발생
    function foo() {
    	var a = 3;
    	var b = 5;
    	return a*b;
    }();
    
    • 함수 선언문 뒤의 ()는 함수 호출 연산자가 아닌 그룹 연산자로 해석되고, 그룹 연산자에 피연산자가 없으므로 에러 발생하는 것

⇒ 그룹 연산자로 묶는 이유 : 먼저 함수 리터럴을 평가하여 함수 객체를 생성하기 위해

재귀 함수

  • 재귀 호출 : 함수가 자기 자신을 호출하는 것
  • 재귀 함수 : 자기 자신을 호출하는 행위 (재귀 호출)을 하는 함수
    • 재귀 함수는 반복 처리를 위해 사용
    • 함수 이름은 함수 몸체 내부에서 유효하므로 함수 내부에서 함수 이름을 사용해 자기 자신 호출 가능
    function countdown(n) {
    	if (n<0) return;
    	console.log(n)
    	countdown(n-1); // 재귀 호출
    }
    countdown(10)
    
  • 재귀 함수는 자신을 무한 재귀 호출함 ⇒ 탈출 조건 필요
  • 재귀 함수는 반복되는 처리를 **반복문 없이 구현**할 수 있다는 **장점**이 있지만, **무한 반복**에 빠질 **위험**이 있음

중첩 함수

  • 중첩 함수 (내부 함수) : 함수 내부에 정의된 함수
    • 자신을 포함하는 외부 함수를 돕는 **헬퍼 함수 역할**
  • 외부 함수 : 중첩 함수를 포함하는 함수
  • 중첩 함수는 외부 함수 내부에서만 호출 가능

콜백 함수

  • 콜백 함수 : 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수
  • 고차 함수 : 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수

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

  • 중첩 함수가 외부 함수 내부에서 할퍼 함수 역할을 하는 것처럼, 콜백 함수도 고차 함수 내부에서 헬퍼 함수 역할을 함 but **콜백 함수는 자유롭게 교체**할 수 있다는 장점이 있음
  • 함수들이 반복하는 일은 변하지 않아 공통적으로 수행해야 하지만, 하는 일의 내용은 다를 때 공통 로직 미리 정의해두고 **변경 로직은 추상화**하여 함수 외부에서 내부로 전달
function repeat(n, f) {
	for (var i=0; i<n; i++) {
		f(i); // 콜백 함수
	}
}

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
  • 콜백 함수는 고차 함수에 의해 호출되며, 이때 콜백 함수에 인수를 전달할 수 있음 ⇒ 고차 함수에 **콜백 함수** 전달할 때 함수를 호출하지 않고 **함수 자체를 전달**해야 됨
  • 콜백 함수는 비동기 처리 뿐만 아니라 배열 고차 함수에서도 사용 (ex. map, filter, reduce,..)

순수 함수와 비순수 함수

  • **순수 함수** : 어떤 외부 상태에 의존하지도 않고 변경하지도 않는, **부수 효과가 없는 함수**
    • 동일한 인수가 전달되면 언제나 동일한 값 반환
    • 오직 매개변수를 통해 함수 내부로 전달된 인수에게만 의존해 값 생성해 반환
    • **최소 하나 이상의 인수를 전달**받음 (인수가 없다면 언제나 동일한 값을 반환하므로 상수와 마찬가지)
    • 인수의 불변성 유지
    var count = 0;
    
    // 순수 함수
    function increase(n) {
    	return ++n;
    }
    
    count = increase(count);  // 1
    count = increase(count);  // 2
    
  • **비순수 함수** : 외부 상태에 의존하거나 외부 상태 변경하는, **부수 효과가 있는 함수**
    • 외부 상태에 따라 반환값 달라짐
    • 외부 상태 : 전역 변수, 서버 데이터, 파일, Console, DOM,…
    • 상태 변화 추적 어려움
    var count = 0;
    
    // 비순수 함수
    function increase() {
    	return ++count;
    }
    
    // 외부 상태(count)를 변경하므로 상태 변화 추적이 어려워짐
    increase();  // 1
    increase();  // 2