우리가 보통 Prototype 이라고 부르는 개념에는 사실 아래 두 가지 요소가 포함 된 개념이다.
Prototype Object
Prototype Link
그렇다면 위 두 가지 개념들은 각각 어떤 역할을 하는지에 대해 알아보겠다.
이전에서 객체를 생성할 때는 함수를 통해 생성하고 후에 특정 변수에 new 키워드를 사용해 생성자 역할을 부여한다고 언급했었다.
function MyInfo () {} // 함수 생성
let myinfo = new MyInfo(); // myinfo 변수에 MyInfo 함수를 부여해 객체 생성
그렇다면 우리는 지금까지 이러한 방식으로 객체를 선언했는가? 답은 아니다.
우리는 흔히 다음과 같은 방식으로 객체를 생성했었다.
let myInfo = {};
이렇게 간편하게 객체를 생성했는데 왜 위에서는 함수를 먼저 생성하고, 그 함수에 new 키워드를 사용해야만 객체를 생성할 수 있다고 했을까?
사실, 우리가 편하게 사용했던 위와 같은 방식은 사실 다음 과정을 통해 생성된 것과 같다.
let myInfo = new Object();
즉, myInfo 는 사실 Object 라는 함수를 통해 생성된 객체인 것이다.
(Object 는 자바스크립트에서 기본적으로 제공되는 함수로 Function, Array 도 마찬가지다.)
생성자 자격이 부여된 함수만 new 키워드를 사용할 수 있다.
(obj1 은 함수를 생성한 것이 아니므로 new 키워드를 사용할 수 없는 모습이지만 obj2 는 함수를 생성했으므로 new 키워드 즉, 생성자 자격이 주어진다.)
함수를 정의하면 함수 생성과 동시에 Prototype Object 도 함께 생성된다. 그렇기 때문에 우리는 Object 의 메소드들을 어떤 함수에서든지 사용할 수 있었던 것이다.
우리가 함수를 정의하면 Object 의 다양한 메소드를 사용할 수 있었던 이유 또한 여기서 설명이 된다.
(obj 함수를 생성함과 동시에 obj 의 prototype 에는 consturctor 와 proto 가 연결되어 있는 모습)
여기서 consturctor 는 생성되었던 함수 obj 를 가리키고 있다.
여기서 proto 는 Prototype Link 로 Object 를 가리키고 있다.
그렇다면 Prototype Link 는 뭘 의미하는지에 대해 알아보자.
function Person () {}
Person.prototype.eyes = 2;
Person.prototype.nose = 1;
let steve = new Person();
let mason = new Person();
console.log(steve.eyes); // => 2
우리는 앞서 Person 이라는 함수를 생성하고 Person 의 prototype 에 eyes 라는 속성을 추가해줬다.
따라서 steve 라는 변수에 new 키워드를 이용함으로써 생성자 자격을 부여하고, Person 의 prototype에 접근할 수 있었다.
그렇다면 다음 코드를 한 번 봐보자.
function Person () {}
Object.prototype.eyes = 2;
Object.prototype.nose = 1;
let steve = new Person();
let mason = new Person();
console.log(steve.eyes); // => 2
eyes 속성을 Person 함수가 아닌, Object 함수에 넣어줬는데도 정상적으로 eyes 값을 참조할 수 있는 모습이다. 이러한 방법이 어떻게 가능한 것일까?
여기서 __proto__
의 개념이 나오게 되는데, 기본적으로 __proto__
속성은 모든 객체가 빠짐없이 가지고 있는 속성으로 자신을 만들어낸 원형인 프로토타입 객체를 참조하는 숨겨진 링크로써 이는 결국 프로토타입을 의미하는 것이다.
(steve 의 __proto__
를 열어보니 생성자는 Person 함수를 가리키고 있고 그 안의 __proto__
를 열어보니 Object 를 가리키는 모습)
즉, 우리가 console.log(steve.eyes)
를 할 때 컴퓨터는 "어라? steve 는 eyes 라는 속성을 가지고 있지 않은데.. 그렇다면 연결되어있는 조상한테서 찾아봐야겠다!" 라는 생각을 가지고 상위 프로토타입(Person)을 탐색한다. Person 에서도 못 찾을 시 Person 에 연결된 또 다른 __proto__
속성에 접근해 탐색한다. 이러한 방식을 반복하면서 최상위인 Object 의 Prototype Object 까지 탐색한다. 최상위까지 도달했는데도 못 찾을 경우 흔히 우리가 잘 아는 undefined
를 리턴한다.
이렇게 __proto__
속성을 통해 상위 프로토타입과 연결되어 있는 형태를 프로토타입 체인(Prototype Chain) 이라고 부르고 우리는 이러한 형태 덕분에 그동안 상위의 상위 스코프에도 접근할 수 있었던 것이다.
Object.create() 메서드는 지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만듭니다.
Object.create 메소드는 ES5에서 도입된 것으로 새로운 객체를 만들 수 있는 메소드다. 현재 ES6 에서는 class 라는 보다 편한 문법이 도입되어 현재는 잘 사용되지 않는 메소드이지만, 이전에는 어떤 방식으로 프로토타입에 접근했는지 알기 위해 간단하게 짚고 넘어가자. 생성된 객체의 프로토타입은 이 메소드의 첫 번째 인수로 지정된다.
let Person = {
eyes: 2
nose: 1
}
let yongho = Object.create(Person); // yongho는 Person 의 prototype 을 상속받는다.
console.log(yongho.eyes); // => 2
단, 주의 할 점은 Object.create() 로 들어온 인자는 해당 프로토타입의 복사본으로 작용된다. 한마디로 원형 값에는 영향을 주지 않는다.
(원형값인 test 에는 영향을 주지 않는 모습)
이전에는 prototype 을 이용해 상위 함수의 프로토타입에 접근했다면, ES6 에서는 Class 및 super 라는 개념을 통해 다음과 같이 좀 더 간편하게 구현이 가능하다.
ES5의 프로토타입 방식으로 간단하게 만들어 보겠다.
function Car (brand,name,color) {
this.brand = brand;
this.name = name;
this.color = color;
}
Car.prototype.drive = function() {
console.log(this.name + '가 운전을 시작합니다.'
}
let sonata = new Car('Hyundai', 'Sonata', 'Gray')
sonata.brand; // 'Hyundai'
sonata.drive(); // 'Sonata가 운전을 시작합니다.'
이번에는 ES6에서 추가 된 class 문법을 사용해 만들어 보겠다.
class Car {
constructor(brand,name,color) {
this.brand = brand;
this.name = name;
this.color = color;
}
drive() {
console.log(this.name + '가 운전을 시작합니다.')
}
}
let sonata = new Car('Hyundai', 'Sonata', 'Gray')
sonata.brand; // 'Hyundai'
sonata.drive(); // 'Sonata가 운전을 시작합니다.'
class 스코프 안의 메소드는 prototype 을 통해 연결을 해주지 않아도 참조할 수 있는 모습이다.
Car 라는 class 를 만들고 그 자식으로 다른 class 를 만들때 그 자식이 Car 의 속성에 접근할 수 있는 방법은 없을까?
다음의 키워드를 통해 이를 가능하게 할 수 있다. class 자식 extends 부모
그 다음, super
라는 키워드를 통해 부모의 생성자에 접근이 가능하다.
아래는 이에 대한 설명을 코드로 나타낸 것이다.
class Car { // 부모 Class
constructor(brand,name,color) {
this.brand = 'Hyundai'
this.name = 'Sonata'
this.color = 'Gray'
}
drive() {
console.log(this.name + '가 운전을 시작합니다.')
}
}
class newMove extends Car { // newMove 라는 자식에게 부모인 Car 속성을 부여하기 위한 메소드
constructor (name, color) { // 부모에게서 가져올 매개변수
super(name, color) // super 키워드를 통해 일부분만 가져올 수도 있다!
}
drive() {
console.log(this.name + ' 는 움직이고 있습니다.')
}
beat() {
console.log(this.name + ' 가 충돌을 감지했습니다.')
}
whatColor() {
console.log(`${this.name} 의 색상은 ${this.color} 색상 입니다.`)
}
}
let Sonata = new newMove;
Sonata.drive() /// => Sonata 는 움직이고 있습니다.
Sonata.beat() // => Sonata 가 충돌을 감지했습니다.
Sonata.whatColor() // => Sonata 의 색상은 Gray 색상 입니다.
자식의 생성자와 부모의 생성자가 동일하다면, 자식의 생성자는 아래와 같이 생략이 가능하다.
class newMove extends Car {
constructor () { // 생략
super() // 이 경우 constructor 에도 매개변수를 입력하면 안된다.
}
}
// 동일한 결과를 리턴
🚫 다음과 같이 super 키워드로만 매개변수를 받아올 경우 다음과 같은 문제점이 발생한다.
class newMove extends Car {
constructor () {
super(brand, name)
}
}
// super 는 기본적으로 constructor 에서 준 매개변수를 기반으로 작동한다.
// 따라서 constructor 에 아무값도 주지 않고 super 로만 특정 매개변수를 받아올 경우
// "brand, name 은 정의된적이 없다!!!" 라는 에러를 나타낸다.
✅ 올바른 방법
class newMove extends Car {
constructor (brand, name) { // 따라서 특정 값만 받아오고 싶은 경우 특정 값만 constructor 의 매개변수로 입력한 뒤
super(brand, name) // super 에 해당 매개변수를 입력하면 된다.
}
}