들어가며
Array.prototype을 콘솔에 한 번 쳌본 적 있는가? forEach, map, filter... 우리가 늘 쓰던 배열 메서드가 주르르 나온다. MDN을 뒤지지 않아도 콘솔 한 줄로 배열이 가진 메서드를 전부 볼 수 있는 것이다. 어떻게 이게 가능할까?
이 질문 하나를 붙잡고 파고들다 보면 자바스크립트가 객체를 다루는 근본 원리, 즉 프로토타입에 닿는다. 배열 메서드가 어디에 선언되어 있는지, 함수는 무엇의 인스턴스인지, 클래스는 결국 무엇인지 — 얘핑 따로 노는 것 같은 다섯 가지 사실이 사실은 프로토타입이라는 하나의 실로 껰어져 있다. 이제 그 다섯 매듭을 하나씩 풀어보자.
1. 우리가 배열/객체/맵 등의 객체를 통해서 접근하는 메서드는 생성자 함수의 프로토타입 메서드이다.
배열을 만드는 방법에는 세 가지가 있다.
const array1 = [1,2,3];
const array2 = new Array(1,2,3);
const array3 = Array(1,2,3);
console.log(array1, array2, array3);
// [1, 2, 3], [1, 2, 3], [1, 2, 3]
Array는 왜 new를 붙여도 되고, 안붙여도 될까?
자바스크립트에서 Array는 빌트인 생성자 함수이다.
자바스크립트에서 Array는 편의성을 위해 특별히 new 키워드 없이 호출을 해도 자동으로 new Array로 호출하도록 만들어져있다.
Array 외에도 String, Number, Boolean, Object, RegExp도 동일한 원리로 동작한다.
[1,2,3]으로 배열 객체를 생성하는 방식을 배열 리터럴이라 하며, new Array(1,2,3)과 같이 생성자 호출 방식으로 배열 객체를 생성하는 것과 동일하다. 그 외에도 불리언, 부동 소수점, 숫자, 객체, 정규식, 문자열 등의 리터럴이 있다.
즉, 우리가 평소에 배열 리터럴 방식으로 배열을 만드는 것은 new Array()같이 배열 객체로 인스턴스를 만드는 것과 같다는 것을 의미한다. 덕분에 우리는 Array의 프로토타입 메서드에 접근할 수 있다. 이를 코드와 함께 살펴보자.
const array = [1,2,3];
console.log(array.__proto__ === Array.prototype); // true
__proto__그냥 써도 되나요?결론부터 말하면, 공부용으로는 편하지만 실제 코드에선 지양하는 게 좋다.
__proto__는 사실 표준이 아니라 브라우저들이 관행적으로 지원하던 걸 나중에 호환성 때문에 명세에 겨우 얙은, 이미 deprecated된 레거시 기능이다. 부모 프로토타입을 읽고 싶으면 표준 메서드인Object.getPrototypeOf(array)를, 설정하고 싶으면Object.setPrototypeOf()를 쓰는 게 맞다. 이 글에서는 프로토타입 관계를 눈으로 보기 편하라고__proto__를 그대로 쓰지만, 표준은 따로 있다는 걸 기억해두자.
우리가 만든 배열 인스턴스의 부모 프로토타입은 Array의 프로토타입과 참조값이 같다는 것을 확인할 수 있다.
또한 Array의 프로토타입에는 forEach, find, filter 등 우리가 보통 배열 인스턴스를 통해서 사용하는 메서드들이 선언되어있다.
2. 객체를 생성하는 생성자 함수는 Function의 인스턴스이다.
어떻게 보면 당연한 일이다. 생성자 함수니깐 당연히 함수이고, 그렇기에 모든 함수는 Function의 인스턴스이다. 이를 코드와 함께 살펴보자.
console.log(Array.__proto__ === Function.prototype); // true
객체를 생성하는 생성자 함수 뿐 아니라 일반 함수와 화살표 함수 또한 Function의 인스턴스이다.
function func1(){}
const func2 = () => {};
console.log(func1.__proto__ === Function.prototype)
console.log(func2.__proto__ === Function.prototype)
화살표 함수는 프로토타입이 없지 않나요?
화살표 함수는 자신의 프로토타입을 가지지 않는다. 하지만 프로토타입 체인 상에 존재는 한다. 위의 코드에서 접근한
__proto__는 부모 프로토타입에 접근한 것이며, 부모 프로토타입이 Function의 프로토타입인 것을 확인할 수 있다.그 외에 화살표 함수의 특징에 대해서도 알려줘요 !
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
3. Function의 부모 프로토타입은 자신인 Function의 프로토타입이다.
console.log(Function.__proto__ === Function.prototype); // true
Function은 함수를 만드는 생성자 함수이다. 모든 함수는 Function의 인스턴스이므로, Function 자신도 Function의 인스턴스가 되어야 한다. 그래서 이런 순환 참조 구조가 됐다.
그런데 여기서 궁금해진다. 이렇게 프로토타입을 타고 부모의 부모로 계속 올라가면 끝은 어디일까? 종점은 Object.prototype이다. Function이든 Array든 결국 __proto__를 계속 따라가면 Object.prototype에 도달한다.
console.log(Function.prototype.__proto__ === Object.prototype); // true
그리고 이 Object.prototype의 부모는 없다. 즉 체인의 진짜 끕이다.
console.log(Object.prototype.__proto__); // null
왜 null일까? 여기가 프로토타입 체인의 최상단이기 때문이다. 우리가 어떤 객체에서 프로퍼티를 찾을 때 자바스크립트는 프로토타입 체인을 타고 올라가며 찾는데, Object.prototype까지 올라와도 없으면 더 올라갈 곳이 null이니 그제서야 undefined를 반환하고 탐색을 멈춘다. 3번의 순환 참조(Function ↔ Function.prototype)와 헷갈리기 쉬운데, 그건 ‘함수를 만드는 함수’라는 특수한 관계에서 생긴 국소적인 고리일 뿐이고, 상속 체인 전체의 종착지는 언제나 Object.prototype → null이다.
4. 클래스 인스턴스의 부모 프로토타입은 클래스의 프로토타입이다.
class Person{}
const person = new Person();
console.log(person.__proto__ === Person.prototype)
이는 클래스도 내부적으로 프로토타입 기반으로 만들어졌음을 의미한다.
5. 클래스는 하나의 생성자 함수이다.
class Person{}
const person = new Person();
console.log(Person.__proto__ === Function.prototype);
즉 우리가 생성자 함수 인스턴스를 만들 때 new 키워드와 함께 호출하듯이, 클래스도 new 키워드와 함께 호출하는 것은 클래스가 곧 생성자 함수이기 때문이다.
맺으며
자바스크립트 생태계는 파면 팔수록 참 흥미롭고 재미난 거 같다. 우리가 사용하는 배열 메서드가 실제로는 배열 생성자 함수의 프로토타입에 선언된 메서드이고, 우리는 이를 __proto__이 생략된 채로 접근한다니 이 얼마나 흥미로운가.
이 프로토타입이 실제로 개발하면서 얼마나 크게 도움이 될 지는 아직 잘 체감이 되지 않는다. 하지만 의문을 해소했을 때의 쾌감이 있다는 것, 그리고 언젠가는 도움이 되지 않을까 하는 희망에 공부를 하고는 한다.
그럼에도 언젠가는 도움이 될 거라는 것은 확신한다. ‘배열 메서드에 어떤게 있을까?’하는 궁금증에 MDN에 Array를 검색해볼 수도 있겠지만, Array.prototype을 콘솔에 입력하면 배열 메서드를 모두 확인할 수 있듯이 말이다. 이러한 기본 원리를 아는 것이 언젠가 유연하게 코드를 짤 수 있는 큰 힘이 되지 않을까.