prototype 두번째 정리 들어갑니다.

 

※ 객체의 관계에 대하여 

자바스크립트의 모든 객체는 Object 객체를 그 원형으로 두고 있습니다. 근데 문제는 자바스크립트에 미리 정의된 객체가 Object 만 있는게 아니라는거죠. String, Array, Number 등의 Wrapper 객체가 여러개 있고, web이냐 nodeJS냐에 따라 미리 정의된 여러개의 객체가 다 다르죠. 우리가 만약 Number 객체의 원형을 이용하되, 그대로 호출하지 않고 몇가지 기능을 더 추가해서 StrongNumber 라는 객체 원형을 만들고 싶다면 어떻게 해야할까요? 혹은 기존 자바스크립트의 Date 객체 기능이 맘에 안들어서 내가 직접 커스터마이징을 하겠다면? 마치 자바의 상속 구조를 타는 거 같은 느낌을 자바스크립트에도 적용하면 좋지 않을까요? prototype을 이용해서 객체간의 의존관계를 만드는 방법을 prototype chaining 이라고 합니다.


※ prototype chaining

자바스크립트의 모든 객체는 Object 객체를 그 원형으로 두고 있다면, Array, Date 등의 자바스크립트 기본 객체들은 정의되어 있는 걸까요? 이들 객체의 원형도 분명 Object 객체 원형일텐데요. 우리는 이에 대한 해답을 prototype에서 찾을 수 있습니다.

Array 객체의 원형을 찍어봤는데요. __proto__ 로 객체의 프로트타입에 접근 가능하다고 했었죠? new 키워드로 생성한 Array 객체는 prototype이 Array 객체의 생성자 함수의 영역으로 되는거죠. 근데 new Array().__proto__.__proto__ 를 볼까요? Object 라고 나오네요. __proto__.__proto__를 확장해보죠.

__proto__.__proto__.constructor 가 Object 객체의 생성자 함수네요. 즉 Array 객체의 원형은 Object 객체의 원형을 프로토타입으로 보고 있는거죠. 이런 방식이 바로 prototype chaining입니다. 새로운 객체 원형의 프로토타입을 기존에 어떤 객체와 연결을 시켜서 프로토타입의 프로토타입으로 접근가능하게 하는거죠.


※ prototype chaining의 구현

이 방식을 이해하는 제일 좋은 방법은 예제 소스를 만들어보는거겠죠? 간단한 예제를 만들어보았습니다.

function Person(_name, _age, _sex, _contact){
  this.name = _name || '이름모름';
  this.age = _age || '나이모름';
  this.sex = _sex || '성별모름';
  this.contact = _contact || '연락처없음';
}

Person.prototype.getInfo = function(){
  return {
    name : this.name,
    age : this.age,
    sex : this.sex
  };
}

function Employee(_name, _age, _sex, _contact, _dept, _grade, _salary){
  this.name = _name || '이름모름';
  this.age = _age || '나이모름';
  this.sex = _sex || '성별모름';
  this.contact = _contact || '연락처없음';
  this.dept = _dept || '미발령';
  this.grade = _grade || '미분류';
  this.salary = _salary || 0;
}

Employee.prototype = new Person();
Employee.prototype.constructor = Employee;
Employee.prototype.getEmpInfo = function(){
  let obj = this.getInfo();
  obj.dept = this.dept;
  obj.grade = this.grade;
  obj.salary = this.salary;
  return obj;
}

실행결과는 다음과 같습니다.

Employee 객체가 생성되었는데, 상위객체인 Person의 프로퍼티를 중복으로 갖고 있네요. 정확히 말하면 Person의 프로퍼티에 접근을 못하고 있는거죠. 덕분에 입력이 제대로 안된거처럼 보일수 있어서 상당히 문제가 될거 같죠?

그럼 Person의 프로퍼티로 접근가능하도록 바꿔야 할텐데요. 이걸 해결하려면 Employee와 Person사이에 Bridge 역할을 하는 객체가 필요해서 서로 상-하위 관계를 설정해주어야 합니다. 이걸 이용해서 위의 예제를 다음과 같이 수정했습니다.

const extendClass = (function(){
  function Bridge(){}
  return function(Parent, Child){
    Bridge.prototype = Parent.prototype;
    Child.prototype = new Bridge();
    Child.prototype.constructor = Child;
    Child.prototype.superClass = Parent;
  }
})();

function Person(_name, _age, _sex, _contact){
  this.name = _name || '이름모름';
  this.age = _age || '나이모름';
  this.sex = _sex || '성별모름';
  this.contact = _contact || '연락처없음';
}

Person.prototype.getInfo = function(){
  return {
    name : this.name,
    age : this.age,
    sex : this.sex
  };
}

function Employee(_name, _age, _sex, _contact, _dept, _grade, _salary){
  this.superClass(_name, _age, _sex, _contact);
  this.dept = _dept || '미발령';
  this.grade = _grade || '미분류';
  this.salary = _salary || 0;
}

extendClass(Person, Employee);
Employee.prototype.getEmpInfo = function(){
  let obj = this.getInfo();
  obj.dept = this.dept;
  obj.grade = this.grade;
  obj.salary = this.salary;
  return obj;
}

Bridge 객체는 객체간의 상-하위 설정 외에는 외부에서 쓰일 일이 없기 때문에 굳이 노출시킬 필요가 없겠죠? 그래서 클로저로 extendClass의 실행부분을 감싸서 Bridge 객체를 접근하지 못하게 하며, 우리가 원하는 상-하위 객체설정만 하는 함수를 반환하게 했습니다.

➣ 클로저는 1회성 코드라고 생각하시면 편한데요. 정확하게는 자바스크립트 소스가 쭉 실행될때 한번 딱 돌고 끝나는 코드 부분입니다. 클로저 부분에서 return으로 함수를 넘기는걸 extendClass 변수가 받아서 extendClass 를 함수로 만드는거죠.

실행결과는 다음과 같습니다.

Employee를 구현한 객체에서 프로퍼티의 중복이 사라려서 훨씬 깔끔해졌네요. Person과 Employee의 생성자를 extendClass 함수로 상-하관계를 설정했기 때문에 Employee의 생성자를 실행하면서 만들어진 객체는 하나의 실행컨텍스트가 되어서 프로퍼티가 모두 한곳에 모인거 같습니다.

원리를 이해하기 위해 prototype chaining을 직접 구현했지만 계속 이런식으로 코드를 짠다고 생각하면... 솔직히 가독성도 떨어지고 별로 좋을거 같지는 않네요. 사실 초보자 입장에선 이런 부분들이 난이도를 높이는데 한몫 하는거고요. 그래서 자바스크립트에서는 간단하게 prototype chaining을 설정할 수 있는 방법을 제공하고 있습니다. 이건 다음 포스트에 이어서 작성하겠습니다.


※ 레퍼런스

이 글은 인프런 사이트의 '핵심개념을 알아보는 Javascript Flow' 강좌의 내용을 토대로 예제코드를 작성하여 설명하였습니다. 클로저 및 prototype 개념을 포함한 기본 자바스크립트를 더 깊이 이해하기를 원하시면 해당 강좌를 수강하시는 것을 권장합니다. 이 강좌는 무료입니다.

Posted by kevin.jeong.
,

대망의 프로토타입답게 설명이 길어질거 같군요.

사실상 프로토타입이 자바스크립트 문법의 꽃이라고 보면 됩니다.

이거 덕분에 자바스크립트가 오늘날 아주 화려해진거고요.

프로그래밍 익숙하지 않으신 분들은 장담컨데 토나온다고 하실겁니다.

(저도 prototype 구현으로 작성된 자바스크립트 소스 처음볼때 미치는줄 알았습니다ㅠㅠ 그땐 누가 뭐 알려주지도 않고 옆에서 갈구기만 하고 아주 그냥 XX XXX XXXX XXXXXXXXXXXXXXX!!!!!!!)

그래서 프로토타입은 2탄 내지 3탄 정도로 나눠서 정리하겠습니다. 만약 다 보고도 이해가 안되시면 프로그래밍 경험을 더 쌓고 오세요. 제가봐도 너무 4가지 없지만 이게 현실입니다. 회사가면 자비 따위 없습니다...

그럼 시작하겠습니다.


※ constructor

자바스크립트에는 클래스라는 구조는 없지만, 객체의 구조를 미리 만들어서 공유하거나 공통된 프로퍼티를 지정하기 위해 생성자 함수를 선언할 수 있습니다. 그리고 new 키워드를 통해 생성자에서 구현한대로 틀을 갖춘 객체가 생성되어 반환되죠.

예제를 통해 설명하겠습니다.

function dateExprObj(_year, _month, _date) {
  this.checkParams = function(__year, __month, __date){
    let result = true;
    if(!Number.isInteger(__year * 1)){
      console.log("첫번째 파라미터는 년도에 해당하므로 숫자형태로만 입력하셔야 합니다!");
      result = false;
    }

    if(!(__month * 1 >= 1 && __month * 1 <= 12)){
      console.log("두번째 파라미터는 월에 해당하므로 1~12 사이의 숫자만 입력하셔야 합니다!");
      result = false;
    }

    if(!(__date * 1 >= 1 && __date * 1 <= 31)){
      console.log("세번째 파라미터는 일에 해당하므로 1에서 최대 31 까지의 숫자만 입력하셔야 합니다!");
      result = false;
    } else {
      let dmonth = __month * 1;
      let dint = __date * 1;
      let allowdMaxDate;
      switch (dmonth) {
        case 4:
        case 6:
        case 9:
        case 11:
          allowdMaxDate = 30;
          break;
        case 2:
          allowdMaxDate = 28;
          break;
        default:
          allowdMaxDate = 31;
          break;
      }
      if(__date * 1 > allowdMaxDate){
        console.log("일자에 해당하는 값이 잘못 입력되었습니다. 입력된 일자 : " + __date + ", 입력 가능한 최대일자 : " + allowdMaxDate);
        result = false;
      }
    }

    return result;
  };
  this.year = _year * 1;
  this.month = _month * 1;
  this.date = _date * 1;
  this.available = this.checkParams(_year, _month, _date);
}

let tobj1 = new dataExprObj("2017", "2", "24");

생성자임에도 선언은 function 키워드로 하기 때문에 보통은 생성자 함수라고 부릅니다. this 키워드를 사용해서 실행컨텍스트에서 프로퍼티를 저장하도록 처리하죠. 이렇게 선언한 이후 마지막 줄에서 new 키워드로 객체를 생성했네요.

자바스크립트에서 생성자의 의미는 객체를 찍어내기 위한 틀을 만드는 거라고 생각하시면 될거 같습니다~!


※ prototype

생성자함수에는 prototype 이라는 프로퍼티가 자동으로 할당되는데, new 키워드를 통해 생성되는 객체들에게 공통적인 프로퍼티를 할당하기 위해 사용됩니다. 예제를 보도록 하죠.

dateExprObj.prototype.addYear = function(inc_year){
  if(!this.available) return false;
  this.year = this.year * 1 + inc_year;
}

dateExprObj.prototype.subYear = function(sub_year){
  if(!this.available) return false;
  this.year = this.year * 1 - sub_year;
}

dateExprObj.prototype.addMonth = function(inc_num){
  if(!this.available) return false;

  if(this.month + inc_num > 12){
    this.year += Math.trunc((this.month + inc_num) / 12);
    this.month = (this.month + inc_num) % 12;
  } else if(this.month + inc_num < 1){
    this.year += Math.trunc(((this.month + inc_num) / 12) - 1);
    this.month = (this.month + inc_num) % 12 + 12;
  }
}

dateExprObj.prototype.subMonth = function(sub_num){
  if(!this.available) return false;
  this.addMonth(-sub_num);
}

dateExprObj.prototype.toDateStr = function(){
  if(this.available)
    return this.year + "-" + this.month + "-" + this.date;
  else {
    console.log("날짜 파라미터가 잘못 입력되었습니다!");
    console.log("입력된 년도 : " + this.year);
    console.log("입력된 월  : " + this.month);
    console.log("입력된 일  : " + this.date);
  }
}

let obj1 = new dateExprObj("2018", "2", "25");
obj1.addMonth(25);
console.log(obj1.toDateStr());
obj1.subMonth(14);
console.log(obj1.toDateStr());

생성자함수.prototype. 으로 생성할 객체에서 사용할 공통 메소드를 지정하였습니다. 이후 객체를 생성해서 prototype으로 지정한 메소드를 마치 자기가 갖고 있는 메소드처럼 사용하네요.

예제의 실행결과는 다음과 같습니다.


※ 객체와 prototype 의 연결

이제 실제로 생성자함수와 new 키워드로 생성된 객체가 어떻게 생겼는지 한번 까보겠습니다.

앞서 설명드린거 처럼 생성자 함수에 함수에 prototype 이라는 프로퍼티로 공통메소드가 선언되었는데요. new 키워드로 생성된 객체인 obj1은 __proto__ 라는 프로퍼티가 있고, 여기서 생성자 함수의 prototype에 선언된 공통 메소드를 접근하네요.

(obj1.__proto__.__proto__ 를 보면 Object 가 나오죠? 자바스크립트의 모든 객체는 Object 객체를 그 원형으로 두고 있는겁니다.)

여기서 중요한게 바로 __proto__ 입니다. 객체의 원형이라고 볼수도 있는 생성자 함수와 프로토타입을 접근하게 해주는 녀석이거든요. 보통은 생략해서 쓰기 때문에 이렇게 콘솔로그로 찍어보지 않는 이상은 파악이 잘 안되죠.  이걸 도형으로 만들어서 설명한게 있어서 공유합니다.

생성자 함수를 선언하면 prototype 이라는 프로퍼티가 생성됩니다. 이 안에서 new 키워드로 생성할 객체의 공통된 메소드 혹은 속성들을 설정할 수 있죠. 그런 다음 new 키워드로 instance 객체를 생성하면 생성자 함수가 실행되고, __proto__ 라는 프로퍼티가 생성자 함수의 프로토 타입을 참조합니다.  __proto__는 생략되므로 마치 아래와 같은 모습처럼 되는 거죠.

(만약 __proto__를 생략하지 않은 경우는 실행컨텍스트가 달라져서 문제가 될수도 있습니다. 직접 구현해보고 테스트하면서 파악해보세요.)

생성자 함수, prototype, instance 객체의 관계는 이렇게 삼각형 구조처럼 갖추게 됩니다. 자바스크립트에서 객체 생성의 큰 그림은 이렇게 된다고 보시면 됩니다.

기존에

var tmp_obj1 = {
  tval1 : "1234",
  tint1 : 33,
  tf : function (abc) {
    console.log(abc);
  }
};

...

이런식으로 객체를 생성하신 분들도 많으실텐데요. 이건 엄밀히 말하면 JSON(JavaScript Object Notation)방식으로 객체를 간편하게 생성하는 하나의 방법일 뿐, 객체간의 관계를 설명하지는 못한다고 생각하시면 되겠습니다.


※ 레퍼런스

이 글은 인프런 사이트의 '핵심개념을 알아보는 Javascript Flow' 강좌의 내용을 토대로 예제코드를 작성하여 설명하였습니다. prototype 개념을 포함한 기본 자바스크립트를 더 깊이 이해하기를 원하시면 해당 강좌를 수강하시는 것을 권장합니다. 이 강좌는 무료입니다.

 

다음에는 프로토타입 체이닝 등으로 프로토타입을 더 깊게 들어가보겠습니다~

 

 

Posted by kevin.jeong.
,