해당 포스팅은 위키북스의 모던 자바스크립트 Deep Dive라는 책을 독학하며 기록하는 글입니다.

자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효울적이며 더 강력한 객체지향 프로그래밍 능력을 지니고 있는 프로토타입 기반의 객체지향 프로그래밍 언어다.

객체지향 프로그래밍은 프로그램을 여러 개의 독립적 단위, 즉 객체의 집합으로 표현하려는 프로그래밍 패러다임을 말한다. 여기서 객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라고 할 수 있는데 객체의 상태 데이터는 프로퍼티, 동작은 메서드라고 부른다.

상속

상속은 객체지향 프로그래밍의 핵심 개념으로 어떤 객체의 프로퍼티 또는메소드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말한다.

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거하는데 이게 무슨 말인지 다음을 살펴보자.

// (1) 생성자 함수내에 메서드가 있는 경우
function Circle(r) {
  this.radius = r;
  this.getArea = function() {
    return Math.PI * this.radius ** 2;
  }
}

// (2) 공통되는 메서드는 생성자 함수밖으로 빼서 프로토타입에 추가한 경우
function Circle(r) {
  this.radius = r;
}

Circle.prototype.getArea = function() {
  return Math.PI * this.radius ** 2;
}

(1)의 방법을 사용해서 생성자 함수를 만든 경우에는 Circle의 새로운 인스턴스를 만들 때 마다 각 인스턴스의 radius값과 getArea라는 메서드를 계속해서 만들어 낸다. 이때의 문제점은 각 인스턴스는 각자의 radius는 가지고 있어야 할 필요가 있지만 완전히 동일한 내용을 갖는 getArea메서드는 굳이 각자가 모두 가지고 있어 메모리낭비를 할 필요가 없다는 것이다.

따라서 (2)와 같은 방법으로 각 인스턴스가 개별적으로 가져야 하는 프로퍼티나 메서드들은 생성자 함수내에 만들고, 모든 인스턴스가 동일하게 공통적으로 가져야 하는 프로퍼티나 메서드들은 프로토타입에 추가해 불필요한 중복을 제거하는 것을 프로토타입을 기반으로 상속을 구현한다고 한다. 글로 이해가 어렵다면 아래 그림을 통해 이해해보자. 왼쪽이 (1)의 방법, 오른쪽이 (2)의 방법이다.

프로토타입 객체

위 그림의 오른쪽 방식에서 Circle.prototype을 프로토타입 객체라고 한다. 프로토타입 객체는 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용되며 어떤 객체의 상위 객체의 역할을 하는 객체로 다른 객체에 공유 프로퍼티나 메서드를 제공한다.

모든 객체는 하나의 프로토타입을 가지고 있으며, 모든 프로토타입은 생성자 함수와 연결되어 있다. 객체는 __proto__라는 접근자 프로퍼티를 통해 자신의 프로토타입 객체에 접근할 수 있다. 명료하게 다음 그림을 보자.

프로토타입 체인은 단방향 링크드 리스트로 구현되여야 한다. 또한 프로토타입 체인의 최상위 객체는 Object.prototype이며, 이 객체의 프로퍼티와 메소드는 모든 객체에 상속된다.

코드 내에서 __proto__를 직접 사용하는 것은 권장되지 않으며 어떠한 객체의 프로토타입객체를 참조하고 싶으면 Object.getPrototypeOf 메서드를 사용하고, 교체하거나 지정하고 싶으면 Object.setPrototypeOf 메서드를 사용하는 것을 권장한다. 두 메서드는 Object.prototype이라는 프로토타입 객체에 존재하며 모든 객체의 프로토타입 체인 최상위에는 Object.prototype이 있기 때문에 해당 메서드를 사용할 수 있는 것이다.

함수 객체만이 소유하는 prototype 프로퍼티는 해당 생성자 함수가 미래에 생성할 인스턴스의 프로토타입을 가리킨다. 따라서 constructor인 함수에만 해당 프로퍼티가 있으며 화살표 함수나 메서드 축약 표현과 같은 non-constructor함수에는 prototype 프로퍼티가 존재하지 않는다.

프로토타입 객체는 constructor라는 프로퍼티를 갖는데, 이 프로퍼티는 prototype이라는 프로퍼티를 통해 자신을 참조하고 있는 생성자 함수를 가리킨다. (위 그림에 표현되어 있다) 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재한다. 따라서 프로토타입은 생성자 함수가 만들어지는 시점에 같이 생성된다.

프로토타입 체인

자바스트립트 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는데 이를 프로토타입 체인이라고 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘이다.

프로토타입의 프로토타입은 언제나 Object.prototype이며, 프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype이다. 따라서 모든 객체는 Object.prototype을 상속받고 Object.prototype을 프로토타입 체인의 종점이라고 한다. 이때 Object.prototype의 프로토타입의 값, 즉 [[Prototype]] 내부 슬롯의 값은 null이다.

프로토타입 체인의 종점인 Object.prototype에서도 프로퍼티를 검색할 수 없는 경우 undefined를 반환할 뿐 오류가 발생하지 않는다는 점에 주의해야 한다.

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

이제 우리는 모든 인스턴스가 가져야 할 공통의 프로퍼티나 메서드들은 생성자 함수가 아닌 프로토타입 객체에 등록되고, 인스턴스는 각자 고유의 프로퍼티와 더불어 프로토타입에 있는 모든 프로퍼티와 메서드를 상속받는 것을 알았다. 그럼 만약 프로토타입에 등록된 메서드를 인스턴스에서 새로 재정의하게 되면 기존의 메서드는 어떻게 되는 것 일까.

그런 일이 생기게 되면 인스턴스에서 재정의한 메서드의 내용이 적용되게 되는데 여기에서 기존에 프로토타입에 있는 메서드를 인스턴스에서 재정의하는 것을 오버라이딩이라고 하고, 오버라이딩에 의해 기존 프로토타입에 있는 메서드가 가려지게 되는 것을 프로퍼티 섀도잉이라고 한다.

프로토타입의 교체

프로토타입 객체를 교체하는 방법으로는 두 가지 방법이 있다. 먼저 첫 번째 방법은 생성자 함수 내부에서 바로 바꾸는 방법이고, 두 번째는 인스턴스를 통해 바꾸는 방법이다. 다음 코드를 보자.

// (1)
const Person = (function() {
  function Person(name) {
    this.name = name;
  }
  
  Person.prototype = {
    sayHello() {
      console.log(`Hi! ${this.name}`);
    }
  };
  
  retrun Person;
}());

위의 방법이 첫 번째 방법으로 Person이라는 객체를 생성하는 생성자 함수 내부에서 바로 prototype객체를 지정했다. 다만 이 경우에는 자동으로 생긴 Person.prototype과 다르게 constructor프로퍼티가 없기 때문에 다음과 같이 직접 지정해줘야 한다.

Person.prototype = {
  constructor: Person,
  sayHello() {
    console.log(`Hi! ${this.name}`);
  }
};
// (2)
function Person(name) {
  this.name = name;
}

const me = new Person('Lee');

const parent = {
  sayHello() {
    console.log(`Hi! ${this.name}`);
  }
};

Object.setPrototypeOf(me, parent);

위는 두 번째 방법으로 인스턴스에서 prototype를 교체하는 방법이다. Object.setPrototypeOf 메서드를 사용하여 첫 번째 인자로는 인스턴스를, 두 번째 인자로는 교체하고자 하는 prototype객체를 넣어주면 해당 객체로 prototype 객체가 교체된다.

이 경우에도 교체된 prototype객체에서는 기본적으로 constructor프로퍼티가 없기 때문에 직접적으로 연결을 해줘야 한다.

또한 추가적으로 인스턴스를 통해 prototype객체를 교체한 경우 생성자 함수를 통해 교체한 것과 다르게 인스턴스를 생성하는 생성자 함수의 prototype프로퍼티도 연결이 끊기게 된다. 즉 생성자 함수의 prototype프로퍼티가 교체한 prototype객체를 가리키고 있지 않게 된다는 말이다. 이 것 또한 다음 코드를 통해 직접 연결해줘야 한다.

생성자함수.prototype = 교체한prototype객체

instanceof 연산자

instanceof연산자는 이항 연사자로 좌변에는 객체를 가리키는 식별자, 우변에는 생성자 함수를 가리키는 식별자가 들어가게 된다. 만역 우변의 피연산자가 함수가 아닌 경우 TypeError가 발생한다.

instanceof연산자의 동작은 우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 프로토타입 체인 상에 존재하면 true를, 아니라면 false를 반환하는 것으로 동작한다. 즉, 좌변의 객체가 우변의 prototype객체를 상속받았는지 아닌지를 확인하는 연산인 것이다.

정적 프로퍼티/메서드

정적 프로퍼티와 메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조와 호출할 수 있는 프로퍼티와 메서드를 말한다.

당연한 말이지만 생성자 함수 또한 객체이므로 자신만의 프로퍼티와 메서드를 가질 수 있다. 이때 이러한 프로퍼티와 메서드를 정적 프로퍼티, 정적 메서드라고 하며 이는 생성자 함수를 통해 생성한 인스턴스를 통해 참조나 호출할 수 없다.

정적 프로퍼티/메서드와 프로토타입 프로퍼티/메서드는 구분할 필요가 있으니 알아둬야 하고 추가적으로 프로토타입 프로퍼티/메서드는 그 사이를 줄여 #으로 표기하는 경우도 있다. (예를 들어 Object.prototype.isPrototypeOf를 Object#isPrototypeOf라고 표기한다)

객체에 사용하는 몇 가지 메서드

  1. in 연산자
    in 연산자는 객체내에 특정 프로퍼티가 존재하는지 확인하는 메서드로 다음과 같이 사용한다.
// person이라는 객체에 name이라는 프로퍼티가 있는지 확인하는 코드로 있다면 true를 없다면 false를 반환한다.
console.log(name in person)
  1. for ... in
    for ... in은 객체를 순회하는 구문으로 객체가 가지고 있는 프로퍼티들 중 열거가 가능한 프로퍼티들을 하나씩 변수에 넣어 코드가 반복된다. 이때 대상 프로퍼티들은 프로토타입 체인 상에 존재하는 모든 프로퍼티들이다. 만약 상속받은 프로퍼티는 제외하고 객체 자신의 프로퍼티만 열거하려면 Object.prototype.hasOwnProperty 메서드를 통해 확인을 하고 진행해야 한다.

    또한 for ... in 은 객체의 key들에 대해 순서를 완전히 보장하지 않는다. 최대한 순서를 보장하긴 하지만 숫자로 된 key가 있으면 해당 프로퍼티들은 오름차순으로 정렬 후 나머지들은 순서대로 순회한다.

// 로또의 회차를 key, 각 회차의 당첨번호를 value로 가지고 있는 lotto객체가 있을 때 다음 코드는 lotto객체를 순회하면서 각 회차별로 당첨번호를 출력한다.
for(let day in lotto) {
  console.log(`${day}의 당첨번호는 ${lotto[day}`);
}
  1. for ... of
    for ... of는 for ... in과 동작은 비슷하지만 순회할 때 변수에 할당되는 값이 객체의 key가 아닌 value라는 점이 다르다. 위의 코드에서 볼 때 for ... in을 for ... of로 바꾸게 되면 day라는 변수에는 각 반복마다 lotto회차가 아닌 당첨 번호가 들어가게 된다.

  2. Object.keys
    해당 메서드는 인자로 들어온 객체의 프로퍼티 중 열거가능한 프로퍼티 key값만을 배열로 반환한다.

  3. Object.values
    해당 메서드는 인자로 들어온 객체의 프로퍼티 중 열거가능한 프로퍼티들의 vlaue값만을 배열로 반환한다.

  4. Object.entries
    해당 메서드는 인자로 들어온 객체의 프로퍼티 중 열거가능한 프로퍼티들만을 key와 value로 배열에 담고 모든 배열을 다시 하나의 배열로 담아 배열로 반환한다.

[[key1, value1], [key2, value2], [key3, value3], ...]
profile
I Will be Relaxed Person

0개의 댓글