왜 함수는 객체인걸까?

“모든 함수는 객체지만, 모든 객체가 함수인 것은 아니다.”

위의 소제목이 이번 포스팅의 골자다.
프로토타입을 공부하다 보니, 자꾸 Function 프로토타입의 __proto__가 객체라는 것이 직관적으로 와닿지가 않아 한번 제대로 짚고 넘어가보고 싶었다.

우선, 우리가 흔히 알고있듯, 자바스크립트에서 자료형은 원시타입참조타입으로 나뉘어진다.

원시타입에는 [number, string, boolean, null, undefined, symbol]이 있다. 그리고 그 이외에는 모두 참조타입이다. 참조타입에는 [Object, Array, Function]이 있다.

이런 줄만 알았지...

근데 모든 참조타입의 원형은 객체라고 한다. 이 때문에 함수건, 배열이건, 프로토타입을 통해 확인해보면 최상위 프로토타입이 Object로 찍히는 것이다. 필자는 그동안 코딩을 하며 위의 사진처럼 굉장히 단순하게만 자료형을 인식하고 있었다. 하지만 얕았던 이러한 생각에 점차 금이 가기 시작했고, 이왕 궁금증과 찝찝함이 생긴 김에 제대로 짚고 넘어가보고자 한다.


객체란?

함수가 왜 “객체”인지 이해하기 위해선, 당연히 객체가 무엇인지 먼저 이해해야 한다.
”고래는 왜 포유류인가?”라는 질문에서, 포유류가 뭔지도 모르는데 고래가 왜 포유류인지 설명할 수는 없는 것처럼 말이다.

일단 각설하고, 본론으로 들어가자면 객체란 키-값 쌍으로 이루어진 참조형 데이터 타입이다. 키-값 쌍은 아래와 같이 표현된다.

1
2
3
4
const obj = {
key: value, // value에는 거의 모든 것들이 할당될 수 있다.
key: method, // 심지어 method처럼 함수가 값으로 할당될 수도 있다.
};

그리고 참조 타입은, 객체의 모든 연산이 실제값이 아닌 참조값으로 처리되는 자료형을 뜻한다. 조금 더 쉽게 말하면 데이터 자체를 저장하는 것이 아니라, 주소를 저장해둔 뒤 해당 주소를 통해 참조하고 있는 데이터에 접근해 값을 계산 및 활용한다는 것이다. 아래의 예시를 확인해보자.

1
2
3
4
5
6
// 이와 같이 객체를 생성했고,
const foo = {
age: 20,
};
// bar에 foo를 할당했다.
const bar = foo;

참조타입

그러면 이와 같이, foo와 bar은 모두 같은 값을 참조하고 있음을 확인할 수 있다. 여기서 다시금 강조하고싶은 것은 ‘참조’하고 있다는 것인데, 이에 따라 age가 30으로 변하면 foo bar 모두 변경된 값을 똑같이 참조한다.

그렇다면 아래와 같은 경우는 어떨까?

1
2
3
4
5
6
7
const foo = {
age: 20,
};

const bar = {
age: 20,
};

두 번째 예시에서는 육안으로 봤을 때 동일해 보여도, 두 값이 서로 다른 데이터를 참조하고 있기 때문에 서로 완전히 다른 참조를 하고 있는 것이다. 따라서 foo의 age가 50으로 바뀌어도, bar이 참조하는 데이터는 완전히 독립적인 다른 데이터이므로 영향을 주지 않는다.

마지막으로 원시타입은 이하와 같은 모양일 것이다.

원시타입

1
2
3
const foo = 20;

// 이렇게 foo에 number 원시타입을 할당해주면, 이는 참조가 아니라 데이터를 직접 저장하는 방식을 취한다.

객체 요약

이처럼, 객체는 키-값 쌍으로 이루어져 있으며, 참조 타입이다. 키-값 쌍에는 프로퍼티와 메소드가 할당될 수 있다. 그리고 자신을 참조하고 있는 다른 변수들은 자신의 데이터를 ‘참조’만 할 뿐, 그 데이터 값을 직접적으로 저장하여 활용하는 것이 아니다. 따라서 참조값이 변하면 변수들은 변경된 참조값을 동일하게 참조하게 된다.

그럼 이제 객체가 무엇인지 이해했으니, 함수가 왜 객체인지에 대해서 이해해보도록 하자.


함수가 왜 객체인가?

정확히 말하면 “함수 객체”다.
함수 객체는 기본적으로 객체의 기본적인 기능들을 사용할 수 있을 뿐만 아니라, 함수 객체만의 고유한 프로퍼티와 메소드들이 존재한다.

즉, 함수객체는 기존 객체에서 조금 더 확장된 개념이라고 봐도 될 것 같다. 객체의 내부 슬롯과 내부 메소드, 그리고 이에 더해 함수 객체만의 [[Envorinment]], [[FormalParameters]] 라는 내부 슬롯 + [[Call]], [[Constructor]] 같은 내부 메소드도 추가적으로 보유하고 있기 때문이다.

글로만 봐서는 이해하기가 어렵기 때문에 아래의 사진들과 함께 훑어 내려가보도록 하자.

hasOwnProperty 에러 미발생

위의 코드의 실행이 정상적으로 되는 이유가 뭘까..? 분명 test는 함수고, Function의 프로토타입에도 hasOwnProperty라는 메소드는 없다. 즉, test에는 존재하지 않는 메소드가 오류 없이 실행된 것이다.

위 사진에서도 볼 수 있듯, hasOwnProperty는 Object 프로토타입에만 존재한다.

함수에서도 이 메소드의 사용이 가능한 이유는 결국 Function 프로토타입의 __proto__Object의 프로토타입과 연결되어 있기 때문이다(프로토타입 체인). 이를 쉽게 설명하자면, test의 프로토타입인 Function에 hasOwnProperty가 없기 때문에 한 단계 더 위의 프로토타입인 Object 프로토타입 객체에서 메소드를 끌어온 것이다.

즉, 함수 상위에는 객체가 존재한다. 함수는 객체에 포섭된다!

함수의 조상은 객체다.

Object는 함수객체의 조상격

콘솔창에 직접 Function.prototype.__proto__를 찍어보면 위와 같은 결과가 나올 것이다. Function의 프로토타입이 참조하고 있는 것은 결국 객체고, 이 Function은 객체로부터 파생된 것이라고 볼 수 있다. 이를 증명하는 것이 바로 constructor다. Function은 결국 Object()라는 생성자 함수에 의해 생성된 것이고, 이는 Function이 Object 프로토타입으로부터 메소드와 프로퍼티를 상속받을 수 있음을 의미한다.

그리고 함수 객체는 아래와 같이 자신만의 고유한 프로퍼티와 메소드를 보유한다.

함수객체만의 프로퍼티와 메소드

apply, bind, call은 이전에 this에 대한 포스팅을 했을 때 다뤘던 메소드들이다. 그리고 이외에도 함수는 length와 name과 같은 함수만의 프로퍼티도 보유한다.

함수 요약

정리하자면, 함수는 하나의 객체다. 그리고 이러한 함수는 Object로부터 파생 된 것인데, 이러한 특성 덕분에 함수는 Object 프로토타입으로부터 메소드와 프로퍼티를 상속받을 수 있다. 그리고 객체의 기본적인 기능 이외에도 함수만이 갖는 apply, bind, length, name 등 고유의 프로퍼티와 메소드를 보유한다. 따라서 함수는 객체인 것이다.

뭐 이제 함수가 객체라는 것은 어느 정도 감이 잡혀가는 것 같다.
근데 더 알아야 할 것이 있다. 자바스크립트에서 함수는 그냥 객체가 아닌 ‘1급 객체’라는 것이다.


자바스크립트에서의 함수는 “일급 객체”다.

쉽게 말해, 일등 시민과 비슷한 맥락이라 봐도 된다. 일급객체는 일급시민과 같이 누릴 수 있는 권리와 자유도가 굉장히 높다.

이러한 일급 객체가 되기 위한 조건은 크게 총 세 가지다.


  1. 데이터 구조 또는 변수에 할당될 수 있다. (+무명의 리터럴로 표현 가능)
  2. 파라미터로써 어딘가에 전달될 수 있다.
  3. 리턴 값으로써 사용될 수 있다.

함수가 데이터 구조 또는 변수에 할당되는 경우.

바로 코드로 설명해도록 하겠다.

1
2
3
4
5
6
7
8
9
10
// 변수에 함수가 할당됐다. + 무명의 리터럴로 함수명이 생략된 모습
let someFunction = function(){};

// 배열 안에 함수가 삽입됐다.
const arr = ['javascript', 'react', someFunction];

// 객체 안의 메소드로써 사용될 수 있다.
const obj = {
someFunction: function(){};
};

함수가 파라미터로 활용되는 경우

1
2
3
4
5
6
7
8
9
10
11
// 일반적인 함수의 선언
function someFunction() {
return 'Hello';
}

// 함수를 매개변수로 활용하는 또 다른 함수.
function takeFunction(func) {
return func();
}

console.log(takeFunction(someFunction)); // Hello

함수가 리턴값으로써 활용되는 경우

1
2
3
4
5
6
7
8
9
10
11
function aboutMySelf() {
const name = 'hoon';
return function () {
return `my name is ${name}!`;
};
}
// 리턴값으로 함수를 받아준다.
const func = aboutMySelf();

// 그리고 그 함수를 실행시킨다.
console.log(func()); // my name is hoon!

이처럼 자바스크립트에서 함수는 굉장히 자율성이 높다. 어디든 끼어들 수 있고, 어디서든 자유롭게 활용될 수 있기 때문이다. 이러한 이유 때문에 함수가 1급 객체 라고 불리는 것이다.


함수가 인식되는 과정

마지막으로 짚고 넘어가야할 것이 있다.
콘솔창에 typeof 함수를 찍으면, object가 아닌 function이 반환된다는 것이다.

진짜 뭔 소린가 싶다. 위에서 실컷 “함수는 객체다. 함수는 객체다” 노래를 불렀는데.. 함수가 함수로 찍힌다니?

보이다시피, func 함수의 타입이 function으로 반환되는 모습이다. (물론 타입만 function으로 ‘인식’되는 것이지, 함수의 실제 자료형은 객체가 맞다)

왜 자바스크립트는 function의 타입을 객체가 아닌 함수로 인식하는 것일까?

[[Call]] : 호출이 될 수 있는가 vs 없는가

호출될 수 있는 객체는 function으로, 없는 것들은 모조리 object로 인식한다는 것이 핵심이다.
이는 아래에서 다루겠지만, 호출될 수 있는 객체는 callable이라고 표현한다.

우선 호출, 표현, 선언, 생성자, 화살표함수 라는 ‘함수 정의’의 개념부터 확실히 학습하고 넘어가보도록 하자. 이는 callable을 이해하는데 핵심적이기 때문이다. 위 네가지 함수 정의를 통해 (함수)객체는 [[Call]] 이라는 메소드를 보유하게 되고, 객체는 callable한 객체가 되어 함수로써 인식된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 함수 선언문
function sayHello() {
return 'Hello';
}

// 함수 표현식
const sayHello = function () {
return 'Hello';
};

// 생성자 함수 사용
const guest = new sayHello();

// 화살표 함수 사용
const sayHello = () => {};

즉, 위와 같이 우리가 알고있던 방식들로 함수를 정의하면, 해당 객체(함수)는 [[Call]] 메소드를 보유하게 되면서 함수로 인식된다. 하지만 이 부분이 조금 직관적으로 이해가 되지 않아서 ECMAScript를 찾아봤다. 해당 문서에서는 [[Call]] 내부 메소드를 보유한 객체는 곧 함수 객체이며, 함수가 호출될 때 이 [[Call]] 내부 메소드가 작동하는 것이라 설명하고 있다. 즉 이렇게 [[Call]] 내부 메소드를 보유하며 호출할 수 있는 객체를 함수 객체라고 정의할 수 있으며, 이는 곧 함수라고 통칭된다. ([[Call]]이 더 궁금하다면 ? ECMAScript-262)

함수가 정의되면 [[Construct]] 메소드를 보유하는 함수 객체로 생성된다. 이 [[Construct]] 를 통해 자바스크립트는 함수를 object가 아닌 function으로 인식하는 것이다.

자, 이제 확실히 정리가 된 것 같다. 함수 객체는 호출될 수 있는 객체고, 그 외의 객체들은 호출될 수 없는 객체다.

[[Construct]] : 생성자인가 vs 아닌가

진짜 마지막이다.
함수 객체가 갖는 고유의 메소드 중에 중요한 마지막 하나는, [[Construct]] 다.

ECMAScript에서는 [[Construct]] 에 대해 이렇게 설명한다. new 또는 super를 통해 함수가 호출될 때 [[Construct]] 메소드가 호출되며 실행된다. 하지만 이러한 [[Construct]] 메소드가 존재하지 않는 함수 객체도 있는데, 이를 non-constructor 함수 객체라고 부른다.

constructor vs non-constructor의 구분은 굉장히 간단하다.

화살표 함수와 메소드만 non-constructor다.

1
2
3
4
5
6
7
8
9
10
// 화살표 함수
const apple = () => {};
// 메소드
const obj = {
tomato() {},
};

//Uncaught TypeError: apple is not a constructor
const foo = new apple();
const bar = new obj.tomato();

이전 포스팅에서 Prototype에 대해서 다뤘을 때, constructor의 지위가 있는 함수만 생성자 함수가 될 수 있다고 했었다. 이처럼 모든 함수가 생성자(constructor)의 자격이 있는 것은 아니다.


정리 및 요약

포스팅 제목과 같다.
”모든 함수는 객체지만, 모든 객체가 함수인 것은 아니다”

자바스크립트에서 함수는 객체다. 특히, Object라는 최상위 프로토타입으로부터 파생된 객체다. 함수 객체는 일반 객체의 기본 기능 이외에도, [[Call]], [[Construct]], apply, bind, call, length, name 과 같은 함수 객체만의 프로퍼티와 메소드를 보유한다. 그리고 Function이 참조하는 프로토타입을 확인해보면, Object가 반환된다. 이는 함수 객체가 Object 프로토타입으로부터 생성된 것이라는 것을 증명한다.


Author

Hoonjoo

Posted on

2022-03-30

Updated on

2022-03-30

Licensed under

Comments