프로토타입을 통한 객체지향 그리고 상속
자바나 C++같은 객체지향 언어와 달리 JS는 객체지향 개념을 지원하기 위해 프로토타입을 사용한다.
이 프로토 타입으로 구현할 수 있는 대표적인 객체지향 개념이 바로 상속이다!
프로토타입은 말 그대로 ‘원형’을 뜻하고 이게 뭔지는 스크롤 좀 더 내리면 알수있다.
자바스크립트와 자바에서의 객체 생성
자바스크립트에선 자바의 문법 중 몇 가지를 따라하고 있는데 그 중 하나가 바로 new 키워드이다. 자바에선 객체를 class로, 자바스크립트에선 function으로 정의하는데
function Person(name, blog) {
this.name = name;
this.blog = blog;
}
let doori = new Person("doori","gollumnima@github.io");
console.log(doori) // Person { name: 'doori', blog: 'gollumnima@github.io' }
위의 예시만 봐도 js에서 function키워드는 class 대신 쓰인것처럼 보인다.
그렇지만 자바의 소스코드와 비교해보면 거의 비슷!!
객체 지향 관점에서 보면 자바스크립트에서 function은 자바의 class 생성자를 합쳐놓은 개념이라고 볼 수 있다.
그치만 자바스크립트에도 class 키워드가 생겼다!! 다른 언어와 이질감을 줄이기 위해 만들었다는데
class Person {
constructor(name, blog) {
this.name = name;
this.blog = blog;
}
}
let doori = new Person("doori","gollumnima@github.io");
console.log(doori)
이렇게 클래스로 정의하면 내부적으로 조금 특수한 function으로 정의된다.
console.log(Person())
다음과 같이 콘솔을 찍어보자.
new 키워드없이 클래스함수를 바로 호출했을때 에러가 난다. 이걸 이해하려면 먼저 함수호출과 this에 대해 이해해야 하는데!
this의 이해
this를 이해하기 위해선 먼저 함수 호출하는 방법에 대해서 알아야 한다.
- 일반 함수로의 호출
- 멤버함수로의 호출
- call()함수를 이용한 호출
- apply()함수를 이용한 호출
각 호출 방법에 따라 결정되는 this를 살펴보자.
function what() {
return this.toString();
}
let is = {
it: what,
toString: function() {
return "[object is]";
}
};
what();
is.it();
what.call();
what.apply(is);
is.it.call(undefined);
is.it.call(is);
위의 코드를 실행시켜보면 다음과 같은 결과가 나온다.
정리해보자면,
- 일반함수에서의 this = window
- 멤버함수에서의 this = 해당 함수가 속한 객체
- 인자X call/apply에서의 this = window
- 인자1개 call/apply에서의 this = 첫번째 인자
- 인자x 멤버함수 call/apply에서의 this = window
- 인자1개 멤버함수 call/apply에서의 this = 첫번째 인자
이처럼 this는 함수나 스코프 기반으로 결정되는 것이 아니라 호출 방법에 따라 변경된다. 함수가 호출되는 방법 외에 this가 사용되는 경우가 또 있는데 그게 바로 생성자로 new 키워드를 사용할때 이다.
new 키워드
보통 자바와 같은 객체지향 언어에서 많이 쓰이는 new 키워드는 메모리를 새롭게 할당하고, 해당하는 클래스의 생성자를 호출해 인스턴스를 초기화한다. JS에서는 이와는 조금 다르게 new 키워드 뒤에 오는 객체의 생성자를 통해 생성과 초기화가 한번에 일어난다.
자세한 과정은 ECMAScript의 [[Construct]] 부분을 참고하면 좋다. 물론 속깊자 122~123p에 아주 잘 나와있다!
프로토타입에 대한 표준 정의
new 키워드의 객체 생성/초기화 단계에 prototype이라는 속성이 나오는데, 이는 다른 객체들과 공유되는 속성을 제공하는 객체를 말한다. 생성자를 통해 객체를 생성할 때, 생성자의 prototype 속상을 내부적으로 참조하는데 여기서 기억해야 할 부분은 생성자의 속상인 prototype또한 하나의 객체라는 것!!
예시를 함께 살펴보자!
프로토타입의 사용 예
이미 위에서 썼던 Person 함수를 다시 가져와봤다.
function Person(name, blog) {
this.name = name;
this.blog = blog;
}
Person.prototype.getName = function () {
return this.name;
}
Person.prototype.getBlog = function () {
return this.blog;
}
let doori = new Person("Doori","gollumnima@github.io")
let strawberry = new Person("Berry","berry@very.io")
console.log(doori.getName())
console.log(doori.getBlog())
console.log(strawberry.getName())
console.log(strawberry.getBlog())
코드의 중간에 constructor.prototype
을 통해 Person 생성자의 prototype 속성을 설정한다.
이후 콘솔을 찍어보면 우리가 원하는 값이 나온다!
이게 바로 생성자를 통해 생성된 객체들이 prototype을 공유하는것..!
Person.prototype.introduce = function() {
console.log(`Hi my name is ${this.name}, plz visit my blog -> ${this.blog}.`)
}
doori.introduce() // 'Hi my name is Doori, plz visit my blog -> gollumnima@github.io.'
Person.prototype.introduce = function() {
console.log(`Hello ${this.name}.`)
}
doori.introduce() // 'Hello Doori.'
Person.prototype.gender = "female"
console.log(doori.gender) // 'female'
위의 코드에서 확인할 수 있다시피
- 프로토타입에 새로운 속성 추가 가능
- 기존에 선언한 속성 수정 가능
- 함수가 아닌 변수도 추가 가능
- 추가된 모든 속성들을 모든 객체가 공유
프로토타입과 생성자
위에서 만든 Person 생성자와 그의 친구들을 다시 한번 살펴보자!
특이한 점은 getName과 getBlog 함수는 new Person 객체에 속하지 않고 proto라는 프로토타입으로 남아있다는 점!
마찬가지로 쫌 더 위의 예시코드 중 gender를 설정하는 게 있었는데 strawberry의 gender를 proto에 있는 값과 다르게 설정해도 원본의 값이 바뀌지 않는 이유가 이와 비슷하다. proto에 있는 gender값이 바뀌는 것이 아니라 strawberry 객체에 gender속성이 따로 추가되어 저장되기 때문!
이러한 구조 때문에 객체가 가지는 변수에 접근하려면 일단 객체 자체의 속성부터 찾고, 그 속성이 있으면 참조하고 없으면 자신의 프로토타입에 저장된 속성을 검사한다. 근데 거기에도 없다? 그러면 undefined를 반환한다. 자바스크립트에서 모든 객체는 프로토타입이라는 다른 객체를 가리키는 내부 링크를 가지고 있다. 한 객체의 프로토타입 또한 프로토타입을 가지고 있고, 이것이 반복되다가 null을 프로토타입으로 가지는 객체에서 끝난다. 이러한 객체들의 연쇄를 프로토타입 체인이라고 부른다!
hasOwnProperty 함수
알고리즘 문제를 풀 때 자주 쓰던 속성인데, 이게 프로토타입과 연관되어 있다는 생각은 한 번도 해본적이 없었는데!! 역시 속깊자 최고 객체의 속성에 접근할 때 객체와 프로토타입을 재귀로 검사하는 단계를 거쳐서 속성을 참조한다. 여튼 hasOwnProperty함수를 사용하면 접근하려는 속성이 현재 객체에 포함되어 있는지 아닌지 구분할 수 있다. 가장 유용하게 쓸 수 있는 상황은 속성 전체를 탐색하는 for in 구문을 사용할때 이다.
자바스크립트에서의 상속 활용
옛날처럼 웹페이지들이 단순한 정보 전달용으로만 썼을 때는 굳이 상속이 필요없었다. 하지만 웹개발 방법론이 발달하면서 다양한 데이터 처리 동작들을 구현하기 시작했고, 객체지향의 개념과 함께 상속이 활용되는 경우가 늘어났다!
기존의 상속 구현 방법
초창기에는 아래의 코드와 같이 this 대신 새로운 obj를 반환하는 방식으로 상속을 구현했다.
function Person() {
this.name = "anonymous";
this.job = "none";
this.sayHello = function () {
console.log(`Hello, my name is ${this.name}`);
};
}
function Doori() {
let obj = new Person();
obj.name = "Doori";
obj.job = "FE_DEV";
return obj;
}
let doori = new Doori();
doori.sayHello();
그치만 이렇게 하면 아주 치명적인 단점이 있는데..
console.log(doori instanceof Doori); // false
console.log(doori instanceof Person); // true
doori라는 변수가 Doori 말고 Person의 인스턴스로만 인식된다는 거..ㅠㅠ
new Doori 키워드를 사용해 객체를 생성했는데, Doori의 인스턴스로 인식못하는 건 아주 치명적인 단점이 될 수 있다.
그래서 이후 자바스크립트는 function에 기본으로 들어있는 프로토타입 속성을 새롱누 객체로 설정해 상속하는 방법을 채택했다.
아래는 객체로 프로토타입을 수정한 자바스크립트의 상속 구현 예시이다.
let person = {
name : "anonymous",
job : "none",
sayHello : function () {
console.log(`Hello, my name is ${this.name}`);
}
}
function Doori() {
this.name = "Doori";
}
Doori.prototype = person;
let doori = new Doori();
doori.sayHello();
person.sayHello();
console.log(doori instanceof Doori); // true
console.log(doori instanceof person); // Error!
이제 doori는 Doori의 인스턴스인것으로 나오는데 문제는 person 변수를 상속했다는 걸 확인 못한다. 그래서 new로 새로운 객체를 만드는 방식으로 이 문제를 해결해보려고 한다.
function Person() {
this.name = "anonymous";
this.sayHello = function () {
console.log(`Hello, my name is ${this.name}`);
}
}
function Doori() {
this.name = "Doori";
}
Doori.prototype = new Person();
let doori = new Doori();
doori.sayHello();
console.log(doori instanceof Doori); // true
console.log(doori instanceof Person); // true
이렇게 바꿨을땐 겉으로 보면 instanceof문제는 해결된 것 같이 보이지만