[JavaScript] 7. Function

100tick·2022년 12월 19일
0

JavaScript Deep Dive

목록 보기
7/16
post-thumbnail

1. Function Definition, Call

함수는 JS를 포함한 여타 프로그래밍 언어에서 핵심이 되는 개념이다.
수학에서의 함수와 같이 특정한 값을 입력 받아, 그 값에 따라 결과가 출력 되는 일련의 과정을 뜻한다.

아래와 같이 함수를 정의할 수 있다.

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

function 뒤에 함수명을 입력하고, 그 뒤 괄호 안에 들어 있는 x, y는 입력 받을 변수를 의미한다.
그 뒤 {} 안에 함수 내부에서 실행할 statement들이 들어간다.

return 뒤에 붙은 expression은 함수가 끝날 때 반환할 값으로, 출력값을 의미한다.

물론 입력값이 없을 수도, 출력값이 없을 수도, 둘 다 없을 수도 있다.

그럴 때는 아래와 같은 모습일 것이다.

function noIn() {
	return 1;
}

function noOut(a, b) { 
}

function noInOut() {}

함수가 반환한 값을 변수에 할당할 수 있는데, 다음의 간단한 덧셈 함수를 통해 1+1의 반환값을 확인해보자.

function add(a, b) {
	return a + b;
}

let a = add(1, 1);
a; // 2

결과값인 2가 잘 할당되었다.

function noOut() {}

let b = noOut();
b; // undefined

반환값이 없는 함수의 결과를 변수에 할당하면 undefined가 된다.

함수가 어떤 입력값, 출력값을 받을 것인지를 나타내는 부분을 Function Declaration, 함수 선언이라 하고, 함수를 실행시켜 결과값을 반환 받는 부분을 Function Call, 함수 호출이라 부른다.

이 둘의 차이를 잘 기억해두자.

function add(x, y) { return a + b; } // function declaration

add(1, 2); // function call

2. Reusability

아래와 같은 연산이 필요하다고 가정해보자.
(아무런 의미 없는 연산이지만 해당 연산이 필요하다고 가정)

let x1 = 1;
let y1 = 1;

let result1 = (x1 + x1 + y1) * (y1 + y1 + x1) * (y1 + x1 + y1) * (x1 + y1);

let x2 = 1;
let y2 = 1;

let result1 = (x2 + x2 + y2) * (y2 + y2 + x2) * (y2 + x2 + y2) * (x2 + y2);

// ...

만약 아래의 연산이 수백 곳에서 매우 자주 사용된다면, 그 때마다 해당 식을 적는 것이 상당히 번거로울 것이다.
게다가 만약 공식이 약간이라도 변경된다면, 수백 곳에서 모두 변경이 이루어져야 할 것이다.

이런 반복적으로 자주 사용되는 연산을 수행할 때, 함수가 막강한 위력을 발휘한다.

function someFn(x1, y1) {
 return (x1 + x1 + y1) * (y1 + y1 + x1) * (y1 + x1 + y1) * (x1 + y1);
}

let x1 = 1;
let y1 = 1;

let result1 = someFn(x1, y1);

let x2 = 1;
let y2 = 1;

let result1 = someFn(x2, y2);
// ...

someFn()이라는 함수를 정의하여 그 안에 피연산자들을 전달하고 만약 공식이 변경된다면 함수 안에서 단 1번만 변경하면 모든 연산에 변경이 적용될 것이다.
거기에 해당 연산이 어떤 역할을 하는지 수식을 보는 것보다는 함수의 이름을 보는 것이 훨씬 파악하기 쉽고 빠르기 때문에, 코드의 양이 늘어날수록 가독성을 높여주는 효과도 있다.

DRY(Don't Repeat Yourself) 라고 불리는 원칙이 있다.
같은 일을 쓸데없이 반복적으로 수행하지 말라는 오래된 소프트웨어 개발 원칙 중 하나다.
함수는 이 원칙을 지키는데 커다란 도움을 준다.


3. Ways to define Function

4가지 함수 정의 방법이 있다.
비슷하지만 약간의 차이가 있다.

참고로 변수는 선언, 함수는 정의라고 하는데, 함수 선언문이 평가되면 identifier가 암묵적으로 생성되고 함수 객체가 할당되기 때문이다.

3.1 Function Definition

function add(x, y) { // 함수명 반드시 필요, statement
	return x + y;
}

함수 선언문은 함수명을 반드시 기재해야 한다.
아니면 SyntaxError가 발생한다.

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

함수 선언문은 statement이므로 선언 시,undefined가 반환된다.
고로 위 addFn 변수는 undefined가 될 것이라고 예상이 되겠지만, JS 인터프리터는 함수 선언과 동시에 변수에 할당하는 경우에는 function literal로 평가된다.

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

위와 같이 그룹 연산자 내에 선언된 함수도 함수 선언문이 아닌 function literal expression으로 평가되어 함수 객체가 반환되기 때문에 선언과 동시에 변수에 할당하지 않으면 해당 함수를 참조할 방법이 없을 것이고, GC에 의해 drop될 것이다.

이는 마치 {}가 문맥에 따라 object로도 해석되고, block으로도 해석되는 것과 비슷하다.

함수 선언도 문맥에 따라 definition, expression이 모두 될 수 있는 것이다.

함수명은 함수 바디 내에서만 참조할 수 있는 identifier다.
따라서 함수 외부에서는 함수명으로 접근할 수 없다.
그러나 함수 선언문인 경우, JS 인터프리터가 암묵적으로 함수명을 identifier로 생성하고 거기에 함수를 할당하는 과정을 자동으로 처리하기 때문에 함수를 따로 변수에 할당할 필요 없이 접근이 가능한 것이었다.

즉, 함수는 실제로는 함수명이 아닌, 함수 객체를 가리키는 identifier로 호출되는 것이다.

let fn = function fn2() {};

가령 위 코드는 함수 선언문 fn2를 변수 fn에 할당했다.
우리가 지금까지 살펴본 내용에 따르면, 함수 선언문에서 사용된 함수명 fn2는 함수 바디 내부에서만 유효하다.

만약 함수 expression이 아닌 statement라면 JS 인터프리터에 의해 자동으로 fn2라는 identifier가 할당 될 것이다.

그러나 JS 엔진은 문맥에 따라 expression인지 statement인지 다르게 판단한다고 하였다.
위 코드에서는 fn이라는 변수에 fn2가 할당되었기 때문에 expression이 된다.

고로 fn2는 따로 identifier가 되지 않고 사라질 것이며, fn가 해당 함수를 참조할 수 있는 유일한 identifier로 남을 것이다.

3.2 Function Expression

let fn = function(x, y) {
	return x + y;
};

JS의 함수는 일급 객체다.
용어만 봐서는 무슨 뜻인지 이해하기 어려우므로 정의를 살펴보자.

In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities.

함수를 마치 값, expression처럼 사용할 수 있다는 뜻이다.

function fn(x, y) { return function() {}; }

fn(function() {}, function() {});

예시와 함께 보자면, 위와 같이 함수를 또 다른 함수의 인자로 넘기고, 함수를 반환값으로 받는 등 그냥 값처럼 쓸 수 있다.

이것이 일급 객체의 의미다.

let fn = function(x, y) {
	return x + y;
};

Function Expression은 함수명을 생략할 수 있으며, 이를 Anonymous, 익명 함수라고 한다.
위에서 봤듯 함수 선언문을 변수에 할당하는 경우, Function Expression으로 평가되어 함수 선언문에 기재된 함수명은 사라지고 변수명이 identifier가 되므로 함수 선언문에 함수명을 기재해도 헷갈리기만 하고, 사용할 수도 없기 때문에 일반적으로 Function Expression익명 함수로 사용된다.

3.3 Built-in Function Constructor

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

add(1, 1); // 2

함수를 이렇게도 만들 수 있다고 알아만 두자.
Function Declaration, Function Expression과 동작도 다르고 뒤에서 살펴볼 Closure도 생성하지 않으며 문자열 형태로 함수 바디 안에 statement를 작성하는 것은 문법 오류도 확인할 수 없으므로 웬만하면 사용할 일이 없을 것이다.

3.4 Arrow Function

let fn = (x, y) => {
	return x + y;
}

let fn2 = (x, y) => x + y;

ES6에서 도입된 문법으로, 함수를 보다 간략하게 정의할 수 있다.
항상 익명 함수로 정의하며, 함수 바디에 expression 하나만 존재한다면 return을 생략할 수 있다.
그럴 경우 단 하나의 expression이 반환된다.

간략화한 것 뿐만 아니라 일반 함수와 다르게 작동한다.
아래에 차이점을 나열하였다.
Constructor로 사용할 수 없다.
this의 바인딩 방식이 다르다.
prototype Property가 없다.
arguments object를 생성하지 않는다.


4. Parameter, Argument

함수 실행 결과는 어떤 값을 넣는지에 따라 달라진다.(값을 받는 경우)
아래 코드를 보면 x, yadd 함수에 전달하고 있는데, 이것을 Paramter, 매개변수라고 부른다.

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

add(1, 1); // 1, 1은 arguments

add 함수를 호출할 때, 1, 1을 넣어줬는데, 이 때는 Argument, 인자라고 부르며 expression만 전달할 수 있다.
선언부, 호출부에 따라 용어가 달라지므로 꽤나 헷갈린다.
잘 숙지해두자.

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

만약 x, y에 모두 1을 넣어서 함수를 호출했다면, 이는 add 함수 내에서 x, y라는 변수에 1을 넣어 생성한 것과 같다고 보면 된다.
즉, 인자로 넘겨진 값들은 해당 함수 내부에서만 존재하는 또 다른 변수가 된다.

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

add();

만약 매개변수는 2개인데, 매개변수 없이 호출한다면?

변수와 마찬가지로 undefined로 초기화된다.

다른 언어에서는 매개변수의 타입이나 개수가 맞지 않으면 대개 오류를 던지지만, JS는 인자를 많이, 혹은 적게 넣어도 오류가 발생하지 않는다.

인자가 더 많이 들어가면 나머지는 무시되고, 인자가 더 적게 들어가면 undefined로 채워진다.

function add(x, y) {
    console.log(arguments); // Arguments(4) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
	return x + y ;
}

add(1, 2, 3, 4);

초과된 인자가 사라지지는 않고 arguments 객체의 Property로 저장된다.
이에 대한 자세한 내용은 나중에 살펴보자.


5. Default Arguments

function add(a = 1, b = 1) {
	return a + b;
}

add(); // 2
add(2, 2); // 4
add(1); // 2
add(undefined, 2); // 3

인자가 생략되었을 때, 위와 같은 방식으로 기본값을 줄 수 있다.
a, b에 값이 전달되지 않았을 경우 각 1이 들어간다.


6. IIFE(Immediately Invoked Function Expression)

(function() { console.log(1); });

위에서 ()로 둘러싼 익명 함수는 표현식으로 평가되어 identifier가 변수에 따로 바인딩 되지 않기 때문에 GC에 의해 drop된다는 것을 알아보았다.

이 점을 이용해서, 한 번 실행하고 drop되는 일회용 함수를 만들 수 있다.

(function() { console.log(1); })();

첫번째 함수와 두번째 함수의 차이는 뒤에 ()의 존재 여부이다.
첫번째 함수는 익명 함수 선언 뒤 drop되기 때문에 아무런 동작도 하지 않지만,
두번째 함수는 익명 함수 선언 -> 실행 뒤 drop되기 때문에 한 번은 실행되는 것이다.

따로 바인딩 된 identifier가 없기 때문에 당연히 이후로는 호출이 불가하다.

(function() { console.log(1); }()); // 미묘하게 () 위치가 다름
(function() { console.log(1); })();

함수 호출부 ()가 그룹 연산자 내부/외부에 있어도 모두 똑같이 작동한다.
단, 화살표 함수는 조금 다른데, 호출부가 외부에 있어야 한다.
안그러면 오류가 발생하거나 제대로 동작하지 않는 것을 볼 수 있었다.

(() => 3()); // () => 3()
typeof (() => 3()); // 'function'
(() => 3())(); // Uncaught TypeError: 3 is not a function
(() => { return 3; }()); // Uncaught SyntaxError: Unexpected token '('

(() => { return 3; })(); // 3
(() => 3)(); // 3

7. Function Hoisting

add1(1, 1); // 2
add2(1, 1); // "ReferenceError: Cannot access 'add2' before initialization

// function declaration
function add1(x, y) { return x + y; }
// function expression
let add2 = function(x, y) { return x + y; };

var로 선언된 변수는 Hoisting되어 런타임 이전에 자동으로 undefined로 초기화되지만, 함수 선언문은 Hoisting될 때 Identifier, Function Object 생성, 할당 모두 완료되기 때문에 C, Python 등과 달리 함수가 작성된 순서를 신경쓰지 않고 그대로 사용할 수 있다.

단, Function Expression은 여타 변수와 똑같이 작동하기 때문에 런타임에 평가된다.
고로 함수가 초기화 된 라인 이전에 사용하면 오류가 발생하는 것을 볼 수 있다.

0개의 댓글