hyongti 2022. 3. 2. 00:17
함수는 일련의 과정을 문(statements)으로 구현하고 코드 블록으로 감싸서 하나의 실행단위로 정의한 것

함수는 (다른 프로그래밍 언어도 마찬가지겠지만) 자바스크립트에서 가장 중요한 개념입니다. 자바스크립트의 핵심 개념인 스코프, 실행 컨텍스트, 클로저, 생성자 함수에 의한 객체 생성, 메서드, this, 프로토타입, 모듈화 등이 모두 함수와 깊은 관련이 있습니다. 이들을 이해하고 사용하기 위해 피해갈 수 없는 핵심 개념입니다.

 

프로그래밍 언어의 함수도 수학의 함수와 같이 입력을 받아서 출력을 내보냅니다. 이때,

  • 입력을 전달받는 변수매개변수(parameter)
  • 입력인수(argument)
  • 출력을 반환값(return value)

라고 합니다. 

 

함수는 이며, 여러개 존재할 수 있으므로 함수를 구별하기 위한 식별자인 함수 이름을 사용할 수 있습니다.

 

함수는 함수 정의(function definition)를 통해 생성합니다.

 

생성된 함수를 사용하는 것을 함수 호출(function call/invoke)이라고 합니다.

위에서 함수는 이라고 했습니다. 자바스크립트에서 함수객체 타입의 값입니다. 숫자 값을 숫자 리터럴로 생성하는 것처럼 함수도 함수 리터럴로 생성할 수 있습니다. 그리고 함수는 객체 타입의 이므로 변수에 할당할 수도 있습니다.

리터럴이란? 사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 값을 생성하는 표기 방식. 리터럴은 런타임시 값으로 평가됨.
// 변수에 함수 리터럴을 할당
var f = function add(x, y) {
  return x + y;
};

함수 리터럴은 함수 이름, 매개변수 목록, 함수 몸체로 구성됩니다.

  • 함수 이름 : 함수 이름은 식별자식별자 네이밍 규칙을 준수해야 합니다. 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자입니다. 함수 이름은 생략할 수 있습니다. 이름이 있는 함수를 기명함수(named function), 없는 함수를 무명/익명함수(anonymous function)라고 합니다.
  • 매개변수 목록 : 각 매개변수에는 함수를 호출할 때 지정한 인수가 순서대로 할당됩니다. 따라서 함수를 호출할 때에는 함수 정의에 나열된 매개변수 목록대로 인수를 입력해야 합니다. 매개변수는 함수 몸체 내에서 변수와 동일하게 취급되므로 식별자 네이밍 규칙을 준수해야 합니다.
  • 함수 몸체 : 함수가 호출되었을 때 실행될 문들을 하나의 실행 단위로 정의한 코드블록입니다. 함수 호출에 의해 실행됩니다.

함수는 객체지만, 일반 객체와는 다르게 호출할 수 있습니다. 또한 함수 객체만의 고유한 프로퍼티가 있습니다. 이에 대해서는 잠시 뒤에 살펴보겠습니다.

 

자바스크립트에서는 함수를 정의하는 여러 방법이 있습니다.

  • 함수 선언문
  • 함수 표현식
  • Function 생성자 함수
  • 화살표 함수(ES6)

함수 선언문

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

함수 선언문은 함수 리터럴과 같은 모습입니다. 하지만 함수 선언문은 함수 이름을 생략할 수 없습니다. 

function (x, y) {
  return x + y;
}
// SyntaxError: Function statements require a function name

함수 선언문은 표현식이 아닌 문입니다. 표현식이 아닌 문이라면 변수에 할당할 수 없습니다. 하지만 실제로 선언문을 변수에 할당하면 할당되는 것처럼 보입니다.

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

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

이는 자바스크립트 엔진이 코드의 문맥에 따라

  • 함수 선언문(표현식이 아닌 문)으로 해석하는 경우도,
  • 함수 리터럴(표현식인 문)으로 해석하는 경우도

있기 때문입니다.

 

함수 선언문함수 이름을 생략할 수 없다는 점을 제외하면 함수 리터럴과 같은 형태입니다. 즉, 기명 함수 리터럴은 함수 선언문과 함수 리터럴 둘 다로 해석될 수 있습니다. 

 

자바스크립트 엔진은 기명 함수 리터럴을 단독으로 사용할 경우 함수 아래와 같이 선언문으로 해석하고,

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

foo(); // foo

함수 리터럴을 변수에 할당하거나 피연산자로 사용하는 경우에는 아래와 같이 함수 리터럴 표현식으로 해석합니다.

(function bar() {
  console.log('bar');
}) // 그룹 연산자 ()의 피연산자로 사용
bar(); // ReferenceError: bar is not defined
// 함수 리터럴에서 함수 이름은 함수 몸체 내부에서만 참조할 수 있는 식별자

 

함수 선언문함수 리터럴 표현식함수 객체를 생성한다는 점에서는 동일하지만 호출에 차이가 있습니다.

위 두 예시에서 foo는 호출할 수 있었지만 bar는 호출할 수 없었죠.

 

함수 리터럴(bar)에서 함수 이름은 함수 몸체 내부에서만 참조할 수 있는 식별자라고 했습니다. 이는 함수 몸체 외부에서는 함수 이름으로 함수를 참조할 수 없으므로 함수 몸체 외부에서는 함수를 호출할 수 없다는 의미입니다. 즉, 함수를 가리키는 식별자가 없다는 것과 마찬가지입니다.

 

그런데 어떻게 함수 선언문(foo)은 함수 이름으로 호출할 수가 있었을까요?

결론부터 말씀드리면, foo는 자바스크립트 엔진이 암묵적으로 생성한 식별자입니다.

자바스크립트 엔진은 함수 선언문을 해석해 함수 객체를 생성하는데, 함수 이름은 함수 몸체 내부에서만 유효한 식별자이므로 함수 이름과는 별도로 생성된 함수 객체를 가리키는 식별자가 필요합니다.

 

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

 

아래와 같이 함수 선언문으로 함수를 생성해도,

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

실제로는 이런 작업이 일어나고 있는 것이죠. (아래는 의사코드입니다.)

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

 

다시 말하면, 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출되는 것입니다. 사실 위의 의사코드가 함수 표현식입니다. 의사코드를 보고 자바스크립트 엔진이 함수 선언문을 함수 표현식으로 변환해 함수 객체를 생성한다고 생각할 수 있습니다. 그러나 함수 선언문과 함수 표현식이 정확히 동일하게 동작하는 것은 아닙니다.


함수 표현식

자바스크립트의 함수는 객체 타입의 값이라고 했습니다. 자바스크립트의 함수는 변수에 할당할 수도 있고, 프로퍼티의 value가 될 수도 있으며 배열의 요소가 될 수도 있습니다. 이처럼 값의 성질을 갖는 객체를 일급객체라고 합니다.

 

자바스크립트에서 함수는 일급 객체입니다. 일급 객체라는 것은 함수를 값처럼 자유롭게 사용할 수 있다는 의미입니다.

 

함수는 일급 객체이므로 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있는데, 이러한 함수 정의 방식을 함수 표현식(function expression)이라고 합니다.

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

함수 리터럴의 함수 이름은 생략할 수 있습니다(익명 함수). 함수 표현식의 함수 리터럴은 함수 이름을 생략하는 것이 일반적입니다.

 

함수 이름은 함수 몸체 내부에서만 유효한 식별자로, 함수를 호출할 때에는 함수 객체를 가리키는 식별자를 사용해야 합니다.

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

console.log(add(2, 5)); // 7
console.log(foo(2, 5)); // ReferenceError: foo is not defined

여기까지 보면 함수 선언문이 식별자를 암묵적으로 생성하고, 생성된 함수 객체를 할당하는 모습이 함수 표현식과 유사하게 동작하는 것처럼 보입니다.

 

하지만 함수 선언문(표현식이 아닌 문)과 함수 표현식(표현식인 문)의 동작에는 중요한 차이가 있습니다.

함수 생성 시점과 호이스팅

// 함수 참조
console.dir(add); // f 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;
};

위 예제 코드에서 볼 수 있듯이,

  • 함수 선언문으로 정의한 함수는 선언문 이전에 호출이 가능하지만,
  • 함수 표현식으로 정의한 함수는 표현식 이전에 호출이 불가능합니다.

자바스크립트 엔진은 소스 코드를 실행할 때 먼저 코드를 평가하고, 평가가 끝난 뒤 코드를 실행합니다. 코드 평가 시에는 변수와 함수 선언문을 먼저 실행하여 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록합니다.

 

함수 선언문으로 정의된 함수코드가 한 줄 씩 순차적으로 실행되는 런타임 이전에 먼저 함수 객체가 만들어지고, 암묵적으로 생성된 함수 이름과 동일한 이름의 식별자에 할당됩니다.

 

즉, 코드가 한 줄씩 순차적으로 실행되기 시작하는 런타임에는 이미 함수 객체도 생성되어 있고, 식별자에 할당까지 완료된 상태인 거죠. 따라서 코드 상으로 함수 선언문 이전에 함수를 참조하거나 호출할 수 있게 됩니다. 이처럼 함수 선언문이 코드의 최상단으로 끌어 올려진 것처럼 동작하는 특징을 함수 호이스팅(function hoisting)이라고 합니다.

 

함수 표현식은 변수에 할당되는 값이 함수 리터럴인 문입니다. 따라서 함수 표현식변수 선언문변수 할당문을 한 번에 기술한 축약 표현과 동일하게 동작합니다. 위 예제 코드를 보면, 런타임 이전에 변수 선언문인 var sub;이 실행되고 undefined로 초기화됩니다. 변수 할당문은 런타임에 평가되어 함수 객체가 됩니다.

 

함수 호이스팅은 함수를 호출하기 전에 반드시 함수를 선언해야 한다는 당연한 규칙을 무시합니다. 자바스크립트 언어의 개발에 참여한 더글라스 크락포드는 함수 선언문 대신 함수 표현식을 사용할 것을 권장합니다.


아직 Function 생성자 함수 화살표 함수가 남아있지만, 그 전에 위에서 언급했던 일급 객체와 함수 객체의 프로퍼티에 대해 먼저 짚고 넘어가겠습니다. 

일급 객체란?

  1. 무명의 리터럴로 생성할 수 있음. 즉, 런타임에 생성이 가능
  2. 변수나 자료구조에 저장할 수 있음.
  3. 함수의 매개변수에 전달할 수 있음.
  4. 함수의 반환값으로 사용될 수 있음.

자바스크립트에서 함수는 위의 조건을 모두 만족합니다.

// 1. 무명의 리터럴로 런타임에 생성되어, 2. 변수에 저장됨.
const increase = function (num) {
  return ++num;
};

const decrease = function (num) {
  return --num;
};

// 2. 객체에도 저장할 수 있음.
const auxs = { increase, decrease };

// 3. 함수의 매개 변수에 전달할 수 있음.
function makeCounter(aux) {
  let num = 0;
  
  // 4. 함수의 반환값으로 사용할 수 있음.
  return function() {
    num = aux(num);
    return num;
  };
}

// 3. 함수의 매개 변수에 전달할 수 있고, 4. 반환값으로 사용할 수 있음.
const increaser = makeCounter(auxs.increase);
console.log(increaser());

함수 객체의 프로퍼티

Object.getOwnPropertyDescriptiors로 함수의 프로퍼티를 보면, 일반 객체와 다르게 arguments, caller, length, name, prototype 프로퍼티가 있는 것을 볼 수 있습니다.(caller 프로퍼티는 ECMAScript 사양에 포함되지 않은 비표준 프로퍼티이므로 따로 정리하지 않겠습니다.)

console.dir로 조회했을 때 나오는 [[Prototype]], [[Scopes]]는 다른 포스팅에서 다루겠습니다.

 

arguments

함수 객체의 arguments 프로퍼티 값은 arguments 객체로, 함수 호출 시 전달된 인수(argument)들의 정보를 담고 있는 순회 가능한(iterable) 유사 배열 객체입니다. 함수 내부에서 지역 변수처럼 사용되며, 외부에서는 참조할 수 없습니다.
(유사 배열 객체란 length 프로퍼티를 가진 객체로 for 문으로 순회할 수 있는 객체를 말합니다.)

 

함수 객체의 arguments 프로퍼티는 ES3부터 표준에서 폐지되었다고 합니다. 따라서 Function.arguments와 같은 사용법은 권장되지 않습니다.

 

자바스크립트는 함수의 매개변수와 인수의 개수가 일치하는지 확인하지 않습니다. 따라서 매개변수의 개수만큼 인수를 전달하지 않아도 에러가 발생하지 않습니다.

 

브라우저의 콘솔창에서 아래의 코드를 입력하면,

function mult(x, y) {
  console.log(arguments);
  return x * y;
}

console.log(mult()); // NaN
console.log(mult(1)); // NaN
console.log(mult(1, 2)); // 2
console.log(mult(1, 2, 3)); // 2

아래와 같이 결과가 나오는 것을 볼 수 있습니다.

arguments 객체는 인수를 프로퍼티 value로, 해당 프로퍼티의 key는 인수의 순서입니다.
arguments 객체 내부의 callee 프로퍼티는 호출되어 arguments 객체를 생성한, 함수 자신을 가리킵니다. length 프로퍼티는 인수의 개수를 가리킵니다.
Symbol(Symbol.iterator) 프로퍼티는 arguments 객체를 순회 가능한 자료구조인 이터러블(iterable)로 만들기 위한 프로퍼티입니다. 심볼에 대해서는 다른 글에서 자세히..

arguments 객체는 매개변수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용합니다.

function sum() {
  let res = 0;
  
  for (let i = 0; i < arguments.length; i++) {
    res += arguments[i];
  }
}

console.log(sum()); // 0
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3)); // 6

 

arguments는 배열과 비슷한 유사 배열 객체이지만, 배열은 아닙니다.  따라서 배열 메서드를 사용하려면 Function.prototype.call, Function.prototype.apply를 사용해 간접 호출해야 합니다. 간접 호출 방법이나 배열에 대한 내용은 다른 포스팅에 정리하겠습니다.

아래는 배열의 메서드인 reduce를 사용하기 위한 예시입니다.

function sum() {
  // arguments 객체를 배열로 변환
  const array = Array.prototype.slice.call(arguments);
  return array.reduce(function(pre, cur) {
    return pre + cur;
  }, 0);
}

console.log(sum(1, 2)); // 3

ES6에서는 이러한 번거로움을 해결하기 위해 Rest 파라피터가 도입됐습니다. 

// ES6 Rest Parameter
function sum(...args) {
  return args.reduce((pre, cur) => pre + cur, 0);
}

console.log(sum(1, 2)); // 3

Rest 파라미터에 대해서는 다른 포스팅에 정리하겠습니다.

 

length

함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킵니다.

arguments 객체의 length는 인자(argument)의 개수를 가리키기 때문에 둘은 다를 수 있습니다.

 

name

함수 객체의 name 프로퍼티는 함수의 이름을 나타냅니다. ES6 이후부터 정식 표준이 되었습니다. name 프로퍼티는 ES5와 ES6에서 다르게 동작합니다. 익명 함수 표현식의 경우 ES5에서 name 프로퍼티는 빈 문자열을 값으로 갖지만, ES6에서는 함수 객체를 가리키는 식별자를 값으로 가집니다.

// 기명 함수 표현식
var namedFunc = function foo() {};
console.log(namedFunc.name); // foo;

// 익명 함수 표현식
var anonymousFunc = function() {};
// ES5에서 name 프로퍼티는 빈 문자열을 값으로
// ES6에서 name 프로퍼티는 함수 객체를 가리키는 식별자(변수 이름)를 값으로
console.log(anonymousFunc.name); // anonymousFunc

// 함수 선언문
function bar() {}
console.log(bar.name); // bar

 

 

prototype

prototype 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor만이 소유하는 프로퍼티입니다. 일반 객체와 생성자 함수로 호출할 수 없는 non-constructor에는 prototype 프로퍼티가 없습니다.


Function 생성자 함수

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

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

자바스크립트가 기본으로 제공하는 빌트인 함수인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면 함수 객체를 생성해서 반환합니다. 위 예제 코드에서 new가 없어도 결과는 동일합니다.

 

Function 생성자 함수로 함수를 생성하는 것은 일반적이지도, 바람직하지도 않은 방법입니다. 클로저(clouser)도 생성하지 않는 등, 함수 선언문이나 함수 표현식으로 생성한 함수와 다르게 동작합니다.

 


화살표 함수

ES6에서 도입된 화살표 함수(arrow function)는 function 키워드 대신 화살표(fat arrow, =>)를 사용해 좀 더 간략한 방법으로 함수를 선언할 수 있습니다. 화살표 함수는 항상 익명함수로 정의합니다.

const add = (x, y) => x + y;
console.log(add(2, 5)); // 7

화살표 함수는 기존의 함수보다 표현만 간략한 것이 아니라 내부 동작 또한 간략화되어 있습니다. 화살표 함수는 생성자 함수로 사용할 수 없으며, 기존 함수와 this 바인딩 방식이 다르고, prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않습니다.


이 외에도 콜백 함수, 순수 함수 등 함수와 관련된 추가적인 내용이 있지만, 글이 길어지는 관계로 다른 글에 정리하겠습니다.

 

출처

https://book.naver.com/bookdb/book_detail.nhn?bid=16710547

 

모던 자바스크립트 Deep Dive

269개의 그림과 원리를 파헤치는 설명으로 ‘자바스크립트의 기본 개념과 동작 원리’를 이해하자!웹페이지의 단순한 보조 기능을 처리하기 위한 제한적인 용도로 태어난 자바스크립트는 과도

book.naver.com