JavaScript Object(1) - Prototype

8분

8/22/2021

Thumbnail

들어가면서

자바스크립트에서는 배열과같은 기능부터 JavaScript 위에 구축된 브라우저 APIs에 이르기까지 대부분의 것들이 객체입니다.

사용자는 관련된 함수들과 변수들을 효율적인 패키지로 추상화하거나 편리한 데이터 컨테이너로 작동하는 객체를 만들 수 있습니다.

언어에 대한 지식을 가지고 더 멀리 나아고자 한다면 자바스크립트의 객체 기반의 본질을 이해하는 것이 중요합니다.

- Javascript 객체 소개_mdn

자바스크립트는 객체 지향을 가진 언어입니다. 그러나 OOP 언어들(JAVA, C#, C++, Python 등)과는 사뭇 다르게 느껴집니다.

그 이유에는 자바스크립트 프로토타입이 있습니다. 이것을 포함해서 자바스크립트의 객체에 대해 좀 더 깊게 알아보려고 합니다.

최근에 개발을 진행중에 상당히 많은 시간을 뺏겼던 부분이 이 부분과 관련이 있어서 이해를 더 깊게 하고자 작성 해봅니다.

먼저 자바스크립트에서 객체지향 개념을 어떤식으로 사용했는지 알아봅시다.

OOJS - Object-oriented Javascript

OOP의 기본 컨셉은 프로그램 내에서 표현하고자 하는 실 세계(real world)의 일들을 객체를 사용해서 모델링 하고, 객체를 사용하지 않으면 불가능 혹은 무지 어려웠을 일들을 쉽게 처리하는 방법을 제공한다는 것입니다.

객체지향 프로그래밍을 공부하면 무수하게 많이 보거나 듣게되는 문장입니다.

지금이야 당연한 거 아니야? 라고 생각할 수 있지만 언어라는 관점에는 큰 전환점이었습니다.

자바스크립트도 마찬가지였습니다.

자바스크립트의 객체 지향 설계는 셀프 라는 언어에 영향을 받았습니다. 그리고 1995년 12월, 프로토타입 형태의 객체 지향 언어인 자바스크립트가 만들어졌습니다.

셀프는 프로토타입 개념 기반의 객체 지향 프로그래밍 언어로 1980년대 1990년대에 언어 디자인을 위한 실험적인 테스트 시스템으로 처음 사용되었습니다.

자바스크립트 Class는 ES6(ECMA2015)가 되서야 Class syntax가 도입이 되었습니다. 그러나 사실 내부적으로는 프로토타입 기반의 객체를 사용합니다.

ES6 Class는 ES5의 Object 생성자 함수의 syntactic sugar입니다.

syntactics sugar: 프로그래밍 언어 내에서 더 쉽게 읽거나 더 쉽게 표현할 수 있게 만들어진 해당 언어 내에 syntax를 뜻합니다.

- Brad Traversy

자바스크립트에서 프로토타입 기반의 객체는 무엇을 뜻하는 걸까요?

Prototype Object

자바스크립트 모든 객체들이 메소트와 속성을 상속 받기 위한 템플릿으로써 프로토타입 객체를 가진다는 의미입니다.

프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속 받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다.

이를 프로토타입 체인이라 부르며 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다.

_- 프로토타입 기반 언어? - MDN*

자바스크립트는 모든 객체들이 프로토타입이라는 속성을 가집니다. 그리고 상속되는 속성과 메소드들은 각 객체가 아니라 객체의 생성자의 prototype이라는 속성에 정의되어 있습니다.

객체 인스턴스와 프로토타입 간에 연결이 구성되며 이 연결을 따라 프로토타입 체인을 타고 올라가며 속성과 메소드를 탐색합니다.

  1. 객체의 prototype(Object.getPrototypeOf(obj))
  2. prototype 속성

이 2개의 차이점을 인지하는 것이 중요합니다.

1.은 개별 객체의 속성입니다. 2.은 생성자의 속성입니다.

다시 말해, Object.getPrototypeOf(new Foobar())Foobar.prototype은 동일한 객체라는 의미입니다.

점점 헷갈리기 시작하는데... 자세히 알기 위해 예제를 통해서 이해해보겠습니다.

다음과 같은 Person()생성자를 작성했다고 봅시다.

function Person(first, last, age, gender, interests) {
  // 속성 정의
  this.first = first;
  this.last = last;
  this.age = age;
  this.gender = gender;
  this.interests = interests;

  // 메소드 정의
  this.bio = function () {
    alert(this.first + ' ' + this.last + ' ' + this.age);
  };
  this.greeting = function () {
    alert("Hi! I'm " + this.name.first + '.');
  };
}

인스턴스도 하나 만들어 봅시다.

var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);

콘솔에서 person1.을 치게 되면 브라우저는 아래처럼 해당 객체의 멤버 이름을 자동 완성 팝업으로 보여줍니다.

prototype-2.png

저희가 person1의 프로토타입 객체인 Person()에 정의된 멤버들이 보입니다. (first, last, age ...)

뿐 만 아니라 valueOf, toString과 같이 Person()의 프로토타입 객체인 Object에 정의된 다른 멤버들도 보입니다.

이렇게 상속된 값들도 다 같이 보인다는 것은 프로토타입 체인이 동작하는 증거입니다.

prototype-2

실제로는 Object에 정의되어있는 valueOf 메소드를 person1에서 호출하면 어떻게 될까요?

prototype-3.png

이 메소드는 호출된 객체(여기서는 person1)의 값을 단순 반환합니다. 프로토타입 체인 관점에서 보자면 다음 흐름과 같습니다.

  1. 브라우저는 우선 person1객체에 valueOf() 메소드를 체크합니다.
  2. 없기 때문에 person1의 프로토타입 객체 인(Person() 생성자의 프로토타입)에 valueOf() 메소드를 체크합니다.
  3. 여전히 없기 대문에 Person() 생성자의 프로토타입 객체의 프로토타입 객체(Object() 생성자의 프로토타입)가 valueOf() 메소드를 가지고 있는 체크합니다. 여기에 있으니 호출하면서 끝납니다.

여기서 중요한 점은 프로토타입 체인에서 한 객체의 메소드와 속성들이 다른 객체로 복사되는 것이 아닌 프로토타입 체인을 통해 타고 올라가며 접근할 뿐입니다.

Object.prototype.valueOf()

valueOf() 메소드는 특정 객체의 원시 값을 반환합니다. (근데 .prototype은 뭐지...? 더 자세한 내용은 아래에서 알아보도록 하겠습니다.)

그런데 실제로 person1.valueOf() 처럼 상속받은 멤버들은 실제로 어디에 정의되어 있을까요?

Object.prototype

Object 에는 수 많은 속성과 메소드들이 있습니다.

그러나 실제로 상속받은 멤버들은 몇 개 되지 않습니다. 일부는 상속 되었지만 나머지는 아닙니다. 왜그럴까요?

실제로 상속 받는 멤버들은 prototype 속성에 정의되어 있습니다.

Object. 가 아닌 Object.prototype.로 접근 되는 것입니다.

prototype 속성도 하나의 객체이며 프로토타입 체인을 통해 상속되는 속성과 메소드를 담아두는 버킷으로 주로 사용되는 객체입니다.

Object.is(), Object.keys()... 등 prototype 버킷에 정의되지 않은 멤버들은 상속되지 않습니다. object-prototype.png ex-prototype.png

전역 객체인 String, Date, Number, Array의 프로토타입도 확인해보는 것도 재밌을 것 같아요.

String 객체 prototype string-prototype.png 우리가 string 변수를 선언하고 사용할 때 replacesplit을 쓸 수 있는 이유가 여기에 있었네요!

다시 한번 정리한다면,

  • prototype 속성에 정의되어 있는 멤버는 상속되어집니다.
  • 인스턴스가 멤버들을 사용하는 방식은 본인 객체와 프로토타입 체인에 의해서 부모 객체를 타고 올라가면서 찾아오는 방식입니다.
  • 무엇보다 직접 console에 뚜드리면서 이해하는게 가장 좋습니다.

prototype 속성은 Javascript에서 가장 헷갈리는 명칭중 하나입니다.

보통 this가 현재 객체의 프로토타입 객체를 가리킬 것이라 오해하지만 그렇지 않습니다. (프로토타입 객체는 __proto__ 속성으로 접근 가능한 내장 객체입니다.)

thisprototype 속성을 가리킵니다.

그리고 prototype 속성은 상속 시키려는 멤버들이 정의된 객체를 가리킵니다. this-prototype.png 여기 thisPersonprototype 속성을 가리키고 그래서 프로토타입에 속성과 메소들을 정의할 수 있습니다.

뭔가 알아가는 느낌이 들면서도 헷갈리는 것 같습니다.

다른 언어와 이질감이 느껴지는 부분이 있긴 합니다.

함수를 만들었지만 객체가 되는 것 처럼 말이죠.

자바스크립트에서 함수?

함수 역시 객체의 하나입니다.

못 미더우시면 Function() 생성자 레퍼런스 페이지를 확인해보세요.

_- 프로토타입 속성_mdn*

이번에 자바스크립트 객체를 공부하면서 아직 너무 많은 부분을 모르고 있었다는 걸 새삼 깨닫게 되는 것 같아요.

공부를 하면 할 수록 더 모르는게 생겨나는 느낌이랄까...

무튼, 자바스크립트에서 함수 또한 객체의 하나일 뿐이라는 것이죠.

  • 자바스크립트에서 거의 모든 것은 객체입니다. - null, undefined를 제외한 모든 것은 객체입니다.
  • 그리고 모든 객체들은 프로토타입 객체를 가집니다.

이 생각을 가진다면 훨씬 나은 이해가 될 수 있을 것 같아요.

객체 인스턴스

  • var foo1 = new Foo('bar')
  • var foo2 = { name: 'bar' }
  • var foo3 = new Object(); foo3.name = 'bar';
  • var foo4 = Object.create(foo1);

자바스크립트에서는 객체 인스턴스를 만드는 방법은 꽤나 많습니다.

다음은 객체 인스턴스를 만드는 방법 4가지를 설명합니다.

  1. 생성자 함수
  2. Object() 생성자
  3. 객체 리터럴
  4. Object.create()

1. 생성자 함수

생성자 함수는 클래스의 자바스크립트 버전입니다.

자바스크립트는 객체와 그 기능을 정의하기 위해 생성자 함수라고 불리는 특별한 함수를 사용할 수 있습니다.

function Person(name) {
  this.name = name;
  this.greeting = function () {
    alert("Hi! I'm " + this.name + '.');
  };
}

생성자 함수는 단순히 속성과 메소드를 정의합니다. 그리고 이를 위해 this를 사용하고 있는 것을 볼 수 있습니다.

이것은 객체 인스턴스가 만들어질 때마다 객체의 name 속성이 생성자 함수 호출에서 전달된 name 값과 같아질 것이라고 말하고 있습니다.

var person1 = new Person('Bob');
person1.greeting();

// alert('Hi! I\'m Bob')

new 키워드가 브라우저에게 우리가 새로운 객체 인스턴스를 만들고 싶어한다는 것을 알려줍니다.

사실 위 처럼 메소드를 직접 정의해놓으면 생성자 함수를 호출할 때마다 다시 정의하게 되어 최선의 방법은 아닙니다.

해결책은 prototype에 함수를 정의해놓고 사용하는 방법이 있습니다.

프로토타입 수정하기

사실 일반적인 방식으로는 속성은 생성자 함수에서, 메소드는 프로토타입에서 정의합니다.

생성자 함수에는 속성에 대한 정의만 있으며 메소드는 별도의 블럭으로 구분할 수 있으니 코드를 읽기가 훨씬 쉬워집니다.

위에서 greeting() 을 생성자 함수 안에 메소드를 선언했지만, 이를 프로토타입에 정의하는 방식으로 변경해보겠습니다.

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

var person1 = new Person('Bob');

Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name + '.');
};

person1.greeting();

나중에 선언했지만 person1에서 바로 greeting() 메소드를 사용할 수 있습니다.

실제로 프로토타입 객체는 모든 인스턴스에서 공유되는 개념이기 때문에 즉시 별도의 갱신 과정 없이 접근이 가능합니다.

프로토타입에 속성을 정의할 수는 있으나 그렇게 하지 않는 이유는 namespace 가 전역 범위를 가리키므로 코드가 의도대로 동작되지 않을 수 있기 때문입니다.

Person.prototype.fullName = this.name.first + ' ' + this.name.last;

// 여기서 this는 전역을 가리키므로 undefined undefined으로 나타날 것입니다.

2. Object() 생성자

최초의 object 역시 생성자를 가지고 있습니다.

var person1 = new Object();
person1.name = 'Bob';
person1.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');

점 표기법이나 괄호 표기법을 이용해 속성과 메소드를 추가할 수 있습니다.

객체 인스턴스를 일회성으로 만들어서 바로 사용하기 때문에 재사용에 있어서 차이가 있습니다.

3. 객체 리터럴

객체 리터럴 구문은 더 간결하며 new Object() 와 성능 차이는 없습니다.

아마 개발하면서 가장 많이 사용하는 syntax 중 하나라고 생각됩니다.

Object() 생성자와 마찬가지로 일회성으로 사용하기 때문에 재사용에 있어서 차이가 있습니다.

var person1 = {
  name: 'Bob',
  greeting: function () {
    alert("Hi! I'm " + this.name + '.');
  },
};

4. Object.create()

하지만 몇몇 사람들은 객체 인스턴스들을 생성할 때 먼저 생성자를 만들기를 원하지 않습니다.

위에서 살펴봤던 재사용성이 없는 객체 리터럴, new Object() 같은 것들도 재사용성을 갖게 할 수 있을까요?

Object 객체에는 create() 메소드가 있습니다. 이미 존재하는 객체 인스턴스를 이용하여 새로운 인스턴스를 만들 수 있는 메소드입니다.

위 예시에서 만들었던 person1을 이용하여 person2를 만들어보겠습니다.

var person2 = Object.create(person1);

create() 메소드가 실제로 하는 일은 주어진 객체를 프로토타입 객체로 삼아서 새로운 객체를 만드는 것입니다.

위 예시는 person2가 person1을 프로토타입 객체로 삼습니다. create-prototype.png

객체 모습이 매우 비슷하지만 살짝 다른 객체 인스턴스를 만들 때 좋지 않을까 생각이 들기도 합니다.

그러면 Class 상속이랑 느낌이 비슷한 것 같다는 생각도 듭니다.

프로토타입에서는 체인을 통해 상속과 비슷한 로직이 이루어진다고 본 것 같은데, 그건 그거고 상속은 또 따로 있는 것일까요?

실제로 Prototype에서의 상속은 이와 어떤 차이가 있으며 어떻게 이루어지는 걸까요?

프로토타입 상속

프로토타입 체인, 체인을 통해 멤버들을 탐색하는 것도 보았죠.

하지만 이는 대부분 브라우저가 알아서 처리하는 로직이었습니다.

그러면 우리가 직접 객체를 생성하고 상속하려면 어떻게 해야 할까요? - 프로토타입 상속_mdn

예시를 보면서 이해해보도록 합시다.

function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last,
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
}

Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name.first + '.');
};

Person.prototype.farewell = function () {
  alert(this.name.first + ' has left the building. Bye for now!');
};

속성은 생성자 함수에 메소드는 prototype에 정의되어 있습니다.

Person 객체를 상속받고 몇 가지를 추가한 Teacher 객체를 만들어보겠습니다.

  • subject 속성 추가
  • greeting() 메소드 변경: 좀 더 공손하게

Function.prototype.call()

call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.

- Function.prototype.call()_mdn

Teacher 객체에서 Person 생성자를 호출하기 위해 사용하는 메소드가 Function 프로토타입에 정의된 call() 메소드입니다.

function Teacher(first, last, age, gender, interests, subject) {
  Person.call(this, first, last, age, gender, interests);

  this.subject = subject;
}

call() 함수의 첫 번째 인자는 다른 곳에서 정의된 함수를 현재 컨텍스트에서 실행할 수 있도록 합니다.(?)

다시 말해, Person 또한 생성자 함수이므로 생성자를 Teacher(= this) 컨텍스트에서 실행하게 된다는 뜻입니다.(첫 번째 인자를 전달하지 않으면, this 의 값은 전역 객체에 바인딩 됩니다.)

엄격 모드(strict mode)에서는 스코프가 함수 컨텍스트 내로 한정되기 때문에 첫 번째 인자를 전달하지 않으면 this는 undefined를 가지게 됩니다.

strict-call.png

그리고 call() 함수의 나머지 인자로 Person에 필요한 인자를 전달합니다.

이렇게 하면 속성에 관해서는 상속받기가 끝이 났습니다.

그러나 greeting() farewell() 과 같이 prototype에 정의된 메소드들은 어떻게 상속받아야 할까요?

prototype에 정의된 메소드도 상속받기

현재 위 상태에서 PersonTeacher 프로토타입을 확인해본다면 method는 상속받은 상태가 아닙니다.

inherit-prototype.png

Teacher가 메소드도 상속받으려면 어떻게 해야 할까요?

  • Object.create() 사용
  • 만들어진 prototype.constructor값을 Person에서 Teacher로 수정
Teacher.prototype = Object.create(Person.prototype);

앞에서 적었듯이 prototype도 객체 이므로 Object.create를 통해 객체 인스턴스를 만들 수 있습니다.

그런데 모든 값이 Person.prototype 객체 인스턴스 값으로 되어있기 때문에 추후 문제 소지가 있는 부분은 수정해야 합니다.

Teacher.prototype.constructor = Teacher;

이렇게 되면 우리가 의도한대로 모든 멤버들에 대해 상속이 완료되었습니다.

상속한 메소드 수정하기

Teacher() 에 새로운 greeting() 함수를 정의하도록 합시다. (정확히는 상속된 메소드를 overriding 하는 것입니다.)

Teacher.prototype.greeting = function () {
  alert('Hello. My name is ' + this.name.last + ', and I teach ' + this.subject + '.');
};

여러 방법이 있겠지만 가장 간단한 방법으로 프로토타입을 통해 overriding하는 방법입니다.

다음엔

ES6(ECMA2015)에서는 Class syntax가 도입이 되었고 객체를 조금 더 쉽고 명확하게 재활용할 수 있게 되었습니다.

대부분 최신 브라우저에서는 새로운 Class syntax를 지원합니다만 일부 구형 브라우저(익스플로러...)에서는 동작하지 않으므로 하위호환성을 위해 프로토타입에 대해 알 필요가 있었습니다.

다음 포스트에서는 자바스크립트에서 Class syntax가 어떤식으로 사용되는지, 그리고 이번 포스트에서 다루지 못한 Object의 여러가지 메소드들을 살펴보도록 하겠습니다.

Reference

마지막 업데이트

8/22/2021


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.