2주차가 시작되었습니다.🤣

Sprint03.Inheritance Pattern을 진행하기 전에 중요한 요소들을 공부합니다.

🏄🏻‍♂️ Solo Part

1. Object.prototype

JavaScript는 프로토타입 기반의 객체 지향형 프로그래밍 언어입니다.

이제 OOP(객체 지향 프로그래밍)에 대해선 조금 알겠는데요, prototype 기반은 무엇을 의미할까요?

prototype을 해석해보면 원형 이라는 의미가 있습니다. 아 그렇구나, 일단 넘어갑시다.

ES6 문법이 도입되기 전에는 JavaScript에는 클래스(Class)가 없었습니다.

그래서 객체 생성을 위해서 prototype을 사용하였는데요.

코드 부터 한번 봅시다!

// EX.01
let singer = {
    name: '다현',
    age: 22,
}

// 크롬 콘솔에서 테스트하면 다음과 같습니다.
singer {name: "다현", age: 22}
  > age: 22
  > name: "다현"
  > __proto__: Object

// people객체의 __proto__는 Object객체의 프로토타입과 같습니다.
console.log(singer.__proto__ === Object.prototype) // true

객체를 생성하면, 프로토타입이 결정되고, 우리는 그 프로토타입을 변경할 수 있습니다!

그렇다면 이를 저희 입맛에 맞게 변경하여, 상속을 구현하는 것이죠!

이번엔 일반 객체가 아닌 함수 객체를 알아볼까요? 여기서부터 조금 헷갈립니다!

// EX.02
function People(name, age) {
    this.name = name;
    this.age = age;
}

// 크롬 콘솔에서 테스트하면 다음과 같습니다. (부분 생략)
console.dir(People);
People(name, age)
  > name: "People"
  > prototype: {constructor: ƒ}
  > __proto__: ƒ ()

// People 객체의 인스턴스를 생성합니다.
const dahyun = new People('다현', 22);

// 크롬 콘솔에서 테스트하면 다음과 같습니다.
console.dir(dahyun);
People
  > age: 22
  > name: "다현"
  > __proto__: Object

함수 객체 People() 앞에 new키워드를 사용하면 생성자가 되고, 이것으로 생성한 객체를 인스턴스라고 합니다.

함수 객체의 출력과 인스턴스 객체의 출력을 보면, 인스턴스 객체에 prototype이 없습니다!

그렇다면 __proto__는 무엇이고, prototype과 무엇이 다른걸까요?

먼저 [[prototype]]을 이해하셔야 합니다! 👈🏻

[[prototype]]

  • JavaScript의 모든 객체에는 [[prototype]]이라는 내부 슬롯이 존재합니다.
  • 함수 객체를 포함한 모든 객체가 가지고 있는 프로퍼티입니다.
  • [[prototype]]의 값은 프로토타입 객체로서, __proto__로 접근할 수 있습니다.

__proto__

  • 객체 입장에서 자신의 부모 역할인 프로토타입 객체를 가리킵니다.
  • __proto__에 접근하면 내부적으로 Object.getPropertyOf()가 호출되어 프로토타입 객체를 반환합니다.
  • People이 함수 객체이면 함수의 프로토타입을 가리킵니다.

prototype

  • 함수 객체만이 가지는 프로퍼티입니다.
  • 함수 객체가 생성자(클래스)로 사용될 때 인스턴스의 부모 역할을 하는 객체를 가리킵니다.

그럼, 위에서 생성한 객체들을 비교해봅시다!

// 대략적인 변수 설명
let singer = { ... }
let People = function { ... }
let dahyun = new Person( ... )

// singer : 일반 객체
singer.__proto__ === Object.prototype   // true
singer.__proto__ === Function.prototype // false

// People : 함수 객체
People.__proto__ === Object.prototype   // false
People.__proto__ === Function.prototype // true

// dahyun : 인스턴스 객체
dahyun.__proto__ === Object.prototype   // false
dahyun.__proto__ === Function.prototype // false
dahyun.__proto__ === People.prototype   // true

2. constructor

다른 언어를 공부하셨다면 class를 배우실 때 constructor라는 개념을 배우셨을 텐데요. 이와 비슷합니다.

프로토타입 객체는 constructor 프로퍼티를 가지며, 이는 객체의 입장에서 자신을 생성한 객체를 가리킵니다.

역시 말보단 코드로 이해해 봅시다. 👍🏻

주석에 설명을 적겠습니다 :)

// 함수 객체를 생성합니다.
function People(name, age) {
    this.name = name;
    this.age = age;
}

// 인스턴스 객체를 생성합니다.
const sohee = new People('소희', 29);

// 크롬 콘솔에서 출력합니다.
console.dir(People)
People
  > age: 29
  > name: "소희"
  > __proto__:
    > constructor: ƒ People(name, age)
    > __proto__: Object

// 결과 1 : People 생성자 함수에 의해 생성된 객체를 생성한 객체는 People 생성자 함수입니다.
console.log(People.prototype.constructor === People) // true

// 결과 2 : 인스턴스를 생성한 객체는 People입니다.
console.log(sohee.constructor === People)            // true

// 결과 3 : 함수 객체를 생성한 객체는 Function 객체입니다.
console.log(People.constructor === Function)         // true

3. Prototype Chain 🔗

JavaScript에서 특정 객체나 프로퍼티에 접근할 때, 해당 객체에 대상이 없다면 [[prototype]]을 통해 대상을 검색합니다.

이것이 사슬 처럼 연결 되어 있다고 하여 Prototype Chain 이라고 합니다.

뭔가 말이 어려운 것같지만, 저희는 이미 자주 사용하고 있습니다!

가장 자주 사용하는 배열을 예로 들어볼까요?

// Array
Array.prototype
  > concat: ƒ concat()
  > constructor: ƒ Array()
  > copyWithin: ƒ copyWithin()
  > entries: ƒ entries()
  > every: ƒ every()
  > fill: ƒ fill()
  > filter: ƒ filter()
  > find: ƒ find()
  > findIndex: ƒ findIndex()
  > flat: ƒ flat()
  ...

배열에서 자주 사용하는 메소드인 concat, every, fill 등이 모두 prototype에 들어 있습니다.

그런데 저희는 [1,2,3].prototype.concat 이 아닌 [1,2,3].concat을 사용합니다.

이런 것들도 모두 프로토타입 체인입니다.

프로토타입을 열심히 공부하셨다면, 프로토타입 체인은 예제로 간단하게 이해하실 수 있습니다!

// prototype chain
const A = function() {}
A.prototype.log = "I'm here";

const B = function() {};
B.prototype = new A();

const C = function() {};
C.prototype = new B();

const message = new C();
console.log(message.log) // I'm here

마지막 줄을 먼저 보면, C 생성자로 만든 인스턴스가 A.prototype.log를 사용합니다?!

예상하셨겠지만, C의 prototype에 log가 없어 차례차례 올라가면서 확인하여 가져오는 것입니다.

너무 간단한데, 주의할 점 하나만 짚고 프로토타입 체인을 마치겠습니다.

// prototype chain
const A = function() {}
A.prototype.log = "I'm here";

const B = function() {};
B.prototype = new A();

const C = function() {};
C.prototype = new B();
C.prototype.log = 1;

const message = new C();
console.log(message.log) // 1

C의 prototype에 다른 값을 추가 하였습니다.

마지막 줄의 결과와 같이, C의 prototype에 log가 존재하므로 1 을 바로 가져옵니다.

4. Object.create()

객체를 생성한다...인가요? 😅

잘 모르겠으니 MDN에서 예제 코드를 가져와봅시다!

// MDN : Object.create() - example
const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  }
};

const me = Object.create(person);

me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten

me.printIntroduction();
// expected output: "My name is Matthew. Am I human? true"

뭔가 복사하는 느낌이 듭니다. 자세히 살펴보겠습니다.

// person
person {isHuman: false, printIntroduction: ƒ}

// me
me {}
  > __proto__:
  > isHuman: false
  > printIntroduction: ƒ ()
  > __proto__: Object

// prototype check
me.__proto__ === person // true

빈 객체를 생성하는 것 같지만, me 객체의 prototype은 person 객체를 가리킵니다.

감을 잡은 채로, 정의를 살펴보겠습니다.

Object.create()

  1. 객체에서 다른 객체로 직접 상속을 구현합니다.
  2. 클래스 패턴과 다르게 new 키워드를 사용하지 않습니다.
  3. 객체 리터럴 패턴에도 사용 가능합니다.

예를 들어보겠습니다.

// Object.create() - example1
const animal = {
  log: function() {
    console.log(this.sound)
  }
}
const dog = Object.create(animal);
console.log(dog) // {}
dog.sound = '멍멍'
dog.log(); // 멍멍

const cat = Object.create(animal);
console.log(cat) // {}
cat.sound = '냥냥'
cat.log(); // 냥냥

animal.isPrototypeOf(dog) // true
animal.isPrototypeOf(cat) // true

animal 객체를 생성하여 log메소드를 추가하였습니다.

Object.create(animal)의 리턴값을 dog에 추가하였습니다.

console을 찍어보면 겉으로 보기엔 빈 객체이지만, __proto__에 animal 객체를 가지고 있습니다.

그래서 animal객체의 log를 상속받아 dog.log 로 '멍멍', '냥냥'을 출력할 수 있습니다.

그럼 Object.create()의 내부를 확인해봅시다.

// MDN Object.create() - Polyfill
Object.create = function (proto) {
  // 예외 처리 생략
  function F() {}
  F.prototype = proto;
  return new F();
};

예외 조건을 생략하니 매우 짧아졌습니다! ✌🏻

인자로 proto를 받아서 생성자의 prototype에 proto를 넣고 리턴하고 있는 것을 확인할 수 있습니다!

다음 코드에서 Object.create()의 문제점을 고치고 마무리하겠습니다.

// Object.create() - example2
// 1. Human 클래스를 생성합니다.
let Human = function(name) {
  this.name = name;
}

// 2. Human의 prototype에 sleep메소드를 추가합니다.
Human.prototype.sleep = function() {
  console.log('zzz...')
};

// 3. 인스턴스 생성
let steve = new Human('steve');

// 4. Student 클래스를 생성합니다. 이때, Human.call()로 this를 연결시켜주어야합니다.
let Student = function(name) {
  Human.call(this, name)
}

// 5. Object.create()의 결과를 Student.prototype으로 넣어줍니다.
Student.prototype = Object.create(Human.prototype)

// 6. Student.prototype의 constructor를 Student객체로 만들어줍니다.
Student.prototype.constructor = Student;

// 7. learn 메소드를 추가합니다.
Student.prototype.learn = function() {
  console.log('배우는중...')
};

// 8. john 인스턴스를 생성합니다.
let john = new Student('john');

john.learn() // 배우는중...
john.sleep() // zzz...

만약 6번 과정을 생략하면 Student.prototype에는 constructor가 존재하지않습니다.

그래서 Student.prototype.constructor는 Student를 가리켜야 하므로 직접 넣어줍니다.

그리고 4번은, new 키워드로 인스턴스를 생성할 때 Human의 this는 Human을 가리키지 않습니다.

따라서 직접 Human과 this를 연결시켜 주는 것입니다.

헷갈리신다면 필요하실 때 직접 넣어보시면 금방 이해가 될 것입니다 🤟🏻