Study JavaScript 0616 - 객체로서의 함수와 기명 함수 표현식

변승훈·2022년 6월 16일
0

Study JavaScript

목록 보기
30/43

객체로서의 함수와 기명 함수 표현식

함수는 호출이 가능한 "행동 객체"이다. 함수를 호출 및 객체처럼 함수에 프로퍼티를 추가·제거하거나 참조를 통해 전달할 수 있다.

1. "name"프로퍼티

함수 객체엔 몇 가지 쓸만한 프로퍼티가 있다.
"name"프로퍼티를 사용하면 함수 이름을 가져올 수 있다.

function sayHi() {
  console.log("Hi");
}

console.log(sayHi.name); // sayHi

함수 객체에 이름을 할당해주는 로직에 의해 익명 함수라도 자동으로 이름이 할당된다.

let sayHi = function() {
  console.log("Hi");
};

console.log(sayHi.name); // sayHi (익명 함수이지만 이름이 있다.)

이는 기본 값을 사용해 할당한 경우도 마찬가지다

function f(sayHi = function() {}) {
  console.log(sayHi.name); // sayHi (이름이 있다)
}

f();

이 기능을 'contextual name’이라고 부르며 javascript 명세서에 저으이되어 있다. 이름이 없는 함수의 이름을 지정할 땐 컨텍스트에서 이름을 가져온다.

객체 메서드의 이름도 ‘name’ 프로퍼티를 이용해 가져올 수 있다.

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

console.log(user.sayHi.name); // sayHi
console.log(user.sayBye.name); // sayBye

그런데 객체 메서드 이름은 함수처럼 자동 할당이 되지 않는다. 적절한 이름을 추론하는 게 불가능한 상황이 있는데, 이때 name 프로퍼티엔 빈 문자열이 저장된다.

// 배열 안에서 함수를 생성함
let arr = [function() {}];

console.log( arr[0].name ); // <빈 문자열>
// 엔진이 이름을 설정할 수 없어서 name 프로퍼티의 값이 빈 문자열이 됨

실무에서 대부분의 함수는 이름이 있으므로 위와 같은 상황은 잘 발생하지 않는다.

2. ‘length’ 프로퍼티

내장 프로퍼티 length는 함수 매개변수의 개수를 반환한다.

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

console.log(f1.length); // 1
console.log(f2.length); // 2
console.log(many.length); // 2

위 예시를 통해 나머지 매개변수는 개수에 포함되지 않는다는 사실 또한 확인해 보았다.

length 프로퍼티는 다른 함수 안에서 동작하는 함수의 타입을 검사(type introspection) 할 때도 종종 사용된다.

질문에 쓰일 question과 질문에 대한 답에 따라 호출할 임의의 수의 handler 함수를 함께 받는 함수 ask를 예시로 이에 대해 알아보자.

사용자가 답을 제출하면 ask는 핸들러 함수를 호출하며, 이때 우리는 두 종류의 핸들러 함수를 ask에 전달할 수 있다.

  • 인수가 없는 함수로, 사용자가 OK를 클릭했을 때만 호출됨
  • 인수가 있는 함수로, 사용자가 OK를 클릭하든 Cancel을 클릭하든 호출됨

그리고 handler.length 프로퍼티를 사용하면 상황에 맞는 handler를 호출할 수 있다.

사용자가 긍정적인 대답을 했을 때 사용 할 인수가 없는 핸들러를 하나 만들고, 사용자의 응답 종류와 관계없이 범용적으로 사용할만한 핸들러도 구축해 ask 내부에서 handler.length와 함께 사용하면 된다.

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// 사용자가 OK를 클릭한 경우, 핸들러 두 개를 모두 호출함
// 사용자가 Cancel을 클릭한 경우, 두 번째 핸들러만 호출함
ask("질문 있으신가요?", () => alert('OK를 선택하셨습니다.'), 
result => console.log(result));

인수의 종류에 따라(위 예시에선 인수의 length 프로퍼티 값에 따라) 인수를 다르게 처리하는 방식을 프로그래밍 언어에선 다형성(polymorphism) 이라고 부른다. javascript 라이브러리를 뜯어보다 보면 다형성이 곳곳에서 사용되고 있다는 것을 확인할 수 있다.

3. 커스텀 프로퍼티

함수에 자체적으로 만든 프로퍼티를 추가할 수도 있다.

이런 특징을 이용해 함수 호출 횟수를 counter 프로퍼티에 저장해보자!

function sayHi() {
  console.log("Hi");

  // 함수를 몇 번 호출했는지 세봅시다.
  sayHi.counter++;
}
sayHi.counter = 0; // 초깃값

sayHi(); // Hi
sayHi(); // Hi

console.log( `호출 횟수: ${sayHi.counter}` ); // 호출 횟수: 2회

★ 프로퍼티는 변수가 아니다.
sayHi.counter = 0와 같이 함수에 프로퍼티를 할당해도 함수 내에 지역변수 counter가 만들어지지 않는다. counter 프로퍼티와 변수 let counter는 전혀 관계가 없다.
프로퍼티를 저장하는 것처럼 함수를 객체처럼 다룰 수 있지만, 이는 실행에 아무 영향을 끼치지 않는다. 변수는 함수 프로퍼티가 아니고 함수 프로퍼티는 변수가 아니기 때문이다.

클로저는 함수 프로퍼티로 대체할 수 있다. 변수의 유효범위와 클로저 챕터에서 살펴본 counter 함수를 함수 프로퍼티를 사용해 바꿔보자!

function makeCounter() {

  // let count = 0 대신 아래 메서드(프로퍼티)를 사용함

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
console.log( counter() ); // 0
console.log( counter() ); // 1

이제 count는 외부 렉시컬 환경이 아닌 함수 프로퍼티에 바로 저장된다.

그런데 과연 이렇게 함수 프로퍼티에 정보를 저장하는 게 클로저를 사용하는 것보다 나은 방법일까?

두 방법의 차이점은 count 값이 외부 변수에 저장되어있는 경우 드러난다. 클로저를 사용한 경우엔 외부 코드에서 count에 접근할 수 없고, 오직 중첩함수 내에서만 count 값을 수정할 수 있습니다.
함수 프로퍼티를 사용해 count를 함수에 바인딩시킨 경우엔 다음 예시와 같이 외부에서 값을 수정할 수 있다.

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
console.log( counter() ); // 10

따라서 구현 방법은 목적에 따라 선택하면 된다.

4. 기명 함수 표현식

기명 함수 표현식(Named Function Expression, NFE)은 이름이 있는 함수 표현식이다.

먼저, 일반 함수 표현식을 살펴보자.

let sayHi = function(who) {
  console.log(`Hello, ${who}`);
};

여기에 이름을 붙여보자!

let sayHi = function func(who) {
  console.log(`Hello, ${who}`);
};

이렇게 이름을 붙인다고 해서 뭐가 달라지며, "func"이라는 이름은 어떤 경우에 붙이는 걸까?

먼저 이렇게 이름을 붙여도 위 함수는 여전히 함수 표현식이라는 점에 주목해야한다. function 뒤에 "func"이라는 이름을 붙이더라도 여전히 표현식을 할당한 형태를 유지하기 때문에 함수 선언문으로 바뀌지 않는다.

이름을 추가한다고 해서 기존에 동작하던 기능이 동작하지 않는 일은 발생하지 않는다.

sayHi()로 호출하는 것도 여전히 가능하다.

let sayHi = function func(who) {
  console.log(`Hello, ${who}`);
};
sayHi("John"); // Hello, John

대신 func과 같은 이름을 붙이면 두 가지가 변화가 생기는데, 이 두 변화 때문에 기명 함수 표현식을 사용하는 것이다.

  1. 이름을 사용해 함수 표현식 내부에서 자기 자신을 참조할 수 있다.
  2. 기명 함수 표현식 외부에선 그 이름을 사용할 수 없다.

함수 sayHi를 예시로 이에 대해 살펴보자.
함수 sayHi는 who에 값이 없는 경우, 인수 "Guest"를 받고 자기 자신을 호출한다.

let sayHi = function func(who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    func("Guest"); // func를 사용해서 자신을 호출
  }
};

sayHi(); // Hello, Guest

// 하지만 아래와 같이 func를 호출하는 건 불가능하다
func(); // Error, func is not defined (기명 함수 표현식 밖에서는 그 이름에 접근할 수 없다.)

그런데 여기서 왜 중첩 호출을 할 때 sayHi대신 func을 사용했을까?

사실 대부분의 개발자는 아래와 같이 코드를 작성한다.

let sayHi = function(who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

하지만 이렇게 코드를 작성하면 외부 코드에 의해 sayHi가 변경될 수 있다는 문제가 생긴다. 함수 표현식을 새로운 변수에 할당하고, 기존 변수에 null을 할당하면 에러가 발생한다.

let sayHi = function(who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // TypeError: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // 중첩 sayHi 호출은 더 이상 불가능하다

에러는 함수가 sayHi를 자신의 외부 렉시컬 환경에서 가지고 오기 때문에 발생한다. 지역(local) 렉시컬 환경엔 sayHi가 없기 때문에 외부 렉시컬 환경에서 sayHi를 찾는데, 함수 호출 시점에 외부 렉시컬 환경의 sayHi엔 null이 저장되어있기 때문에 에러가 발생한다.

함수 표현식에 이름을 붙여주면 바로 이런 문제를 해결할 수 있다.

let sayHi = function func(who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    func("Guest"); // 원하는 값이 제대로 출력됩니다.
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (중첩 호출이 제대로 동작함)

"func"이라는 이름은 함수 지역 수준(function-local)에 존재하므로 외부 렉시컬 환경에서 찾지 않아도 된다. 외부 렉시컬 환경에선 보이지도 않는다. 함수 표현식에 붙인 이름은 현재 함수만 참조하도록 명세서에 정의되어있기 때문이다.

이렇게 기명 함수 표현식을 이용하면 sayHi나 welcome 같은 외부 변수의 변경과 관계없이 func이라는 '내부 함수 이름’을 사용해 언제든 함수 표현식 내부에서 자기 자신을 호출할 수 있다.

★함수 선언문엔 내부 이름을 지정할 수 없다.
지금까지 살펴본 '내부 이름’은 함수 표현식에만 사용할 수 있고, 함수 선언문엔 사용할 수 없다. 함수 선언문엔 ‘내부’ 이름을 지정할 수 있는 문법이 없다.
개발을 하다 보면 믿을만한 내부 이름이 필요할 때가 생기곤 하는데, 이 때 바로 함수 선언문을 기명 함수 표현식으로 다시 정의하면 된다.

profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글