Programming/javascript

자바스크립트 기초부터 모던 자바스크립트까지 - 함수편

KOOCCI 2022. 7. 4. 01:12

목표 : 자바스크립트의 함수에 대해 알아본다.


자바스크립트에서 가장 중요한 개념이다.

스코프, 실행 컨텍스트, 클로저, this, 프로토타입, 모듈화 등 모두 함수에서부터 시작된다.

 

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

 

간단히 용어를 정리해보자.

  • 매개변수(parameter) : 함수 내부로 입력을 전달받는 변수
  • 인수 (argument) : 입력
  • 출력 (return value) : 출력
function add(x, y) { // parameter
	return x+y; // return value
}

add(2,5); // argument

함수 리터럴

자바스크립트의 함수는 객체 타입의 값이다. 따라서 숫자 값을 숫자 리터럴로 생성하고 객체를 객체 리터럴로 생성하듯, 함수도 함수 리터럴로 생성할 수 있다.

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

위 예제를 보면 함수 리터럴을 변수에 할당하고 있다.

리터럴 정의는 이전 포스팅에서 다루었다. 리터럴은 값을 생성하기 위한 약속된 표기법이다.

함수 리터럴도 평가되어 값을 생성하며, 이 값은 객체다. 즉, 함수는 객체다.

다른 점은 일반 객체는 호출할 수 없지만, 함수는 호출할 수 있다.

 

함수 정의

함수를 정의하는 방법은 4가지가 있다.

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

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

// 3. Function 생성자 함수
var add = new Function('x', 'y', 'return x+y');

// 화살표 함수 (ES6)
var add = (x,y) => x+y;

함수 선언문

함수 선언문은 함수 리터럴과 형태가 동일하다.

단, 함수 리터럴은 함수 이름을 생략할 수 있으나 함수 선언문은 함수 이름을 생략할 수 없다.

 

함수 선언문은 표현식이 아닌 문(statement)이다.

이전 포스팅 내용처럼 값으로 평가할 수 있는 문이 표현식이다.

즉, 값으로 평가할 수 없다는 것이다. 실제로, 위 함수 선언문을 개발자 환경에서 실행하면 undefined가 나온다.

표현식이라면 그 값이 찍혀야 한다.

 

그런데 조금 이상한 경우가 있다.

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

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

분명 표현식이 아닌 문인데, 변수에 들어가는 것처럼 보인다.

이는 자바스크립트 엔진의 해석차이다.

엔진에서 위 식을 함수 리터럴로 볼수도 있고, 표현식이 아닌 문으로 볼 수도 있다.

중이적인 표현이 된다는 것이다.

즉 위 예시에서는 함수 리터럴로 판단하였고, 정상적인 방식으로 정의 된 것이다.

function foo() { console.log('foo')};
foo(); // foo

(function bar() { console.log('bar'); });
bar(); // ReferenceError: bar is not defined

foo는 함수 선언문으로 해석되었다.

그러나, 그룹 연산자()안에 들어간 bar는 함수표현식으로 해석된 것이다.

그룹연산자에는 값으로 평가될 수 있는 표현식만 들어갈 수 있다.

또한, 함수 리터럴에서는 함수명이 내부에서만 쓸수 있는 식별자다.

외부에서는 함수 리터럴의 함수명을 호출 할 수 없다. 따라서, 외부에서는 bar라는 존재를 알지 못하게 된다.

그런데 foo를 호출할 수 있었던 이유는 무엇인가?

함수 리터럴의 함수명은 외부에서 호출 할 수 없는데, 함수 선언문은 예외인것일까라고 생각할 수 있다.

 

자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당한다.

즉, 우리는 함수 이름으로 호출하는 것처럼 보였지만 사실은 함수 객체를 가리키는 식별자로 호출하는 것이다.

var add = function add(x,y) { // 식별자add와 함수이름 add를 구분하자
	return x+y;
};

console.log(add(2,5)); // 7, 식별자 add다.

함수 표현식

자바스크립트 함수는 객체 타입의 값이라고 앞서 설명하였다.

즉, 값처럼 변수에 할당하거나 프로퍼티가 되거나 배열이 될 수 있다.

이렇게 값의 성질을 갖는 객체를 일급 객체라고 한다.

일급 객체이므로, 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다. 이런 함수 정의 방식을 함수 표현식이라 한다.

 

함수 리터럴의 함수 이름은 생략할 수 있고, 이를 익명 함수라 한다.

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

 

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

//함수 참조
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;
}

위 예시의 특징을 분석해보면 다음과 같다.

  • 함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있다.
  • 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다.

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

함수 선언문도 다른 선언문(변수 선언 등)과 동일하게 코드가 한 줄씩 순차적으로 실행되는 시점인 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행된다.

함수 선언문이 코드의 선두로 끌어올려진 것처럼 동작하는 자바스크립트 고유의 특징호이스팅이라고 한다.

 

단, 함수 호이스팅과 변수 호이스팅은 약간의 차이가 있다.

var 키워드를 통한 변수 선언문과 함수 선언문은 런타임 이전에 엔진에 의해 먼저 실행되어 식별자를 생성하는 건 동일하다.

하지만 var 키워드로 선언된 변수는 undefined로 초기화되고, 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화된다.

즉, 함수 선언문으로 정의한 함수를 함수 선언문 이전에 호출하면 함수 호이스팅에 의해 호출이 가능하다

 

함수 표현식은 변수에 함수 리터럴이 할당되는 문이다.

변수 할당문의 값은 할당문이 실행되는 시점, 즉 런타임에 평가되므로 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 된다.

즉, 함수 표현식으로 함수를 정의하면 함수 호이스팅이 발생하는 것이 아니라 변수 호이스팅이 발생한다.

 

Function 생성자 함수

매개변수 목록과 함수 몸체를 문자열로 전달 받아 new로 생성이 가능하다. (new가 없어도 가능하다)

일반적이지도 않고, 바람직한 방법도 아니다.

추후 알아볼 클로저를 생성하지 않는 등, 앞서 알아본 것들과 다르게 동작한다.

 

화살표 함수

ES6에 도입되었으며, 항상 익명함수로 정의한다.

표현만 간략한게 아니라, 내부 동작 또한 간략화되어 있어 함수 선언문 혹은 함수 표현식을 완전히 대체하지는 못한다.

그 특징을 간략히 알아보고, 추후에 다시한번 만나보도록 하자.

  • 생성자 함수로 사용할 수 없다.
  • 기존 함수와 this 바인딩이 다르다.
  • prototype 프로퍼티가 없다.
  • arguments 객체를 생성하지 않는다.

함수의 호출

매개변수(parameter)와 인수(argument)

함수가 호출되면 함수 몸체 내에 암묵적으로 매개변수가 생성되고 일반 변수와 마찬가지로 undefined로 초기화된 이후 인수가 순서대로 할당된다.

그리고 별도로 알아보겠지만, 매개변수의 스코프(유효 범위)는 함수 내부다.

 

또한, 매개변수의 갯수와 인수의 갯수는 같은지 체크하지 않는다.

매개변수가 인수보다 많다면, 할당되지 않은 매개변수에는 undefined가 들어가게 되며, 초과된 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관된다.

 

타입이 없는 자바스크립트에서는 어떤 타입이 파라미터로 전달되는지 알지 못한다.

이를 방지하기 위해 타입스크립트같은 정적 타입 선언 언어를 사용하기도 한다.

 

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

함수 파라미터로 객체를 받을 때 주의해야 할 점은 참조의 의한 전달이 있다는 것이다.

원시 타입은 변경이 불가능 하니, 파라미터에서 재할당을 통해 새로운 원시값으로 교체하여 기존 값의 변경이 되지 않는다.

그러나 객체 타입으로 인수를 받는다면, 참조 값이 복사되어 원본에도 영향이 간다.

 

다양한 함수의 형태

즉시 실행 함수

함수의 정의와 동시에 즉시 실행되는 함수다.

단 한번만 호출되며 다시 호출할 수 없다.

(function() {
    var a = 1;
    var b = 2;
    return a+b;
}());

보통 이름없는 익명함수를 사용한다. (있어도, 사용할 수 없다)

 

즉시 실행함수는 반드시 그룹 연산자로 감싸야 한다. 그렇지 않으면 에러가 발생한다.

function() { //Uncaught SyntaxError: Function statements require a function name
    var a = 1;
    var b = 2;
    return a+b;
}();

위 에러가 나는 이유는 함수 정의가 함수 선언문에 맞지 않기 때문이다. (이름을 생략할 수 없다)

그럼 이름을 넣어보자.

function foo() { //Uncaught SyntaxError: Unexpected token ')'
    var a = 1;
    var b = 2;
    return a+b;
}();

에러 내용이 바뀌었다.

자바스크립트 엔진은 암묵적으로 수행하는 세미콜론 자동 삽입 기능으로, 함수 선언문 이후에 자동으로 세미콜론을 넣게 된다. (함수 코드 블록의 닫는 중괄호 뒤)

function foo(){}(); // function foo(){};();

함수 선언문 뒤의 ()는 함수 호출 연산자가 아닌 그룹 연산자로 해석된다. 따라서, 그룹 연산자에 피연산자가 없어 에러가 나는 것이다.

 

그룹 연산자의 피연산자는 값으로 평가되므로, 기명 또는 무명 함수를 그룹 연산자로 감싸면 함수 리터럴로 평가되어 함수 객체가 된다.

즉, 그룹 연산자로 묶는 이유는 먼저 함수 리터럴을 평가해서 함수 객체를 생성하기 위해서다.

따라서 함수 객체를 생성할 수 있으면, 다른 연산자를 사용해도 된다.

(function() {
	// ...
}());

(function() {
	// ...
})();

!function() {
	// ...
}();

+function() {
	// ...
}();

중첩 함수

함수 내부에 정의된 함수중첩 함수(Nested Function) 또는 내부 함수(Inner Function) 이라고 한다.

중첩 함수는 외부 함수 내부에서만 호출 할 수 있다.

일반적으로 중첩 함수는 자신을 포함하는 외부 함수를 돕는 헬퍼 함수(Helper Function) 역할을 한다.

function outer() {
    var x = 1;

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

    inner();
};

outer();

ES6부터 함수 정의는 문이 위치할 수 있는 문맥이면 어디든 가능하다. (if 문이나 for문 내에서도 가능하다.)

단, 호이스팅으로 혼란이 올 수 있으므로, if문이나 for 문 등의 코드 블록에서 함수 선언문을 통해 함수를 정의하는 것은 바람직하지 않다.

중첩 함수는 이후 스코프나 클로져를 배울 때 한번 더 보도록 하자.

 

콜백 함수

함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수콜백 함수라고 하며, 매개 변수를 통해 함수의 외부에서 콜백함수를 전달받는 함수고차 함수라고 한다.

 

중첩 함수는 고정되어 있어 교체하기 곤란하지만, 콜백 함수는 함수 외부에서 고차 함수 내부로 주입하기 때문에, 자유로운 교체가 가능하다. 즉, 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다.

 

고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해 호출한다.

즉, 고차함수에 의해 호출되며 이 때, 고차 함수는 필요에 따라 콜백 함수에 인수를 전달할 수 있다.

 

function repeat(n, f) {
    for(var i = 0; i < n; i++) {
        f(i);
    }
};

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

repeat(5, logAll);

순수 함수와 비순수 함수

함수형 프로그래밍에서 어떤 외부 상태에 의존하지도 않고 변경하지도 않는, 즉 부수 효과(Side Effect)가 없는 함수를 순수함수라고 한다.

순수 함수는 동일한 입력이면 동일한 결과를 반환한다.

함수형 프로그래밍은 순수함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성을 지향하는 프로그래밍 패러다임이다.

로직 내 존재하는 조건문과 반복문을 제거해서 복잡성을 해결하고, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다.

 

다음에는 스코프에 대해 배워보도록 하자.