함수와 일급 객체 정리
함수가 "일급 객체"라는 표현은 들어봤지만, 정확히 어떤 조건을 만족해야 그렇게 부르는지, 그리고 함수 객체가 일반 객체와 무엇이 다른지 설명하라고 하면 막막했다. 모던 자바스크립트 Deep Dive를 따라가며 일급 객체의 정의 → 함수 객체 고유의 프로퍼티 순서로 짚었다. 짚는 과정에서 책의 표가 현재 사양과 어긋난 부분이 몇 군데 있어 그것도 같이 정리해뒀다.
함수가 왜 일급 객체로 분류되는가
일급 객체는 다음 네 조건을 만족하는 객체다.
- 무명의 리터럴로 생성할 수 있다 (런타임 생성 가능).
- 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.
- 함수의 매개변수에 전달할 수 있다.
- 함수의 반환값으로 사용할 수 있다.
자바스크립트의 함수는 네 조건을 모두 만족한다. 그래서 함수는 일급 객체로 분류된다. 변수 할당문, 객체의 프로퍼티 값, 배열의 요소, 함수 호출의 인수, 함수의 반환문처럼 값을 쓸 수 있는 자리라면 어디서든 함수 리터럴로 정의할 수 있고, 런타임에서 객체로 평가된다.
이 가운데 가장 강력한 특징은 "매개변수로 전달할 수 있고 반환값으로 쓸 수 있다"는 두 가지다. 이 두 가지가 함수형 프로그래밍을 가능하게 한다.
다만 함수 객체는 일반 객체와 다른 점이 하나 있다. 일반 객체는 호출할 수 없고, 함수 객체는 호출할 수 있다. 그리고 함수 객체에는 일반 객체에는 없는 고유의 프로퍼티가 따로 있다.
함수 객체의 프로퍼티는 생각보다 단순하지 않았다
책의 표를 보면 함수 객체의 프로퍼티가 데이터 프로퍼티와 접근자 프로퍼티로 깔끔하게 갈리는 것처럼 보였다. 그런데 실제 사양을 따라가 보니 "함수 고유의 프로퍼티"라는 라벨을 일률적으로 붙이기는 어려웠다.
| 분류 | 프로퍼티 |
|---|---|
| 데이터 프로퍼티 | length, name, prototype (함수 고유) |
| 접근자 프로퍼티 | __proto__ (Object.prototype에서 상속) |
arguments와 caller는 사정이 다르다. ECMAScript 사양상 Function.prototype의 poison-pill accessor로 정의되어 있다. strict 모드, 화살표 함수, async 함수, 제너레이터 함수에서 접근하면 TypeError가 발생하는 비표준/deprecated 프로퍼티다. 그래서 데이터 프로퍼티로 단정하기 어려웠고, "함수 고유의 프로퍼티"라고 묶는 것도 정확하지 않다는 걸 알게 됐다.
아래부터는 각 프로퍼티가 실제로 어떤 일을 하는지 하나씩 살핀다.
arguments — 가변 인자를 받을 때 손에 잡히던 객체
함수 객체의 arguments 프로퍼티 값은 arguments 객체다. 함수가 호출될 때 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체다. 함수 내부에서 지역 변수처럼 사용되고, 함수 외부에서는 참조할 수 없다.
호출 시 전달된 인수가 매개변수 개수보다 많아도 초과된 인수는 버려지지 않고 arguments 객체에 보관된다. 인수 개수를 확인하고 그에 따라 함수의 동작을 다르게 가져가야 할 때, 그리고 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용했다.
arguments 객체는 유사 배열 객체이면서 동시에 이터러블이다. 이터러블은 이터레이션 프로토콜을 준수해 순회 가능한 자료구조를 말한다.
arguments 객체는 callee 프로퍼티를 갖는다. callee는 호출되어 arguments 객체를 생성한 함수, 즉 함수 자신을 가리킨다. 그런데 arguments.callee는 ES5 strict 모드에서 사용이 금지되었다. strict 모드에서 접근하면 TypeError가 발생한다. 별개로 Function.arguments(함수 객체의 arguments 프로퍼티)는 어떤 표준에도 포함되지 않은 비표준 프로퍼티이며 deprecated 상태다. 그래서 Function.arguments와 같은 사용법은 권장되지 않는다.
arguments는 배열이 아니라 유사 배열이라 map, filter 같은 배열 메서드를 바로 쓸 수 없다. 그래서 배열로 변환해 쓰는 패턴이 따라붙는다.
function sum() {
const args1 = Array.from(arguments);
const args2 = [...arguments];
const args3 = Array.prototype.slice.call(arguments);
return args1.reduce((acc, cur) => acc + cur, 0);
}세 가지 다 결국 배열을 만든다는 점은 같다. 다만 ES6의 Rest 파라미터를 쓰면 처음부터 배열로 받을 수 있어 arguments를 거치지 않고 끝난다.
function sum(...args) {
return args.reduce((acc, cur) => acc + cur, 0);
}Rest 파라미터는 정해지지 않은 수의 파라미터를 배열로 묶어주는 문법이다. ...args 형태로 매개변수에 선언한다.
caller — 비표준이라 깊게 들어가지 않았다
caller 프로퍼티는 함수 자신을 호출한 함수를 가리킨다. 다만 ECMAScript 사양에 포함되지 않은 비표준 프로퍼티라 짚고 넘어가는 정도로만 봤다.
length — arguments의 length와는 다른 값을 가리킨다
length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수다. 같은 이름이지만 arguments 객체의 length와 의미가 다르다.
arguments객체의length: 호출 시점에 실제로 전달된 인수의 개수- 함수 객체의
length: 함수가 선언될 때 정의된 매개변수의 개수
호출이 어떻게 들어오든 함수 객체의 length는 고정이다.
name — ES5와 ES6 사이에서 가장 헷갈렸던 프로퍼티
name 프로퍼티는 함수 이름을 나타낸다. 책에서는 ES5와 ES6의 동작 차이를 짧게 짚었는데, 사양을 따라가 보니 그 차이가 단순히 "빈 문자열에서 식별자로 바뀐 것"은 아니었다.
ES5에서는 name 프로퍼티가 표준이 아니었다(비표준 구현). 변수에 할당된 익명 함수 표현식의 name은 ES5에서 빈 문자열이었다.
ES6부터 name이 정식 표준이 됐다. 익명 함수 표현식이 변수·메서드·기본값 같은 구문 위치에 놓이면, 그 위치의 식별자를 name으로 추론해 채운다(name inference).
단, 익명 함수 표현식 자체의 name은 ES6에서도 빈 문자열이다.
(function () {}).name // ""빈 문자열에서 식별자로 바뀐 게 아니라, 특정 구문 위치에서 추론이 적용되는 것뿐이라는 걸 알게 됐다.
proto — 상속받은 접근자, 함수 고유 프로퍼티가 아니다
모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. 객체지향 프로그래밍의 상속을 구현하는 프로토타입 객체를 가리키는 자리다. [[Prototype]] 내부 슬롯에 직접 접근할 수는 없고, __proto__ 접근자 프로퍼티를 통해 간접적으로 접근한다.
__proto__는 Object.prototype에서 상속받은 프로퍼티다. 함수 객체에만 있는 프로퍼티가 아니라는 점이 중요했다. 함수 객체가 Object.prototype을 상속받기 때문에 함수 객체에서도 쓸 수 있는 것이다.
상속받은 프로퍼티인지 객체 고유의 프로퍼티인지 확인할 때는 hasOwnProperty 메서드를 썼다. 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키이면 true, 상속받은 프로토타입 프로퍼티이면 false를 반환한다.
prototype — constructor만이 가진다
prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor만이 소유한다. non-constructor에는 prototype 프로퍼티가 없다.
함수가 객체를 생성하는 생성자 함수로 호출될 때, prototype은 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.
함수 객체의 프로퍼티를 한 바퀴 돌고 나서야 책의 표가 왜 헷갈렸는지 알았다. arguments와 caller가 데이터 프로퍼티 칸에 같이 들어가 있었던 게 가장 큰 혼선의 원인이었다. 실제로는 length, name, prototype이 데이터 프로퍼티이고, __proto__는 상속받은 접근자, arguments와 caller는 사양상 따로 떼어서 봐야 하는 비표준/deprecated 프로퍼티다.