자바스크립트에서는 다른 상속 기반의 언어와 달리, prototype
(원형) 기반의 언어로, 모든 객체는 특정 객체를 원형(prototype
)으로 삼고 이를 복제(참조)하는 방식을 통해 상속과 비슷한 효과를 가지게 한다.
프로토타입을 추상화 하면 위와 같은 그림이 나오는데 이 그림은 다음의 의미를 가진다.
Constructor
)은 원형(prototype
)을 속성으로 가진다.new
키워드를 결합하면 인스턴스(instance
) 가 생성된다.__proto__
속성을 가진다.__proto__
속성은 생성자의 prototype
을 참조한다.let Person = function (name) {
this._name = name;
}
Person.prototype.getName = function () {
console.log('call getName method');
return this._name;
}
위와 같이 생성자 함수 Person
을 정의하고 생성자 함수의 prototype
에getName
이라는 메소드를 정의하였다.
const j113 = new Person("seungchan");
j113.__proto__.getName();
// call getName method
// undefined
j113
이라는 인스턴스를 생성해주었고 이 인스턴스의 prototype
에 정의된 getName
이라는 메소드를 호출하였다. 이때 결과로 undefined
가 나오고 에러가 발생하지는 않는다.
Person.prototype === j113.__proto__ // true
참고로, 인스턴스에서 호출하는 __proto__
는 Person
의 prototype
과 동일하기에 getName
메소드를 호출 할 수 있다.
이를 통해 알 수 있는 것은 prototype
에 정의된 getName
메소드에는 잘 도달하지만 getName
에서 반환하는 this._name
이 문제가 될 것이다. 이는 getName
메소드에서 this
가 무엇인지를 확인하면 원인파악이 되는데 getName
메소드를 호출하는 주체가 j113.__proto__
이고 이는 곧 Person
의 prototype
인데 여기에는 _name
이라는 것이 존재하지 않는다. 그래서 위의 예시에서 undefined
가 나오게 된 것이다.
// Person 생성자함수 정의는 생략
Person.prototype._name = "seungchan"
const j113 = new Person("seungchan");
j113.__proto__.getName();
// seungchan
실제로 위에 처럼 Person
의 prototype
자체에 _name
프로퍼티를 정의해 놓으면 getName
이 원하는대로 출력되는 것을 확인할 수 있다.
const j113 = new Person("seungchan");
j113.getName();// seungchan
또한, 인스턴스에서 __proto__
라는 키워드를 생략한뒤 메소드를 호출하면 this
가 인스턴스가 됨으로써 원하는 결과가 나오게 된다. 여기서 인스턴스에서 어떻게 prototype
으로 접근이 가능한지 궁금할 수 있는데 이는 자바스크립트 문법을 설계하면서 정해진 규칙이라고 한다 (이런거 보면 참 근본 없다.)
이러한 특징들을 정리하자면 다음과 같다.
__proto__
키워드는 생략이 가능하다.자바스크립트 내 배열 자료구조를 통해서 생성자함수 Array
와 이것의 인스턴스를 이해해보고자 한다.
const arr = [1,2];
console.dir(arr);
console.dir(Array);
위의 출력 결과는 다음과 같다.
생성자함수 Array
의 인스턴스인 arr
의 __proto__
는 생성자함수 Array
의 prototype
과 동일한 것을 참조하는 걸 알 수 있다. 이를 추상화 해보면 다음과 같다.
인스턴스는 __proto__
라는 키워드를 생략하더라도 prototype
에 정의된 메소드 및 프로퍼티에 자유롭게 접근이 가능하다. 한편 프로토타입에 정의된 메소드와 동일한 이름의 메소드를 인스턴스에 정의하게 될 경우에는 인스턴스의 함수로 덮어씌워진다.
let Person = function(name){
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
const seungchan = new Person('승찬');
seungchan.getName = function () {
return 'i am ' + this.name;
}
console.log(seungchan.getName()); // i am 승찬
한편, 인스턴스의 메소드가 아닌 prototype
에서 정의한 메소드에 직접 접근하기 위해서는 아래와 같이 __proto__
키워드를 사용해야 한다.
console.log(seungchan.__proto__.getName()); // undefined
하지만 위와 같이 this
가 가리키는 대상이 인스턴스의 __proto__
가 참조하는 prototype
이 되기 때문에 적절한 name
을 가져오지 못한다. 아래와 같이 이는 call
를 사용하여 this
를 인스턴스로 지정해주면 해결 될 수 있다.
console.log(seungchan.__proto__.getName()).call(seungchan); // "seungchan"
배열 인스턴스의 프로토타입인 Array
생성자함수의 프로토타입을 자세히 살펴보면 아래와 같다.
console.dir([1,2]);
[1,2]
의 __proto__
는 Array
를 가리키고 다시 Array
의 __proto__
는 Object
를 가리키는 것을 확인할 수 있다. 이를 추상화하면 아래와 같다.
이러한 관계 덕분에 배열의 인스턴스인 [1,2]
는 Array의 메소드 뿐만 아니라 Object에서 정의된 메소드가 까지 사용이 가능해진다.
var arr = [1,2];
arr(.__proto__).push(3); // Array에 정의된 push 메소드
arr(.__proto__)(.__proto__).hasOwnProperty(2); // Object에 정의된 메소드
한편 모든 타입은 prototype
을 가지고 prototype
은 Object
형태를 가지게 되기 때문에 prototype
체인의 최상단에는 Object
가 존재하게 된다.
이렇다보니 모든 데이터타입에서 Object
프로토타입에 정의된 메소드들을 접근할 수 있게 된다. 가령 hasOwnProperty
가 대표적인 예이다. 한편 모든 Object
의 메소드 중 일부는 모든 데이터타입에서 사용될 수 있게 설계 되어 있지는 않다. 이 때문에 그러한 메소드들은 정적인 메소드로 정의되게 되었다.
Object.freeze(...);
Object.entries(...);
Obejcts.keys(...)
이 때문에 위의 메서드들은 Object
키워드와 함께 사용되어야 하는 것이다.