자바스크립트는 프로토타입 기반 언어이다.
클래스 기반 언어에서는 '상속'을 사용하지만 프로토타입 기반 언어에서는
어떤 객체를 원형으로 삼고 이를 복제(참조)함으로써 비슷한 효과를 얻는다.
프로토타입이란 대체 무엇일까?
자바스크립트의 모든 함수는 prototype
객체를 프로퍼티로 갖는다.
그리고 이 함수를 생성자 함수로서 호출하여 인스턴스를 생성하면
이 인스턴스는 __proto__
라는 프로퍼티를 갖는다.
__proto__
는 생성자 함수의 prototype
을 그대로 참조한다.
prototype
과 __proto__
프로퍼티가 왜 자동으로 생성되는 것이며
이 둘은 어떤 의미를 담고 있을까?
프로토타입에 대한 설명을 쭉 이어나간 뒤에 위 의문들을 다시 짚어보도록 하겠다.
var Person = function (name) {
this.name = name;
};
Person.prototype.greet = function () {
console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};
var iu = new Person('지은');
iu.__proto__.greet(); // 안녕하세요, 저는 undefined입니다.
iu.greet(); // 안녕하세요, 저는 지은입니다.
Person
생성자 함수로부터 iu
라는 객체를 생성하였다.
이 iu
객체는 name
과 __proto__
프로퍼티를 보유한다.
__proto__
은 생성자 함수인 Person
의 prototype
을 참조한다.
따라서 __proto__
을 통해 Person
의 prototype
에 정의된 함수를 자유롭게 사용할 수 있다.
= greet
함수를 자유롭게 호출할 수 있다.
위 코드에서 iu.__proto__.greet();
을 실행하여 프로토타입에서 정의한 greet
함수를 실행한 결과 undefined
가 출력되었다.
그 이유는 this.name
을 출력하려고 할 때 this
가 iu.__proto__
로 바인딩 되었기 때문이다.
Person
의 prototype
에는 name
프로퍼티가 존재하지 않기 때문에 undefined
를 출력하였다.
그러나 __proto__
을 생략하여 Person
의 prototype
의 함수를 호출할 수 있다!
따라서 iu.greet();
을 실행하면 this
는 iu
객체로 바인딩되어 this.name
을 정상적으로 출력하게 된다.
이처럼 객체는 __proto__
을 생략하여 생성자 함수의 prototype
에서 정의한 메소드들을
마치 자신의 메소드인 것처럼 자유롭게 사용할 수 있다.
생성자 함수의 프로퍼티인 prototype
객체 내부에는 constructor
라는 프로퍼티가 있다.
인스턴스의 __proto__
객체 내부에도 마찬가지이다.
이 프로퍼티는 단어 그대로 원래의 생성자 함수(자기 자신)을 참조한다.
인스턴스로부터 그 원형이 무엇인지 알고 싶다면 이 프로퍼티를 사용하면 된다.
var iu = new Person('지은');
var arr = [1, 2, 3];
console.log(iu.constructor); // [Function: Person]
console.log(arr.constructor); // [Function: Array]
iu
는 Person
생성자 함수로부터 생성된 객체이다.
따라서 iu.constructor
을 출력하면 그 원형인 Person
이 출력된다.
arr
은 Array
생성자 함수로부터 생성된 객체이다. (배열도 객체다.)
따라서 arr.constructor
을 출력하면 그 원형인 Array
가 출력된다.
prototype
객체를 참조하는 __proto__
을 생략하면
인스턴스는 prototype
에 정의된 프로퍼티나 메서드를 마치 자신의 것처럼 사용할 수 있다.
그러나 만약 인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 갖고 있다면 어떨까?
var Person = function (name) {
this.name = name;
};
var iu = new Person('지은');
iu.greet = function () {
console.log(`안녕! 난 ${this.name}이야!`);
};
Person.prototype.greet = function () {
console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};
iu.greet(); // 안녕! 난 지은이야!
Person
의 prototype
에도 greet
메서드를 정의하고
iu
객체에서도 동일한 이름의 greet
메서드를 정의해두었다.
iu.greet();
을 호출하면 prototype
의 greet
메서드는 무시되고
iu
객체에서 정의한 greet
메서드가 호출된다.
이는 greet
메서드가 오버라이딩 되었기 때문이다!
자바스크립트 엔진이 greet
메서드를 찾는 방식은
가장 가까운 대상인 자신의 프로퍼티를 검색하고,
없으면 그 다음으로 가까운 대상인 __proto__
를 검색하는 순서로 진행된다.
🚨 여기서 중요한 점
모든prototype
에는Object.prototype
을 참조하는__proto__
프로퍼티가 존재한다.
이는prototype
이 객체이기 때문이다!
따라서 위의 예제 코드에서 greet
메서드를 찾기 위해,
iu → Person → Object
순서로 메서드를 검색한다.
만약 greet
메서드를 찾았다면 검색을 멈춘다.
이처럼 어떤 데이터의 __proto__
프로퍼티 내부에
다시 __proto__
프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인 이라고 한다.
또한 이 체인을 따라가며 검색하는 것을 프로토타입 체이닝이라고 한다.
생성자 함수와 객체에서 prototype
과 __proto__
프로퍼티가 왜 자동으로 생성되는 것이며
이 둘은 어떤 의미를 담고 있을까?
prototype
은 생성자 함수를 통해 생성한 여러 객체 혹은 이를 상속받은 여러 객체가
공통적으로 사용할 메소드를 정의하는 역할을 한다.
객체는 __proto__
을 통해 상위 프로토타입 체인에 접근할 수 있고,
이를 통해 상속받은 메서드를 자신의 것처럼 사용할 수 있다!
아래는 Student
가 Person
을 상속받기 위해 프로토타입을 활용한 예시이다.
var Person = function (name) {
this.name = name;
};
var Student = function (name, grade) {
Person.call(this, name); // Person의 생성자 호출
this.grade = grade;
};
Person.prototype.greet = function () {
console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};
Student.prototype.study = function() {
console.log(`${this.name} 학생이 공부 중입니다.`);
};
// Student가 Person을 상속하도록 설정
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
var iu = new Student('지은', 3);
iu.greet(); // 안녕하세요, 저는 지은입니다.
iu.study(); // 지은 학생이 공부 중입니다.
Person.call(this, name);
전달받은 name
인자를 Student
의 this.name
에 저장해주기 위해 call
을 이용하였다.
이를 통해 this
가 iu
객체를 바라보도록 한다.
Student.prototype = Object.create(Person.prototype);
Object.create
을 통해 Person.prototype
을 원본 객체로 하는 새로운 객체를 생성하였다.
그리고 이 객체를 Student
의 prototype
객체로 할당해주었다.
위 과정을 통해 iu
객체는 부모 객체인 Person
의 prototype
에 접근할 수 있다.
Student.prototype.constructor = Student;
부모 생성자 함수인 Person
의 프로토타입 객체를 토씨 하나 안바꾸고 그대로 복제했기 때문에,
새롭게 생성한 자식 생성자 함수인 Student
의 프로토타입 객체의 constructor
프로퍼티는
여전히 부모 생성자 함수인 Person
을 참조하고 있다.
하지만 자식 생성자 함수인 Student
를 통해 생성된 객체가
Person
를 사용하여 생성된 것처럼 처리되면 안되므로,
다시 constructor
프로퍼티를 Student
로 변경해줘야한다.
이처럼 위의 프로토타입을 이용하면 상속 관계를 정의할 수 있고
Student
생성자 함수를 통해 생성된 iu
객체가 greet()
함수를 마치 자신의 함수인 것처럼 사용할 수 있다!
prototype
과 __proto__
프로퍼티는 이처럼 여러 객체가 공통적으로 사용할 기능을 정의할 수 있도록 돕기 때문에
자바스크립트에서 상속과 재사용의 패턴을 효율적으로 구현할 수 있게 해준다!
📎 참고 자료
https://evan-moon.github.io/2019/10/27/inheritance-with-prototype/