ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] Prototype(프로토타입)
    Front-End(Web)/Javascript 2021. 2. 24. 02:45
    반응형

    🤔 서론

    프로토타입 기반 언어란 무엇일까? JS의 프로토타입 언어의 특징은 뭐가 있을까?

    이를 이해해야 ES5까지 있었던 클래스 문법, 그리고 ES6에 새로 등장하게 된 클래스 문법에 대해 이해할 수 있을 것 같다.


    📒 Prototype 기반 언어

    Javascript는 원형 객체로 새로운 객체를 생성하는 프로토타입(원형) 기반 언어이다. 이는, C나 Java같은 클래스 기반 언어와 구분된다.

     

    - 프로토타입 기반 언어

    [위키피디아]

    프로토타입 기반 언어는 클래스 기반 언어에서 상속을 사용하는 것과는 다르게, 객체를 원형(프로토타입)으로 하는 복제 과정을 통해 객체의 동작 방식을 재사용 할 수 있게 한다.

    프로토타입 기반 언어는 원형 객체를 복제하여 새로운 객체를 생성하는 언어이다. 그런데 자바스크립트도 복제를 하는가?

    자바스크립트는 복제가 아닌 프로토타입 링크를 타고 가서 원형을 참조한다.

     

    - 프로토타입 링크

    자바스크립트에서 단순 원시 타입(simple primitive)인 문자열, 숫자, 불리언, null, undefined를 제외한 모든 타입은 객체다.

     

    Javascript 객체에 대한 정의는 위와 같다. 즉, Javascript 에서는 배열도 객체고 함수도 객체인 것이다.

    Javascript의 모든 객체는 원형 객체로부터 생성되며, 생성된 객체는 원형에 대한 프로토타입 링크(__proto__)를 가진다.

    * __proto__ 는 원형에 대한 참조정보를 갖는 객체의 내부속성으로, ES6부터는 표준으로 제정되었다.

     

    원형 또안 객체이기에, 이는 또다른 원형을 참조하고 이렇게 연속된 프로토타입 링크를 통해 최종원형인 Object.prototype까지 연결된다.

    이러한 일련의 링크를 프로토타입 체인이라고 부른다.

    - 프로토타입 기반 프로그래밍(Prototype-based programming)

    다른 명칭으로는 클래스 리스(class-less), 프로토타입 지향(prototype-oriented), 인스턴스(instance-based) 프로그래밍으로 불린다.

    프로토타입 프로그래밍은 Java의 클래스 상속을 흉내내는 방식인데, 아래에서 차이점을 보도록 하자.

     

    * 클래스 기반

    클래스라는 추상화된 개념을 선언한 뒤, 이를 기반으로 객체에 상속을 지원한다. 여기서, 클래스는 객체(or 함수)가 아니다.

    이 클래스를 기반으로 객체를 생성하며, 이를 '인스턴스' 라고 칭한다.

    // 1. 클래스 정의 
    public class Person{ 
      public string country = "korea"; 
      public string name; public int age; 
      
      // 2. 클래스 생성자 정의 
      public Person(string name, int age){ 
        this.name = name; this.age = age; 
      } 
    } 
    
    // 3. 객체 생성 
    Person boy = new Person("yoonhee", "12");

     

    * 프로토타입 기반

    프로토타입 원형 객체를 생성한 뒤, 이 객체를 이용해서 클래스 상속을 흉내낸다. 여기서, 생성자는 프로토타입 상속절차가 필요하다.

    // 1. 프로토타입 객체 정의 
    var base = { 
      country = "korea" 
    } 
    
    // 2. 프로토타입 객체 생성자 정의 
    var Person = function(name, age){ 
      this.name = name; 
      this.age = age; 
    } 
    
    // 3. 생성자에 프로토타입 상속 
    Person.prototype = base; 
    
    // 4. 객체 생성 
    var boy = new Person("yoonhee", "12");
    

    이를, 상속이 아닌 __proto__ 링크를 통해 참고하면서 상속을 받는 것(상속을 흉내내는 것에 가깝다)이 자바스크립트의 방식인 것 같다.


    📒 Prototype 구현

    Javascript 에는 ES6에 Class 문법이 추가됬지만, 그렇다고 클래스 기반언어가 된 것은 아니다. (여전히 프로토타입 기반이다.)

    하지만, 함수(function)와 new 연산자를 통해 클래스를 흉내낼 수 있다.

     

    function Person() {
      this.eyes = 2;
      this.nose = 1;
    }
    
    var kim  = new Person();
    var park = new Person();
    
    console.log(kim.eyes);  // => 2
    console.log(kim.nose);  // => 1
    console.log(park.eyes); // => 2
    console.log(park.nose); // => 1

    위 같은 사례를 보자. kim, park 은 공통적으로 eyes, nose를 가지고 있으나, 각각 변수가 메모리에 할당되므로 4개의 메모리가 소비된다.

     

    function Person() {}
    Person.prototype.eyes = 2;
    Person.prototype.nose = 1;
    
    var kim  = new Person();
    var park = new Person():
    
    console.log(kim.eyes); // => 2
    // ...

    이 문제를 프로토타입으로 해결할 수 있다. 먼저, Person.prototype 이라는 객체(Object)가 어딘가 존재한다.

    Person() 함수로부터 생성된 kim, park 객체는 위 객체(Person.prototype)에 접근해서 여기에 존재하는 값들을 사용할 수 있다.

    , 프로토타입은 어떤 객체의 상위레벨에서, 참조를 위한 객체로서 존재하는 것이다. 참조를 이해하기 위해 세부개념으로 들어가자.

     

     

    📒 Prototype 구성

    Javascript에는 Prototype Object, Prototype Link 가 존재하며, 이 둘을 통틀어 Prototype 이라고 부른다.

     

    - Prototype Object

    자바스크립트에서 객체는 언제나 함수(Function)로 생성된다.

    function Person() {} // => 함수
    var personObject = new Person(); // => 함수로 객체를 생성

    이는, 내가 평소에 빈 객체를 형성할 때도 마찬가지이다.

    var obj = {};			// 이렇게 생성되는거 같지만
    var obj = new Object();		// 사실 이 문법과 동일한 것이다!

    이 Object() 역시 함수로, 자바스크립트에서 기본적으로 제공하는 함수인 것이다. 배열(Array), 함수(Function)도 마찬가지이다.

    이렇게, 함수가 정의될 때는 2가지 일이 동시에 이루어진다.

     

    1) 해당 함수에 Constructor(생성자) 자격 부여

    생성자 자격이 부여되면, new 키워드를 통해 객체를 만들어낼 수 있다.

     

    2) 해당 함수의 Prototype Object 생성 및 연결

    함수를 정의하면, 함수뿐만 아니라 이것의 Prototype Object 도 같이 생성된다.

    이처럼, 생성된 함수(Person)는 prototype 속성을 통해, Prototype Object 객체에 접근할 수 있다.

    Prototype Object 역시 일반적인 객체로, 속성으로 constructor, __proto__ 를 가지고 있다.

    • constructor(생성자) : Prototype Object 와 함께 생성되었던 함수(Person)을 가리키고 있다.
    • __proto__(링크) : Prototype Link 이다. 아래에서 다시 언급하겠다.

     

    이제, 왜 위의 예제에서 Person.prototype 을 사용했는지 확인할 수 있다.

    Prototype Object는 일반적인 객체이므로, 속성을 임의로 추가/삭제할 수 있다. kim, park 모두 결국 이 프로토타입을 참조한 것이다.

     

    * [[Prototype]] vs prototype 프로퍼티

    • [[Prototype]] : 모든 객체가 갖는 필드. 객체 입장에서 부모역할을 하는 프로토타입 객체를 가리키며, 함수는 Function.prototype.
    • prototype 프로퍼티 : 함수 객체만 갖는 필드. 함수가 생성자로 사용될 때, 이로 생성된 객체의 부모역할을 하는 프로토타입 객체를 가리킨다. 

     

    - Prototype Link : __proto__

    kim은 다음과 같이 빈 객체이다. 하지만, 어떻게 kim.eyes 는 빈 값이 아닌, 2가 출력되는 것일까? 이를 가능케 하는 것이 __proto__ 이다.

    이를 Prototype Link 라고 하며, 모든 객체가 빠짐없이 가지고 있는 속성이다. 

    이 링크는 객체가 생성되었을 때 조상이었던 함수의 Prototype Object를 가리키고 있다. 즉 Person의 Prototype Object 가 된다.

    kim은 eyes라는 필드를 직접 가지고 있지 않다. 하지만, 바로 undefined를 반환하는 것이 아니라 프로토타입 탐색이 선행된다.

    최상위 프로토타입까지 확인하면서 eyes 속성을 찾는다면 해당값을, 아니면 undefined 가 반환되는 것이다. (프로토타입 체인)


    📒 Prototype Chain

    자바스크립트는 특정 객체의 프로퍼티나 메서드에 접근할 때, 해당 객체에 존재하지 않다면 [[Prototype]] 링크를 따라 부모 프로토타입 객체를 차례대로 검사한다. 이것이 위에서 언급한 프로토타입 체인이었다.

    var student = {
      name: 'Lee',
      score: 90
    }
    
    // Object.prototype.hasOwnProperty()
    console.log(student.hasOwnProperty('name')); // true
    console.log(student.__proto__ === Object.prototype); // true
    console.log(Object.prototype.hasOwnProperty('hasOwnProperty')); // true

    대표적으로, 객체의 메서드들이 있겠다. student 객체는 hasOwnProperty() 메서드가 없다.

    하지만, 객체의 프로토타입인 Object.prototype 객체는 hasOwnProperty() 메서드를 지원하기 때문에, 이를 호출한 것이다.

     

    1. 객체 리터럴 방식으로 생성된 객체의 프로토타입 체인

    자바스크립트의 객체 생성방법은 3가지가 있다.

    1. 객체 리터럴
    2. Object() 생성자 함수
    3. 생성자 함수(Function) 활용

    객체 리터럴은 결국, 자바스크립트 내장함수인 Object() 생성자 함수로 객체를 생성하는 것을 단순화한 것이다.

    자바스크립트 엔진은 객체 리터럴로 객체를 생성하는 코드를 만나면, 내부적으로 Object() 생성자 함수를 적용해 객체를 생성한다.

     

    즉, 객체 리터럴을 사용해 객체를 생성한 경우, 이 객체의 프로토타입 객체는 Object.prototype 이 되는 것이다.

    var person = {
      name: 'Lee',
      gender: 'male',
      sayHello: function(){
        console.log('Hi! my name is ' + this.name);
      }
    };
    
    console.log(person.__proto__ === Object.prototype);   // ① true
    console.log(Object.prototype.constructor === Object); // ② true
    console.log(Object.__proto__ === Function.prototype); // ③ true
    console.log(Function.prototype.__proto__ === Object.prototype); // ④ true

     

    2. 생성자 함수로 생성된 객체의 프로토타입 체인

    생성자 함수로 객체를 생성하려면 우선 생성자 함수를 정의해야 한다. 함수 정의방법은 3가지가 있다.

    // 1) 함수 선언식
    let Square = function(l) {
      return l * l;
    }
    
    // 2) 함수 표현식
    function Square(l) {
      return l * l;
    }
    
    // 3) Function() 생성자 함수
    let Square = new Function();
    Square = // ....

    마찬가지로, 1) 함수 선언식, 2) 함수 표현식은 Function() 생성자 함수의 절차를 단순화시킨 것이다.

    그렇기 때문에, 모든 생성자 함수의 프로토타입은 Function.prototype 이다.

     

    이제, 생성자 함수로 생성된 객체를 보자. 이 객체는 Object.prototype 이 아닌, [생성자 함수].prototype 을 프로토타입으로 가진다.

    객체 생성 방식 엔진의 객체 생성 인스턴스의 prototype 객체
    객체 리터럴 Object() 생성자 함수 Object.prototype
    Object() 생성자 함수 Object() 생성자 함수 Object.prototype
    생성자 함수 생성자 함수 생성자 함수.prototype

     

    function Person(name, gender) {
      this.name = name;
      this.gender = gender;
      this.sayHello = function(){
        console.log('Hi! my name is ' + this.name);
      };
    }
    
    var foo = new Person('Lee', 'male');	// 생성자 함수로 생성된 객체(인스턴스)
    
    console.log(foo.__proto__ === Person.prototype);                // ① true
    console.log(Person.prototype.__proto__ === Object.prototype);   // ② true
    console.log(Person.prototype.constructor === Person);           // ③ true
    console.log(Person.__proto__ === Function.prototype);           // ④ true
    console.log(Function.prototype.__proto__ === Object.prototype); // ⑤ true

    앞서, 자바스크립트에선 함수도 객체이기 때문에, 결국 Object.prototype 에서 체인이 귀결된다.

    이 객체를 프로토타입 체인 종점(End of Prototype Chain) 이라고 칭한다.

     

    3. 프로토타입 체인 동작 조건

    객체 프로퍼티를 참조할 때, 해당 객체에 프로퍼티가 없다면 프로토타입 체인이 동작하게 된다.

    객체에 프로퍼티를 추가할 경우에는 프로토타입 체인이 동작하지 않는다.

    이는, 객체에 해당 프로퍼티가 있다면 값을 재할당하지만, 프로퍼티가 없다면 해당 객체에 동적으로 추가하기 때문이다.

    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.gender = 'male'; // ①
    
    var foo = new Person('Lee');
    var bar = new Person('Kim');
    
    console.log(foo.gender); // ① 'male'
    console.log(bar.gender); // ① 'male'
    
    // 1. foo 객체에 gender 프로퍼티가 없으면 프로퍼티 동적 추가
    // 2. foo 객체에 gender 프로퍼티가 있으면 해당 프로퍼티에 값 할당
    foo.gender = 'female';   // ②
    
    console.log(foo.gender); // ② 'female'
    console.log(bar.gender); // ① 'male'

    * foo, bar 모두 Person.prototype 을 참조하기 때문에 gender 값이 출력된다. 또한, foo의 gender 수정은 bar에 영향을 미치지 않았다.


    📒 Prototype 추가 문법

    1. Prototype 객체 확장

    프로토타입도 객체이므로 프로퍼티의 추가/삭제가 가능하다. 이렇게, 추가/삭제된 프로퍼티는 즉시 프로토타입 체인에 반영된다.

    Person.prototype 프로토타입에 sayHello() 메서드가 추가되었고, 여기에 상속된 foo 객체 역시 이 메서드를 참조할 수 있다.

    function Person(name) {
      this.name = name;
    }
    
    var foo = new Person('Lee');
    
    Person.prototype.sayHello = function(){
      console.log('Hi! my name is ' + this.name);
    };
    
    foo.sayHello();

     

    2. 원시타입(Primitive Data Type) 확장

    자바스크립트에서 원시타입(Number, String, Boolean, Null, Undefined)을 제외한 모든 것은 객체이다.

    하지만, 아래 예제를 보면 원시타입인 문자열(String)이 객체와 유사하게 동작한다.

    var str = 'test';
    console.log(typeof str);                 // string
    console.log(str.constructor === String); // true
    console.dir(str);                        // test
    
    var strObj = new String('test');
    console.log(typeof strObj);                 // object
    console.log(strObj.constructor === String); // true
    console.dir(strObj);
    // {0: "t", 1: "e", 2: "s", 3: "t", length: 4, __proto__: String, [[PrimitiveValue]]: "test" }
    
    console.log(str.toUpperCase());    // TEST
    console.log(strObj.toUpperCase()); // TEST

    원시타입 문자열과 String() 생성자 함수로 생성한 문자열 객체의 타입은 분명 다르다. (string vs object)

     

    원시타입은 객체가 아니므로, 프로퍼티나 메서드를 가질 수 없다. 아래처럼, 원시타입에 메서드를 직접 추가한 뒤 확인하면 에러가 생긴다.

    var str = 'test';
    
    // 에러가 발생하지 않는다.
    str.myMethod = function () {
      console.log('str.myMethod');
    };
    
    str.myMethod(); // Uncaught TypeError: str.myMethod is not a function

    하지만, String() 객체의 프로토타입인 String.prototype 객체에 메서드를 추가하면 객체뿐만 아니라 원시타입도 메서드를 사용할 수 있다.

    var str = 'test';
    
    String.prototype.myMethod = function () {
      return 'myMethod';
    };
    
    console.log(str.myMethod());      // myMethod
    console.log('string'.myMethod()); // myMethod

     

    자바스크립트의 내장함수인 String(), Number(), Array() 표준 메서드는 각각 프로토타입 객체인 String.prototype, Number.prototype, Array.prototype 등에 정의되어 있다. 

    이들 역시, Object.prototype 으로 귀결되며, 자바스크립트는 표준 내장함수의 프로토타입 객체의 개발자 메서드 추가를 허용한다.

     

    3. 프로토타입 객체 변경

    객체를 생성하면 프로토타입은 결정된다. 이를 다른 임의의 객체로 변경할 수 있다.

    • 프로토타입 객체 변경 시점 이전에 생성된 객체 : 기존 프로토타입 객체를 [[Prototype]] 에 바인딩한다.
    • 프로토타입 객체 변경 시점 이후에 생성된 객체 : 변경된 프로토타입 객체를 [[Prototype]] 에 바인딩한다.
    function Person(name) {
      this.name = name;
    }
    
    var foo = new Person('Lee');
    
    // 프로토타입 객체의 변경
    Person.prototype = { gender: 'male' };
    
    var bar = new Person('Kim');
    
    console.log(foo.gender); // undefined (변경전 객체 -> 기존 Person.prototype)
    console.log(bar.gender); // 'male' (변경후 객체 -> 변경된 Person.prototype)
    
    console.log(foo.constructor); // ① Person(name)
    console.log(bar.constructor); // ② Object()

    foo 객체는 Person() 생성자 함수를 프로토타입으로 가졌다.

    bar 객체는 프로토타입이 변경되면서, Person.prototype이 일반 객체를 가리키게 되면서 constructor 프로퍼티가 삭제되었다. (함수X)

    즉, Person.prototype 이 아닌, Object.prototype 의 constructor 로 연결되는 것이다.


    클래스를 공부하기에 앞서, 자바스크립트의 클래스는 클래스의 모방임을 이해해야했다.

    여기에 연계되는 개념이 프로토타입 기반 언어라는 점이었다. 이 프로토타입의 플로우에 대해 제대로 공부한 계기였다.

     

    [출처]

    - MDN 공식 사이트 : developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain

    - bluesh55 님의 블로그 : https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67

    - PointMaWeb : https://poiemaweb.com/js-prototype  

    반응형
Designed by Tistory.