Programming language/JavaScript

JavaScript, Prototype

iKay 2020. 11. 14. 18:45
반응형

서론

ES6+ 이후 class 문법이 지원되면서 prototype을 직접 사용할 일이 드물겠지만, JavaScript 객체를 더 잘 이해하기 위해서는 prototype이 무엇인지 어떻게 동작하는지 어느정도 이해할 필요가 있다. 그래서 이번에는 JavaScript의 prototype에 대해 정리해보고자 한다.

 

Prototype이란?

JavaScript에서 prototype은 함수를 정의하자 말자 사용할 수 있는 속성이다. 만약 foo라는 함수를 생성하는 경우, foo라는 함수의 속성으로 prototype 이라는 이름의 속성도 함께 생성된다. 이 prototpye 속성은 함수 자체에는 아무런 영향을 미치지 않고, foo를 생성자로 호출(new foo())할 때 생성된 객체는 함수 foo의 속싱인 prototype과 연결된 링크(__proto__)를 갖게 된다. 이런 특성 때문에 prototype 링크는 동적으로 추가되거나 변경될 수 있다.

 

function Person(name) {
    this.name = name;
    this.sayHello = function() {
        console.log('Hi i am ' + this.name);
    } 
}
var p1 = new Person('Amy');

p1.walk(); // (1) p1.walk is not a function

Person.prototype.walk = function() {
    console.log(this.name + 'walks');
}

p1.walk(); // (2) Amy walks

 

(1) 시점에서 Person의 prototype 속성은 walk를 갖는다.
(2) 시점에서 Person의 prototype 속성은 walk를 갖게 된다.

 

동작방식

p1 이라는 객체는 walk라는 속성을 갖고 있지 않다. 하지만 어떻게 (2)에서 "Amy walks" 를 출력할 수 있을까? 바로 prototype 링크를 통해 Person.prototype.walk에 접근했기 때문에 가능한 것이다. 실제로 p1.__proto__ === Person.prototype 임을 확인할 수 있다.

 

P1.__proto__ === Person.prototype 이 된다.

 

prototype 링크를 조금 더 일반화 시키면, JavaScript의 모든 객체는 Object 라는 함수의 생성자로부터 만들어지므로 계속적으로 prototype 링크를 타고 거슬러 올라가게 되면 Object 함수의 prototype 객체까지 갈 수 있게 된다. 흔히 우리가 사용하는 hasOwnProperty() 와 같은 객체 메소드는 Object의 prototype 속성이다.

 

오버라이딩

동작방식에서 객체의 속성을 참조할 때 객체 자체 속성을 먼저 검색한 후 prototype 링크를 검색한다고 했다. 아래 코드에서객체 p1에 name이라는 속성이 있기 때문에 prototype을 검색하지 않는 것이다.

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

Person.prototype.name = 'Jane';

var p1 = new Person();

console.log(p1.name); // Amy
console.log(p1.__proto__.name); // Jane
console.log(p1.hasOwnProperty('name')); // true

 

p1 에서 name 속성을 지우면 name prototype 링크를 통해 Jane을 참조하게 되고 p1.hasOwnProperty('name') === false 가 된다.

function Person1(name) {
    this.name = 'Amy';
}

Person1.prototype.name = 'Jane';
var p1 = new Person1();

delete p1.name;
console.log(p1.name); // Jane
console.log(p1.hasOwnProperty('name')); // false

 

메모리 관점

prototype을 사용하게 되면 객체가 직접 속성을 갖지 않게 됨을 알 수 있게 되었다. OOP 관점에서 보면 클래스의 메소드라면 공통적인 것이므로 prototype으로 선언하면 객체가 생성될 때 마다 객체의 속성으로써 갖지 않아도 되어 메모리 낭비가 되지 않을 것다. 과연 정말일까? 실제로 그렇다

 

아래 실험은 sayHello 라는 메소드를 객체 속성으로 가질 때와 prototype 속성으로 가질 때, 인스턴스화 시키면 heap memory가 어느 쪽이 더 많은지를 비교해 보는 것이다.

 

결과, 실험2를 보면 heapUsed가 대체적으로 더 낮음을 알 수 있고 인스턴스화 되는 객체가 공통적인 속성을 갖게 된다면 prototype 링크로 연결하는 것이 더 나은 선택일 것 같다. 참고로, heap은 객체가 생생되는 메모리 공간이다. 그리고 아래 주석에 출력된 내용은 process.memoryUsage()로 얻은 것이다.

실험1. 객체가 sayHello 메소드를 속성으로 직접 갖는 경우

function Person1(name) {
    this.name = name;
    this.sayHello = function() {
        console.log('Hi i am ' + this.name);
    }
}

var arr1 = [];
// A: {rss: 25821184, heapTotal: 8839168, heapUsed: 3421128, external: 1154060, arrayBuffers: 9402}
// B: {rss: 24993792, heapTotal: 8839168, heapUsed: 2871288, external: 1154060, arrayBuffers: 9402}
// C: {rss: 25251840, heapTotal: 9105408, heapUsed: 3224888, external: 1154060, arrayBuffers: 9402}

for (var i = 0; i < 10000; i++) {
    arr1[i] = new Person1('Name'+i);
}

console.log(arr1);
// A: {rss: 29077504, heapTotal: 9416704, heapUsed: 5798680, external: 1154100, arrayBuffers: 9402}
// B: {rss: 28774400, heapTotal: 9678848, heapUsed: 6422104, external: 1154100, arrayBuffers: 9402}
// C: {rss: 27832320, heapTotal: 9416704, heapUsed: 5318024, external: 1154100, arrayBuffers: 9402}

실험2. 객체의 prototype 링크에 sayHello 메소드 추가한 경우

function Person1(name) {
    this.name = name;
}
Person1.prototype.sayHello = function() {
    console.log('Hi i am ' + this.name);
}

var arr1 = [];
// A: {rss: 25067520, heapTotal: 9101312, heapUsed: 2859888, external: 1154060, arrayBuffers: 9402}
// B: {rss: 24920064, heapTotal: 8577024, heapUsed: 2859992, external: 1154060, arrayBuffers: 9402}
// C: {rss: 25153536, heapTotal: 8839168, heapUsed: 2860032, external: 1154060, arrayBuffers: 9402}

for (var i = 0; i < 10000; i++) {
    arr1[i] = new Person1('Name'+i);
}

console.log(arr1);
// A: {rss: 25952256, heapTotal: 9367552, heapUsed: 4526056, external: 1154100, arrayBuffers: 9402}
// B: {rss: 25370624, heapTotal: 8843264, heapUsed: 4520576, external: 1154100, arrayBuffers: 9402}
// C: {rss: 25780224, heapTotal: 9105408, heapUsed: 4520728, external: 1154100, arrayBuffers: 9402}

 

Prototype을 어디에 사용할 수 있을까?

1. 기능 확장

JavaScript는 대부분이 객체이다. 그래서 쉽게는 Prototype을 통해 객체를 확장하는데 사용할 수 있다. 구체적으로, Array, String, Object 그리고 Function 등으로 부터 생성되는 객체를 커스텀하게 사용하기 위해 Prototype을 사용할 수 있다. 예를 들어, 아래와 같이 Array 를 확장해서 hasEven() 이라는 것을 만들어 낼 수 있다. 서드 파티 라이브러리를 확장해서 사용하고 싶을 때도 이와 비슷하게 할 수 있을 것 같다.

var arr1 = [1,2,3,4,5];
var arr2 = [2, 4, 8];
var arr3 = [1, 3, 9];

// console.log(arr1.hasEven()); // Uncaught TypeError: arr1.hasEven is not a function

Array.prototype.hasEven = function() {
    for (var i = 0; i < this.length; i++) {
        console.log(this[i]);
        if (this[i] % 2 === 0) {
            return true;
        }
    }
    return false;
}

console.log(arr1.hasEven()); // true
console.log(arr2.hasEven()); // true
console.log(arr3.hasEven()); // false

2. class 만들기

class란 객체를 만들기 위한 틀이다. 쉽게 생각하면 붕어빵을 만들기 위해 붕어빵 틀이 class이고, 객체란 팥, 밀가루 등을 통해 고온에서 구체적으로 만들어진 붕어빵이라 볼 수 있겠다. 이런 식으로 객체를 만들기 위한 class를 prototype을 이용해 만들 수 있다.

 

static 메소드는 Person이라는 클래스의 속성이지 p1이라는 객체의 속성이랑 관련 없어서 prototype으로 연결하지 않는다.

 

var Person = (function () {
    // 생성자 method
    function Person(name) {
        // 속성
        this.name = name;
    }

    // static method
    Person.className = function () {
        return 'Person';
    };
    
    // 일반 method
    Person.prototype.sayHello = function () {
        console.log("Hello, I am " + this.name);
    };
    return Person;
}());

console.log(Person.className()); // Person

var p1 = new Person('Laura', 24);
p1.sayHello(); // Hello, I am Laura
console.log(p1.name); // Laura

결론

JavaScript를 잘 이해하기 위해서는 JavaScript의 객체를 잘 이해하는 것이 중요하다. 그런데 모든 객체는 prototype과 관련이 있어서 prototype이 무엇인지, 어떻게 동작하는지, 어디에 사용되는지 등을 잘 이해한다면 더 나은 JavaScript 프로그래밍을 할 수 있을 것이라 생각한다.

 

JavaScript에서 함수를 선언하게 되면, 모든 함수는 prototype 라는 객체 속성을 갖게 되고, 생성자 함수를 통해 객체를 생성하게 되면 그 객체는 생성자 함수로부터 prototype 링크가 연결될 수 있다.

 

JavaScript 객체에 속성이 없는 경우 prototype 링크(__proto__)를 타고 올라가서, 그 객체에서 속성을 다시 검색한다. 또 발견되지 않으면 계속 타고 거슬러 올라가 Object.prototype 까지 타고 올라가게 된다.

 

JavaScript에서 객체의 기능, 서드 파티 라이브러리를 확장, 수정하기 위해 prototype을 사용할 수 있다. 그리고 ES6+ 문법을 지원하지 않는 경우 class가 필요할 때 prototype으로 메소드를 구현할 수 있다.

 

JavaScript에서 여러 객체가 인스턴스화 될 때 공통적인 속성은 객체의 속성이 직접 갖는 것 보다 prototype의 속성으로 갖게 하는 것이 메모리 적으로 덜 낭비하게 된다.

반응형