All Articles

속깊은 JavaScript - 프로토타입 2탄

image.png

자바스크립트에서의 상속 활용

옛날처럼 웹페이지들이 단순한 정보 전달용으로만 썼을 때는 굳이 상속이 필요없었다. 하지만 웹개발 방법론이 발달하면서 다양한 데이터 처리 동작들을 구현하기 시작했고, 객체지향의 개념과 함께 상속이 활용되는 경우가 늘어났다!

기존의 상속 구현 방법

초창기에는 아래의 코드와 같이 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문제는 해결된 것 같아서 정상적으로 작동하는것처럼 느껴지지만 사실 완전 해결된 건 아니다.

https://images.velog.io/post-images/dooreplay/b1365f90-2ef4-11ea-b1db-754382fb2e0a/image.png

doori를 실제로 생성한건 Doori지만, doori.constructor를 콘솔에 찍어보면 function Person()이 나온다. 이러한 이유로 일부 js개발자들은 new와 프로토타입을 수정해서 상속하는걸 좋아하지 않는다고 한다. 자바스크립트 답지 않다고, 자바에서 억지로 가져온 키워드라고…! 그럼 이렇게 생각하는 사람들이 주장하는 바람직한 상속법이란 무엇일까!

instanceof 동작 원리

자세한 과정에 대해선 속깊자 144p에 잘 나와있다. 간략히만 짚고 넘어가자면! 일반적인 객체지향 언어에서는 생성자를 기준으로 비교해서 상속을 확인한다. 하지만, 자바스크립트에선 생성자의 연결이 깨지는 현상때문에 생성자를 기준으로 두지 않고, 프로토타입을 서로 비교하면서 상속여부를 확인한다.

doori instanceof Doori가 true가 나오는 이유는 Doori의 프로토타입도 new Person, doori변수에서 사용된 프로토타입도 new Person으로 동일해서 이다. 내부적으로 생성된 생성자의 연결은 깨졌지만, instanceof가 프로토타입을 기준으로 비교하기 때문에 외부적으로 상속을 확인하는 동작은 정상으로 동작하는 것!

Object.create 함수

위와 같이 내부에서 생성자로 객체를 생성할때 연결이 깨지는걸 싫어하는 개발자들이 만든게 바로 이 함수다!

https://images.velog.io/post-images/dooreplay/4489a890-2efc-11ea-8c37-6f879ec9a53a/image.png

위의 캡쳐는 caniuse.com에서 Object.create 검색한 결과이다. 속깊자 서적이 출간된 해와는 달리 현재 대부분의 브라우저에서 지원을 해주는 속성이다!

써보자!

function Person(name) {
  this.name = name;
}

Person.prototype = {
  yell: function() {
    alert(`My name is ${this.name}`);
  }
};

let doori = Object.create(Person.prototype);
doori.name = "Doori";
doori.yell()

doori 변수에 doori.name과 같이 속성을 직접 부여하는게 비효율적으로 보이긴 하는데, 좀 더 직관적이다! 여튼 이 방식대로 쓰면 new 키워드를 쓰지 않고도 함수 호출로 객체가 생성이 된다. 전체적으로 소스에 생성자의 개념이 약해지고 객체의 인스턴스와 인스턴스 간 상속을 강조하는 것이 Object.create함수의 특징!

https://images.velog.io/post-images/dooreplay/0f297aa0-2f05-11ea-baff-b5f62b3e7332/image.png

new 키워드를 사용하는 경우 상속여부를 instanceof를 통해 구분했다. 근데 Object.create는 인자로 생성자가 아닌 객체를 받아서 이를 확인하는게 조금 다르다. 다시 말해, 인자로 Person.prototype을 사용해서 생성한 뒤 Person을 비교하는 것이 약간 직관성이 떨어진다. 예를들어…

let person = {
  yell: function() {
    alert(`My name is ${this.name}`)
  }
}

let doori = Object.create(person)
doori.name = "Doori";
doori.yell();

console.log(doori instanceof person)

doori instanceof person을 콘솔창에 찍어보면 TypeError가 발생한다. Object.create는 객체간 상속을 시켜주는 함수라서 Function을 인자로 받는 instanceof를 실행했을 때에는 에러가 날 수밖에 없다. 이럴때 필요한 것이 바로 Object.getPrototypeOf이다.

https://images.velog.io/post-images/dooreplay/af74a2a0-2f05-11ea-92df-19a12d7a5013/image.png

위의 캡쳐화면처럼 Object.create함수로 생성한 객체의 프로토타입을 확인하려면 Object.getPrototypeOf를 쓰시오~~

그밖에도.. Object.create함수 인자로 일반 객체를 활용했을 때는 isPrototypeOf를 쓰면 된다.(프로토타입 쪽에서 함수를 확인하려고 할 때)

Object.create의 객체 초기화

객체 생성시엔 new 키워드를 사용하면 생성자 안에서 생성되는 객체를 초기화하는 작업을 수행할 수 있는데, Object.create함수 구현방법을 보면 Object.create와 별도로 객체를 초기화한다. 이런 과정을 하나의 함수로 묶어서 객체별로 간단한 상속함수를 만들수도 있지만, Object.create함수는 2번째 인자를 선택적으로 받아서 객체를 초기화 한다.

예시를 살펴보자.

function Person(name) {
  this.name = name
}

Person.prototype = {
  yell: function() {
    alert(`My name is ${this.name}`)
  }
}

let doori = Object.create(Person.prototype, {
  name: {
    value : "Doori"
  }
})

doori.yell() // "My name is Doori"
doori.name = "Gollum"
doori.yell() // "My name is Doori"

doori.name을 골룸이라고 다시 설정해줬는데도 doori.yell을 실행시키면 골룸이 아닌 두리라고 뜬다. 왜냐, Object.create함수를 통해 값만 설정하면 읽기전용 속성이 되서 값을 수정할 수 없다. 이러한 읽기 전용 속성이 불편할것 같지만, 라이브러리를 만들때 굉장히 유용쓰..!

값을 수정하고 싶다면 Object.defineProperty 설정 파라미터를 수정해주면 된다.

  • value : 설정할 속성의 값(default: undefined)
  • configurable : 속성 지우거나 value 속성 외 설정속성을 바꿀지 여부(default : false)
  • enumerable : for-in 루프에서 해당 속성도 참조할지 여부(default : false)
  • writable : 속성 값 설정 가능 여부(default: false)
  • get: 속성 참조하게 되면 참조용으로 호출할 함수 (default: undefined)
  • set : 속성 설정하게 되면 설정용으로 호출할 함수 (default : undefined)

예를 들어 이렇게 써주면 된다.

let doori = Object.create(Person.prototype, {
  name: {
    value: "Gollum",
    configurable : true,
    enumerable: true,
    writable: true
  }
})

Object.create와 new 키워드 콜라보

두 개를 따로 쓰는 것이 아니라 조합해서 쓸 수도 있다는 사실! 둘을 한꺼번에 쓰면 앞서 나왔던 생성자의 연결이 깨지는 것을 보완해 설정하면 더욱 좋겠죠?

function Person() {
  this.name = "anonymous";
}

function Doori() {
  this.name = "Doori";
}

Doori.prototype = Object.create(Person.prototype, {
  constructor: {
    value: Doori
  }
})

let doori = new Doori();
console.log(doori instanceof Doori) // true
console.log(doori instanceof Person) // true
console.log(doori.constructor) // function Doori()

이렇게 쓰게 되면 Object.create의 두번째 인자로 Doori.prototype의 생성자 속성을 읽기 전용으로 설정해서 기존에 JS가 자체적으로 가지고 있었던 생성자의 연결이 깨졌던 부분이 보완되서 new 키워드를 통한 상속을 사용할 수 있다.

class와 extends를 통한 상속

new키워드와 함수를 사용하는 것에 대해 함수가 모호한 기능을 수행하고 있다는걸 인지한 개발자들은 ECMAScript6 부터는 class와 extends 개념을 도입했다.

class Person {
  constructor() {
    this.name = "anonymous"
  }
}

class Doori extends Person {
  constructor() {
    super();
    this.name = "Doori"
  }
}

let doori = new Doori();
console.log(doori instanceof Doori) // true
console.log(doori instanceof Person) // true
console.log(doori.constructor) // class Doori extends Person

리액트 클래스형 컴포넌트에서 많이 본 구조다 ㅋㅋ 여튼 js에서도 인제 클래스라는 개념이 생겨서 다른 객체지향 언어들과 비슷하게 상속을 할 수 있게 되었다. class와 extends를 쓰더라도 프로토타입 상속을 한것과 동일하게 instanceof가 작동하고, 생성자 또한 프로토타입과 동일하게 출력된다.

생성자 vs Object.create 성능 비교

성능 측면에서 보자면 생성자를 이용하는게 훨씬 좋다. 근데 앞서 살펴본 몇 가지 이슈들이 있고, 사실상 객체를 18만 개 이상 생성할게 아니면 큰 차이는 없다.

Object.create는 new와 같이 자바스크립트에 객체지향 개념을 넣고자 하는 게 아니라, 원래 js가 가지고 있는 Object 기반의 언어적 특징을 강화하면서, 객체지향이 필요할때 활용할 수 있는 class와 extends 키워드를 정의했다. 따라서 JS를 공부하는 우리 개발자들은 new와 Object.create를 구분해서 개발하는 지식이 필요하다! 성능 이슈는 서버환경에서 큰 영향이 없지만 모바일 환경에서의 자바스크립트라면 조금 고민해볼 필요가 있다는 거..!


정리해보자

  • Fucntion을 생성할 때 기본적으로 프로토타입 속성이 생성된다.
  • 프로토타입을 다른 객체로 설정함으로써 다른 객체의 속성들과 함수들을 공유/상속 할 수 있다
  • 객체는 프로토타입과 내부 링크로 연결되어 있어 프로토타입의 속성들을 자기의 속성인 것처럼 접근할 수 있다
  • 객체에서 직접 this.constructor.prototype으로 접근하지 않으면 프로토타입의 값은 수정되지 않고, 현재 객체 내에 같은 이름의 속성이 설정되어 프로토타입의 설정값이 가려진다
  • new로 객체를 생성할 때 프로토타입은 객체 간 공유되어 메모리 자원이 절약될 수 있다
  • 여러 개의 프로토타입 체인을 만들 경우 속성 조회에 있어서 성능 저하가 있을 수도 있다
  • 여러 개의 프로토타입 체인을 만들 경우 속성 조회에 있어서 성능 저하가 있을 수도 있다
  • new는 생성자 기반 상속으로 사용되며 Object.create는 객체 기반 상속으로 사용된다.
  • 객체지향을 위한 class와 extends가 키워드가 정의되어있다.
  • new와 생성자로 객체를 생성하는 것이 Object.create 로 생성하는 것보다 성능상 유리하다
  • 성능 이슈는 객체를 다량응로 사용하는 경우 조심하면 좋지만 특별한 경우가 아니면 체감하기 힘들다.

Reference

  • 속깊은 자바스크립트 (양성익 저)