[JavaScript] 생성자함수, Prototype기반 객체지향

Hoplin·2023년 1월 9일
0

생성자 함수

객체 리터럴의 문제점

자바스크립트의 객체 리터럴 형식의 객체 생성은 직관적이고, 간편하다. 프로퍼티, 메소드(객체에서 함수형태의 프로퍼티 의미)를 한눈에 알아볼 수 있다.

const obj = {
    // Property
	radius : 5,
  	// Method
  	getDiameter(){
      return 2 * this.radius
    }
}

객체 리터럴은 하나의 객체만 생성하게된다. 만약 동일한 프로퍼티들을 가진 객체를 또 생성하고 싶다면 객체 리터럴로 객체를 생성해 주어야한다.

const obj = {
    // Property
	radius : 5,
  	// Method
  	getDiameter(){
      return 2 * this.radius
    }
}

const obj2 = {
    radius : 10,
    getDiameter(){
        return 2 * this.radius
    }
}

객체는 프로퍼티를 통해 각각의 고유 상태를 저장한다. obj와 obj2는 각각의 프로퍼티를 가지지만 getDiameter라는 메소드는 동일한 기능을 하는 어떻게 보면 중복코드가 된다. 객체 리터럴로 위와 같이 일일히 만들어 줄 수 있지만, 만약 이 객체 리터럴을 백개 이상 만든다고 가정하면, 골치아프다.

생성자 함수를 사용해보자

생성자 함수 라고 한다면, Java, C++와 같이 객체지향 언어에서, 인스턴스를 만들때 인스턴스 변수 및 설정을 초기화 후 인스턴스 참조 주소를 반환하는 역할을 한다. JS에서 생성자 함수도 비슷한 느낌의 의미를 가진다. 생성자 함수를 이용하면, 하나의 생성자 함수로, 여러 객체를 생성할 수 있다.

function Circle(radius){
    this.radius = radius;
    this.getDiameter = function(){
        return 2 * this.radius;
    }
}

const c1 = new Circle(10);
const c2 = new Circle(20);

console.log(c1.getDiameter());
console.log(c2.getDiameter());

this를 잠시 짚고 넘어가자면, this는 함수 호출 방식에 따라 가리키는 값이 다를 수 있다(이를 this binding 이라고 한다).

함수 호출 방식this 바인딩
일반 함수로서 호출전역객체(Browser : Window Node.js : global)
메소드로서 호출메소드를 호출한 객체
생성자 함수로서 호출생성자가 생성할 인스턴스

생성자 함수 또한 객체를 생성하는 함수이다. JS에서 생성자 함수는, 일반적인 함수를 정의하듯 함수를 정의하고 new 연산자와 함께 호출하면 해당 함수가 생성자 함수로 작동한다. 인스턴스가 반환되는 과정은 아래와 같다.

  1. 인스턴스 생성과 this 바인딩
    인스턴스가 될 빈 객체가 생성된다. 그리고 이 인스턴스는 this에 바인딩 된다. 이 처리는 Runtime 이전에 실행된다.

  2. 인스턴스 초기화
    생성자 함수에 기술된 코드가 한줄씩 실행되어 this에 바인딩 되어있는 인스턴스를 초기화 한다. 즉, this에 바인딩 되어있는 인스턴스에 프로퍼티, 메소드를 추가하고 생성자 함수가 전달받은 매개변수를 통해 인스턴스 프로퍼티에 할당해 초기화를 진행한다.

  3. 인스턴스 반환
    위 과정이 끝났다면 인스턴스가 바인딩된 this가 암묵적으로 반환된다. 단, 생성자 함수가 다른 객체를 반환하는 코드가 있다면, this가 정상적으로 반환되지 않는다(단 원시값인 경우 this 반환).

[[Call]]과 [[Construct]]

함수 또한 객체이다. 그렇기에 객체가 가진 내부 슬롯, 메소드 모두 동일하게 가지고 있다. 다만 일반 객체와 함수 객체는 차이점이 있다. 함수객체는 호출이 가능하다라는 점에서 차이점이 있다. 그렇기에, 함수객체만을 위한 [[Environment]], [[FormalParameters]]와 같은 내부슬롯과 [[Call]],[[Construct]]와 같은 내부 메소드를 추가로 가지고 있다.

함수 객체를 일반 함수로 호출하면, [[Call]]이 호출되고, new 연산자로 호출하면 [[Construct]] 가 호출된다. [[Call]]을 갖는 함수 객체를 callable이라고 하며, 내부 메소드 [[Construct]]를 갖는 함수 객체를 constructor, 갖지 않는 함수를 non-constructor 이라고 한다. 당연한 이야기지만, non-constructor 객체는 생성자 함수로 호출할 수 없다(new 연산자와 함께 사용이 불가능하다는 뜻이다). 함수에도 여러 형태가 있는데, constructor, non-constructor의 종류는 아래와 같다

  • constructor : 함수 선언문, 함수 표현식, 클래스
  • non-constructor : 축약표현 형식의 메소드(ES6+), 화살표 함수

일급객체

자바스크립트에서 함수는 일급 객체이다.일급 객체는 아래 조건들을 만족해야한다

  • 무명의 리터럴로 생성할 수 있다. 런타임에 생성이 가능하다
  • 변수나 자료구조에 저장할 수 있다
  • 함수의 매개변수에 전달할 수 있다.
  • 함수의 반환값으로 사용할 수 있다.
function test(){
    return 2 + 2;
}

function functionTest(fn){
    return function(number) {
        return fn() + number;
    }
}

const number = functionTest(test);
console.log(number(10));

프로토타입과 객체지향

자바스크립트의 패러다임

자바스크립트는 여러 패러다임을 가진다.

  • 명령형
  • 함수형
  • Prototype기반
  • 객체지향 (단 일반적인 객체지향에 있는 접근제어자는 존재하지 않는다)

자바스크립트는 프로토타입 기반 객체지향언어이다. 자바스크립트 자체적인 문법으로 클래스가 존재하지만, 이 또한 프로토타입 패턴 기반으로 동작하게 된다.

프로토타입을 이용한 상속

상속이란 어떤 객체의 프로퍼티 혹은 메소드를 다른 객체가 상속받아 동일하게 사용하는 개념을 의미한다. 하나의 생성자 함수를 만들고, 인스턴스 두개를 생성해보자.

function Circle(radius){
    this.radius = radius;
    this.getDiameter = function(){
        return 2 * this.radius;
    }
}

const c1 = new Circle(10);
const c2 = new Circle(20);

console.log(c1.getDiameter === c2.getDiameter); // false

위 코드같은 경우, 각각의 인스턴스가 radius라는 프로퍼티와 getDiameter라는 메소드를 가진다. 그리고 위 코드의 다이어그램을 그리면 아래와 같다.

getDiameter같은 경우, 모든 함수들이 동일하게 가지는 함수이다. 하지만 인스턴스마다 가지고 있으니, 이는 메모리 낭비의 요소가 될 수 있다. 이런 경우 프로토타입 기반의 상속을 구현하여 방지할 수 있다.

function Circle(radius){
    this.radius = radius;
}

Circle.prototype.getDiameter = function(){
    return 2 * this.radius;
}

const c1 = new Circle(10);
const c2 = new Circle(20);

console.log(c1.getDiameter === c2.getDiameter); // true

위 코드와 같이 구현하면, 아래와 같은 다이어그램으로 변경된다.

Circle 생성자 함수가 생성한 모든 인스턴스는 상위 객체를 하는 Circle의 Circle.prototype의 모든 프로퍼티와 메소드를 상속받는다. 이와 같이 하면, getDiameter메소드는 하나만 생성된다. 각 인스턴스는 독립적인 상태값을 가져야하는 radius만 개별적으로 가지고 있으면 되기에, 메모리 적으로 더 효율적이다.

프로토타입 객체, constructor 프로퍼티

프로토타입은 특정 객체의 상위 객체 역할을 하는 객체로서, 다른 객체에 공유 메소드, 프로퍼티를 제공한다. 프로퍼티를 상속받은 하위 객체는 상위 객체의 프로퍼티, 메소드를 마음대로 사용할 수 있다. 모든 객체는 [[prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조이다. 객체가 생성될때 객체 생성 방식에 따라 프로토타입이 결정, [[Prototype]]에 저장된다. 내부슬롯은, 자바스크립트의 내부 원리이므로 접근이 불가능하지만 __proto__라는 접근자 프로퍼티를 사용하여 내부슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다.

__proto__접근자 프로퍼티를 통해 자신의 [[Prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있게되고, constructor프로퍼티를 통해 생성자 함수에 접근, 생성자 함수는 자신의 prototype프로퍼티를 통해 프로토타입에 접근하게 되는것이다.

constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 한가지 더 알아야할 사실은 __proto__ 접근자 프로퍼티는 Object.property의 프로퍼티라는 것이다. 그리고 __proto__를 코드 내에서 직접 사용하는것은 권장되지 않는다. 아래 사진을 보면, __proto__는 Object의 프로토타입인것을 알 수 있다.

프로토타입 체인

Object는 모든 객체의 최상위 객체이다

크롬 브라우저에서 Object객체의 프로토타입을 출력해보면 아래와 같이 나오는것을 알 수 있다.

그리고 새로운 생성자 함수를 만들고, 프로토타입에 메소드를 추가한 뒤 객체를 생성해 보자. 그 다음에 hasOwnProperty를 통해 객체에 name이라는 프로퍼티가 있는지 확인해보자

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

Person.prototype.introduce = function(){
    console.log(`My name is ${this.name}`)
}

const me = new Person("Hoplin");
console.log(me.hasOwnProperty('name'));

위 사진에서 봤듯이 hasOwnProperty()는 Object의 메소드이다. 어떻게 사용할 수 있는것일까? 우리가 주목해야할것은 인스턴스 객체인 me의 관점이다

객체 me의 데이터를 console.dir로 살펴보자. me 객체의 프로토타입은 Person.prototype 다음 Object.prototype을 참조하고있는것을 볼 수 있다. 자바스크립트는 결국 Person.prototype에 존재하지 않기때문에 Object.prototype에 있는 메소드를 호출하는것이다.

위의 개념과 별개지만, 생성자함수 Person의 관점을 살펴보자. 물론 이 관점에서는 Person이 일반함수로 사용되었을때에 해당한다. Person의 프로토타입은 Function의 프로토타입을 가리키고, Function의 프로토타입Object의 프로토타입를 가리키는것을 볼 수 있다. 이를 통해 프로토타입 계층은 Person - Function - Object로 되어있는것을 확인할 수 있다. 결론적

한가지 참고할 점은, Object.prototype.__proto__는 null이다.(프로토타입 체인의 끝이기 때문)

결론적으로 이러한 것을 프로토타입 체인이라고 하며, 자바스크립트가 객체지향을 구현하는 매커니즘이다. 그리고 위 예제를 다이어그램으로 나타내면 아래와 같이 나타낼 수 있다.

오버라이딩과 프로퍼티 섀도잉

객체지향에서 오버라이딩이란 상위 클래스의 크래스를 하위클래스에서 재정의하는것을 의미한다. 아래와 같은 코드가 있다고 가정하자.

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

Person.prototype.introduce = function(){
    console.log(`My name is ${this.name}`)
}

const me = new Person("Hoplin");
me.introduce = function(){
    console.log(`Overrided Function`)
}

me.introduce();

실행을 해보면, "Overrided Function"이 출력되는것을 볼 수 있다. 이유는 자바스크립트는 객체의 프로퍼티, 메소드를 시작으로 프로토타입을 검색하게 되는데, 생성자 함수의 프로토타입을 검색하기 전에 introduce 메소드가 발견되어 프로토타입의 introduce를 사용하지 않게 되는것이다. 이와 같이 상속관계에 의해 프로퍼티 혹은 메소드가 가려지는것을 프로퍼티 섀도잉이라고 부른다

정적 프로퍼티와 메소드

흔히 자바 및 C++에서 클래스 메소드 혹은 클래스 변수라고 하는것들이 있다. 이들의 공통점은, 인스턴스 선언 없이 클래스를 통해 바로 접근이 가능하다는것이다. 자바스크립트에서도 기본적으로 생성자 함수로 인스턴스를 생성하지 않는다면, 프로토타입 체인을 사용할 수 없다(생성자 함수.prototype을 통해 접근은 가능하다). 생성자 함수 또한 객체이므로, 자신만의 프로퍼티 메소드를 소유할 수 있으며, 이는 인스턴스를 생성하는것과 별개로 사용할 수 있다. 그리고 이를 정적 프로퍼티/메소드라고 부른다.

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

Person.prototype.introduce = function(){
    console.log(`My name is ${this.name}`)
}

Person.staticProperty = "Static Property";
Person.staticMethod = function(){
    console.log("Static method")
}

Person.staticMethod();
console.log(Person.staticProperty);
// Person.introduce() // TypeError: Person.introduce is not a function
profile
더 나은 내일을 위해 오늘의 중괄호를 엽니다

0개의 댓글