이제 Dog 의 인스턴스가 run 메서드를 호출하면, 자바스크립트는 먼저 해당 인스턴스에서 run 메서드를 찾아보고, 없다면 Object.create로 생성된Dog의 Prototype 에서 찾아보고, 또 없다면 Dog.prototype의 __proto__ 속성이 가리키는 Animal.prototype
의 객체에서 찾아보고 그래도 없다면 계속 상위 prototype 객체로 거슬러 올라갈 것이다.
다만 우리가 주의할 점이 한 가지 있다.
* Dog.prototype 객체의 constructor 를 Dog 함수로 변경하기
앞서 Object.create(Animal.prototype) 메서드의 결과를 Dog.prototype 객체에 할당해주었는데 여기서 한 가지 문제점이 발생한다.
이전 포스트에서 프로토타입 객체에 정의된 constructor 함수는 기존 함수 자체를 바라보고 있다고 했다.
따라서 Dog.prototype.constructor === Dog 여야 한다.
그러나 지금 같은 경우 Dog.prototype.constructor === Animal 이다.
우리가 Animal 객체를 Prototype 객체로 가지는 새로운 객체를 만들 때 Object.create() 함수 호출 시 Dog 함수에 대한
아무런 정보를 주지 않았기 때문에, 자바스크립트는 새로 만들어진 객체의 constructor에 무슨 값을 넣어줘야 하는지 모른다.
따라서 Object.create()는 인자로 들어온 Animal.prototype의 constructor의 값을 그대로 복사해 새 객체의 constructor 값으로 설정해주며, 이 때문에 새 객체의 constructor가 Animal.prototype 객체의 constructor가 가리키는 Animal 함수를 가리키게 된 것이다.
자바스크립트에서 객체의 작동 원리를 알아보자! 또한 작동 원리를 알기 위해 우리는 잠시 ES6의 Class 문법 이전으로 돌아갈 것이다.
1. 객체의 생성
let obj = {}
let obj = new Object()
//빈 객체를 선언과 동시에 할당하는 방법
//두 방법은 같은 결과를 초래한다.
ES6 이전의 자바스크립트 세계에선 Class라는 개념이 없었다. 그렇다면 객체를 생성하기 위한 new Object()에서 Object는 무엇일까?
여기서 Object는 바로 함수이다. 즉 ES6 이전에는 OOP를 함수로 구현했음을 짐작할 수 있다.
함수는 클래스와 비슷하다. 함수 내부에서 여러 변수들과 함수를 정의할 수 있으며 여러 번 호출할 수 있기 때문이다.
다음의 예시를 살펴보자.
function Animal(name, speed) {
this.name = name;
this.speed = speed;
this.run = function () {
console.log(`${this.name}가 ${this.speed}km/h의 속력으로 달립니다!`);
}
}
const myDog = new Animal('doggy', '10');
위와 같은 코드를 통해 Animal 타입의 myDog 라는 객체를 생성할 수 있다.
그런데 위와 같은 코드는 하나의 문제를 가지고 있다.
클래스 기반 객체 지향 프로그래밍에서 클래스에 메서드를 정의하면, 해당 클래스 타입의 모든 객체들은 하나의 메서드를 공유하게 된다.
하지만, 위와 같은 자바스크립트 코드는 다르다.
이 코드는 Car 타입 객체가 생성될 때 마다 Drive라는 변수를 생성하고, 이 변수에 익명 함수를 저장하는 형태로 동작한다.
즉, 객체가 생성될 때 마다 새로운 익명 함수가 생성되어 Drive에 저장되고 메모리 낭비가 심할 것이다.
무엇보다 진정한 의미에서의 객체 지향을 구현한 것이 아니게 된다.
자바스크립트에서 동일한 타입의 모든 객체가 하나의 메서드를 공유하도록 코드를 구현하려면, 위의 Animal 함수 정의에서 run 메서드 부분을 아래와 같이 고쳐야 한다.
function Animal(name, speed) {
this.name = name;
this.speed = speed;
}
Animal.prototype.run = function () { <= 변경된 부분
...
}
const myDog = new Car("doggy", 10);
앗 갑자기 생소한 것이 등장했다. prototype? 이 의미하는 것은 무엇일까
2. prototype
prototype의 사전적 의미로는 원형이라는 뜻을 가지고 있다.
위의 Animal 함수의 run 메서드를 모든 Animal 타입 객체들이 공유하게 만들기 위해 Animal.prototype이라는 곳에 run 메서드를 정의했다.
Animal의prototype이라는 변수는모든 Animal 타입 객체들의 기준이 되는 단 하나의 '원형' 객체를 가리킨다.
우리가 아래 코드처럼 Animal 함수를 정의하면, Animal 함수에는 run이라는 메서드가 존재하지 않는다.
대신 Animal.prototype을 확인해보면 run 메서드가 존재함을 확인할 수 있다.
자바스크립트는myDog.run()을 통해 메서드를 호출할 때, 해당 객체에 Drive 메서드가 존재하지 않는다면 그 객체의prototype으로 이동하여 run 메서드가 존재하는지 확인하고, 존재한다면원형 객체의 run 메서드를 호출한다. 이런 방식으로 동작하기 때문에 Animal 타입의 모든 객체들이 동일한 원형 객체의 run 메서드를 호출하게 되는 것이다.
그런데 원형 객체라는 것은 언제 만들어진 것이고, Animal.prototype이라는 건 어떻게 원형 객체를 가리키게 된 것일까?
우리는 Animal에 prototype이라는 이름의 변수를 선언한 적도 없으며 Animal의 원형 객체라는 것도 만든 적이 없는데 말이다.
자바스크립트에서는 우리가 어떤 함수를 정의하든 간에 해당 함수에 대한Prototype 객체를 자동으로 생성해주며, 그 함수에 대해 '함수. prototype'이라는 변수도 생성한 후 이 변수가 Prototype 객체를 가리키도록 연결해준다. 지금 당장 콘솔에 가서 확인해보라.
그냥 일반적인 함수 호출을 사용하기 위해 함수를 정의했다고 하더라도 자바스크립트는 무조건 해당 함수에 대한 Prototype 객체를 생성한다.
3. Constructor(생성자)
constructor 함수는 클래스 기반 객체 지향 프로그래밍에서도 등장하는 익숙한 개념인 바로 그 생성자로,
우리가 객체 생성을 요청할 때 호출되어 실제 객체를 만드는 역할을 하는 함수이다.
즉 new Animal()라는 코드를 호출해 Animal.prototype.constructor 함수가 자동으로 호출되어 객체를 생성해준다.
사실 자바스크립트에서는 Prototype 객체의 constructor가 기존 함수 자체를 가리키기 때문에,Animal.prototype.constructor는function Animal(name, speed)를 가리킨다.
단지,new 키워드가 함수 내부에서 사용한 this를 실제로 생성된 instance를 가리키도록 만들어주므로, 자바스크립트의 객체 생성에 있어 진짜 중요한 역할은 생성자가 아니라 new 키워드가 처리해준다고 볼 수 있다.
4. __proto__
constructor메서드는 오직'함수의 Prototype 객체'만이 가진다.
하지만__proto__는 자바스크립트의'모든 객체'에 대해 자동으로 추가되는 속성이다.
그렇다면 이 __proto__에는 어떤 값이 담겨있을까?
위 글에서 Animal.prototype이 가리키는 Prototype 객체에 Drive 함수를 정의하면, Animal 타입의 모든 객체들이 Prototype 객체의 run 함수를 사용하게 된다고 했었다.
하지만 자바스크립트가myDog.run()라는 코드를 보고 Prototype 객체의 run 메서드를 호출하려면, myDog 객체에서 Prototype 객체에 참조할 수 있어야 한다.
이를 위해 자바스크립트는 모든 객체의 인자로Prototype 객체를 가리키는 __proto__라는 변수를 정의해준다.
즉 위 예제에서 myDog.__proto__ === Animal.prototype 인 것이다.
따라서,myDog.run()라는 코드가 실행되면 자바스크립트는 다음과 같은 과정을 거쳐 Prototype 객체의 run 메서드를 호출한다.
myDog 객체에 run 메서드가 있는지 확인한다.
myDog 객체에 run 메서드가 없으면, Prototype 객체에 해당 메서드가 존재하는지 확인하기 위해 __proto__ 변수가 가리키는 객체로 이동한다.
Prototype 객체에 run 메서드가 존재한다면, Prototype 객체의 run 메서드를 호출한다.
5. Prototype chain
만약 Animal.prototype에 run 메서드를 정의하지 않아서, Prototype 객체에도 메서드가 존재하지 않는다고 가정해보자.
그렇다면, 자바스크립트는 Animal 함수의 Prototype 객체에서 탐색을 종료할까?
정답은 아니다. 자바스크립트에서 모든 객체는최상위 객체로 Object라는 객체를 가진다.
Prototype 객체도 결국객체이므로 최상위 객체로 Object 객체를 가진다.
따라서 Prototype 객체인 Animal.prototype의 __proto__ 속성은 아래와 같이최상위 객체인 Object의 Prototype 객체를 가리키게 된다.
이처럼 Prototype 객체들이 __proto__ 속성을 통해 연결되는 구조를'Prototype Chain'이라는 용어로 부른다.
자바스크립트는 어떤 함수의 Prototype 객체에서 원하는 메서드를 찾지 못한다면 해당Prototype 객체의 __proto__ 속성이 가리키는 부모 Prototype 객체로 이동한다. 즉 찾을 때까지 계속 거슬로 올라가는 것이다.
이 작업은Object 함수의 Prototype 객체에 도달할 때까지 반복되며, Object.prototype 객체는 최상위 객체이므로__proto__ 속성의 값이 null이기 때문에 더 이상 이동할 수 있는 부모 Prototype 객체가 존재하지 않아 작업을 중단한다.
후.. 이해하는데 너무 어려운 주제였다. 블로깅 내용의 80%는 다음의 블로그를 참조했다!
오늘 배운 것 : 화살표 함수, 구조 분해 할당, 실행 콘텍스트에 따른 this의 5가지 값, call/apply/bind 메서드
1. 화살표 함수
프리 코스 때부터 의도적으로 쓸려고 노력해서 크게 어렵지 않았다. 무난하게 과제를 수행할 수 있었음.
코드가 길어지면 {}가 필요한데 이때 명시적으로 return을 해줘야 한다는 점만 주의하면 됨!
2. 구조 분해 할당
rest/spread syntax와 함께 중첩된 객체를 구조 분해하는 경우를 제외하곤 괜찮았다.
또한 하나의 객체가 다른 객체에서 rest parameter로 들어가 있는 경우 만약 같은 key를 가진 property가 있다면
뒤에 나온 property가 앞에 있는 property를 덮어 씌우게 된다!
3. this
조금 헷갈리긴 했지만 그래도 괜찮았다. 익숙해지면 괜찮을 것 같다. 그래도 개념은 다시 한번 정리하고 가야 할 것 같다.
this는 함수 실행 시 호출(invocation) 방법에 의해 결정되는 특별한 객체다.
함수 실행시 결정되므로, 실행되는 맥락(execution context)에 따라 this는 다르게 결정된다.
3 - 1) 함수 실행에 따른 this의 5가지 바인딩 패턴
Method 호출 : 부모 객체 (실행 시점에 온점 왼쪽에 있는 객체), 중첩된 객체가 있더라도 바로 본인의 부모를 가리킴.
new 키워드를 이용한 생성자 호출 : 이 때는 new 키워드로 만들어진 instance가 this가 가리키는 값이 된다.
. call 또는. apply 호출 : 첫 번째 인자로 전달된 객체, 즉 사용자가 명시적으로 this 값을 주어야 함!
Global : 브라우저의 경우 -> window (strict mode에서는 undefined) 객체, node.js -> module.exports
function 호출 : 브라우저 -> window (strict mode에서는 undefined) 객체, node -> global
global, function 호출의 경우 애초에 this를 사용하지 않을 것을 권고.
4. .call/.apply/.bind 메서드
.call, .apply, .bind는 this값을 명시적으로 지정하고 싶을 때 사용한다.
.call과 .apply는 매우 유사하다. 지정된 this 콘텍스트와 추가적인 인수를 사용하여 함수를 호출한다.
.call과 .apply의 유일한 차이점은 .call은 인수를 하나씩 전달해야하며 .apply는 인수를 배열로 취한다는 것이다.
예시를 통해 알아보자.
4 - 1) .call, .apply
const book = {
title: 'Brave New World',
author: 'Aldous Huxley',
}
function summary() {
console.log(`${this.title} was written by ${this.author}.`)
}
summary()
summary 함수와 객체 book은 어떠한 connection이 없다.
때문에 summary를 호출하면 전역 객체에서 해당 속성을 찾을 때 undefined가 나타날 것이다.
그러나 call 및 apply를 사용하여 함수에서 객체 book의 this 콘텍스트를 호출할 수 있다.
summary.call(book)
// or:
summary.apply(book)
//"Brave New World was written by Aldous Huxley."
이러한 방법을 적용하면 이제 객체와 함수가 연결된다.
그리고 this 값은 우리가 명시적으로 전달해준 객체 book이 된다.
두 개의 메서드의 차이점은 위에서 언급했듯이 call은 추가적인 인수를 하나씩 받는 반면 apply는 배열에 모두 담아 전달해야 한다.
4 - 2) .bind
call과 apply는 모두 일회성 사용 방법이다.
this 콘텍스트를 사용하여 메서드를 호출하면 그대로 유지되지만 원래 함수는 변경되지 않는다.
때로는 다른 객체의 this 콘텍스트와 함께 메서드를 반복해서 사용해야 할 수 있다.
이 경우 bind 메서드를 사용하여 명시적으로 바인딩된 새로운 함수를 만들 수 있다.
마찬가지로 예시를 통해 알아보자.
const braveNewWorldSummary = summary.bind(book)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.
const book2 = {
title: '1984',
author: 'George Orwell',
}
braveNewWorldSummary.bind(book2)
braveNewWorldSummary() // Brave New World was written by Aldous Huxley.
이 예에서는 braveNewWorldSummary를 호출할 때마다 항상 바인딩된 원래 this 값을 반환한다.
새로운 this 콘텍스트를 바인딩하려는 시도는 실패하므로 항상 바인딩된 함수를 신뢰하여 예상한 this 값을 반환할 수 있다.