[Effective JavaScript] prototype, getPrototypeOf, _ _ proto _ _의 차이점을 이해하자

김범식·2023년 7월 13일
0

Effective JavaScript

목록 보기
27/33
post-thumbnail

객체와 프로토타입의 차이




객체(Object)

  • javascript에서 모든 것은 객체이다. 속성과 메서드를 가질 수 있다.
  • 키 - 값 쌍으로 이루어진 속성을 포함, 객체의 상태를 나타낸다.
  • {} 또는 new Object() 를 통해 생성할 수 있다.



프로토타입(Prototype)

  • 프로토타입은 객체의 부모 역할을 하는 객체이다. 다른 객체들이 상속을 통해 프로토타입의 속성과 메서드를 공유할 수 있다.
  • javascript는 프로토타입 기반의 언어이다. 객체들 간의 상속관계를 형성하는데 프로토타입체인을 사용한다.
  • 객체는 _ _ *proto* _ _ ****라는 내부 속성을 통해 자신으 프로토타입에 대한 참조를 유지
  • ES6부터는 Object.getPrototypeOf(). Object.setPrototypeOf() 메서드로 프로토타입 조작 가능



예시 1 : 객체 생성자와 프로토타입

function Person(name){ //이걸 생성자로 사용해서 하나의 객체를 만들수 있다. 
	this.name = name;
}

//프로토타입에 메서드 추가 
Person.prototype.greet = function(){
	console.log("Hello, " + this.name+"!");
};

//객체 인스턴스 생성
var person1 = new Person('Alice');
var person2 = new Person('Bob');

// 프로토 타입 메서드 호출
person1.greet(); // Hello Alice!
person2.greet(); // Hello Bob!
  • Person이라는 객체 생성자를 정의 하고 Person.prototype을 통해 greet 메서드를 추가한다. 이렇게 하므로써 모든 객체가 greet를 공유할 수 있다.
  • 헷갈릴수 있는데 함수안에 this로 함수 추가하면서 사용하는게 원래 이상함 때문에 prototype이 없으면 추가할 수없음
  • 즉 function으로 만든 생성자가 아니면 추가할 일이 없다.



예시 2 : 내장 객체의 프로토타입 확장

String.prototype.reverse = function(){
	return this.split('').reverse().join('');	
}

//문자열에서 프로토타입 메서드 호출
var str = 'Hello, World!';
console.log(str.reverse()); //"!dlroW ,olleH"
  • javascript 내장 객체인 String의 프로토타입에 reverse라는 메서드를 추가했다.
  • 이제 reverse메서드를 호출하여 문자열을 뒤집을 수 있다.

이 처럼 prototype은 내장객체의 기능을 확장하고 공유할 수 있다.



prototype, getPrototypeOf, _ proto _의 차이점을 이해하자


다음 세가지는 이름이 비슷하기 때문에 자연스럽게 헷갈린다. 이번에 확실히 정리해보자

  • C.prototypenew C()로 생성된 객체의 프로토타입을 만드는데 사용된다.
  • Object.getPrototypeOf(obj)는 obj의프로토 타입 객체를 가져오기 위한 ES5 표준 메커니즘이다.
  • obj. _ _ proto _ _는 obj의 프로토타입 객체를 가져오는 비표준 매커니즘 이다.



prototype 이란?

각각을 이해하기 위해 자바스크립트 데이터형의 일반적인 정의를 보자

function User(name, passwordHash){
	this.name = name;
	this.passwordHash =passwordHash;
}

User.prototype.toString = function(){
	return "[User" + this.name+ "] ";
}

User.prototype.checkPassword = function(password){
	return hash(password) === this.passwordHash;
};
var u = new User("sfalken","alkjselijfecaij14212323");
  • User 함수는 디폴트 prototype 프로퍼티를 갖게 되는데 , 이 프로퍼티는 처음에는 비어있는 객체를 나타낸다. 이 예제는 toString과 checkPassword를 추가했다.
console.log(User.prototype)

// 다음과 같은 문자열이 출력된다. 
[object Object] {
  checkPassword: function(password){
  return hash(password) === this.passwordHash;
},
  toString: function(){
  return "[User" + this.name+ "] ";
}
}
console.log(u.prototype) // 아무것도 안나옴 왜냐하면 u는 User.prototype을 상속 받기 때문

console.log(u)

//프로토 타입을 상속 받고 생성자로 만들어진 변수도 존재한다. 
[object Object] {
  checkPassword: function(password){
  return hash(password) === this.passwordHash;
},
  name: "sfalken",
  passwordHash: "alkjselijfecaij14212323",
  toString: function(){
  return "[User" + this.name+ "] ";
}
}

다음과 같은 그림을 보면 이해하는데 도움이 된다.

화살표가 인스턴스 객체 u와 프로토타입 객체 User.prototype을 연결하고 있다. 이 연결은 상속관계를 설명한다. 프로퍼티 탐색은 객체 자신의 프로퍼티로 부터 시작한다. 예를 들어 u.name과 u.passwordHash 는 u의 해당 프로퍼티와 현개 값을 즉시 반환한다.

u에서 직접 찾을 수 없는 프로퍼티는 User의 prototype에서 찾게 된다. 또한 u.checkPassword에 접근하면 User.prototype에 저장된 메서드를 반환한다.



Function.prototype이란?


Function.prototype은 javascript의 내장 객체인 Function의 프로토 타입이다. 모든 함수 객체는 Function.prototype을 상속 받는다.

Function.prototype은 함수 객체를 위한 메소드와 속성을 포함하고 있고, 이 속성과 메서드는 모든 함수 객체가 공유하고 있다.

  • call()
  • apply()
  • bind()

다음 과 같은 함수를 실행하거나 함수의 prototype 속성을 확인하는데 사용되기도 한다.

다음은 Function.prototype으로 상속받은 객체들이다.

function greet() {
  console.log('Hello!');
}

console.log(greet.call); // 함수의 call 메서드
console.log(greet.apply); // 함수의 apply 메서드
console.log(greet.bind); // 함수의 bind 메서드
console.log(greet.toString); // 함수의 toString 메서드
console.log(greet.hasOwnProperty); // 객체의 hasOwnProperty 메서드

이 함수들은 모두 Function.prototype에서 정의된 메서드이며 함수를 실행하거나 바인딩 하고자 할 때 유용하게 사용된다.



“javascript의 모든 함수는 자신의 prototype을 갖고 있는가?”


모든 함수가 자신의 prototype을 갖고 있는것은 아니다. 일반적인 javascript함수는 prototype이라는 속성을 갖고있다. 이 속성은 해당 함수로 생성된 객체 인스턴스들이 상속을 받을 수 있는 포로토 타입객체를 가리킨다. 때문에 함수 생성자 ( new Person() )사용하여 객체 인스턴스를 생성할 때 해당함수의 prototype에 정의된 속성과 메서드를 상속 받을 수 있다.

하지만 모든함수가 자신의 prototype을 가지는것은 아니다.

function regularFunction() {}
var arrowFunction = () => {};

console.log(regularFunction.prototype); // 일반적인 함수는 prototype을 가짐
console.log(arrowFunction.prototype); // 화살표 함수는 prototype을 가지지 않음

위의 예시에서 regularFunction은 일반 함수로 prototype을 가지고 있지만 arrowFunction은 화살표 함수로 prototype을 가지고 있지 않다.

const func = function() {};
console.log(func.prototype); // 함수의 prototype에 접근 가능

물론 다음과 같은 형태를 사용해도 prototype에 접근할 수 있다.



constructor란?

prototype안에는 constructor라는 중요한 속성이 들어있다. 이속성은 함수를 new 연산자를 사용해 객체를 만들어 줄수 있게하는 속성이다.

다음은 일반함수 생성자로 만든 함수의 constructor 속성이다.

const func1 = function() {return 1;};
console.log(func1.prototype.constructor); // function() {return 1;}

function func2(){ return 2; }
console.log(func2.prototype.constructor); //function func2(){ return 2; }

다음은 좀더 일반적은 사례를 보자

function Person(name, age){
  this.name = name;
  this.age = age;
}
console.log(Person.prototype.constructor)
// 출력
// function Person(name, age){
// this.name = name;
// this.age = age;
// }

let person = new Person("이순신", 45);
console.log(person.constructor)
// 출력
// function Person(name, age){
// this.name = name;
// this.age = age;
// }

이처럼 자신을 생성한 생성자가 어떻게 생겼는지 constructor를 통해 확인할 수 있다. ‘

다만 arrowFunction으로 만들어진 함수는 prototype이 없기 때문에 constructor도 없으며 new 연산자를 통해 객체를 생성할 수 없다.

var func3 = (name,age)=>{ this.name = name; this.age = age; }
console.log(func3.prototype); //undefined
console.log(new func3("홍길동",30)); // 에러



직접 함수 생성과 prototype에 함수 생성


우리는 prototype에 함수를 정의하면 new 연산자로 만든 인스턴스에서 그 함수를 사용할 수 있다는점을 배웠다. 그렇다면 prototype에 정의하는것과 생성자 함수에 적접 함수를 정의하는것에는 어떤 차이가 있을까?

function Person(name, age){
  this.name = name;
  this.age = age;
  function show(){
    console.log(name+" "+age);
	}
}
function Person(name, age){
  this.name = name;
  this.age = age;
}

Person.prototype.show = function(){
    console.log(name+" "+age);
}
  1. 메모리 사용량 : 함수 내부에 직접 메서드를 정의하는 경우 생성자 함수를 호출 할 때마다 해당 메서드가 새로운 인스턴스마다 생성된다. 따라서 인스턴스마다 독립적인 메모리 공간을 차지하게 되므로 메모리 사용량이 늘어날 수 있다.

  2. 메서드 업데이트 : 직접 정의하는 경우, 생성자 함수를 수정하거나 새로운 인스턴스를 생성할 때마다 재정의해야한다. 이는 코드 유지보수의 어려움을 야기할 수 있다. prototype에 메서드를 정의하면 해당 메서드를 한 번 정의하면 모든 인스턴스에 공유 되므로 생성자 함수를 수정하지 않아도 메서드 업데이트를 할 수 있다.

  3. 프로토타입 체인 : 함수 내부에 직접 메서드를 정의하는 경우, 해당 메서드는 인스턴스의 프로토 타입 체인에 포함되지 않는다. 따라서 인스턴스에서 메서드를 호출할 때 해당 메서드를 찾기위해 프로토 타입 체인을 거슬러 올라가야한다. 반면 prototype에 메서드를 정의하면 해당 메서드는 인스턴스의 프로토타입에 포함되어 바로 접근할 수 있으므로 성능상의 이점이 있다.

따라서 일반적으로 생성자 함수의 메서드는 prototype에 정의하는것을 권장하게 된다.



프로토타입 체인이란?

모든 객체는 prototype 이라는 내부 속성을 가지고 있는데, prototype은 상위 부모 객체를 가리킵니다. 이렇게 객체의 prototype을 따라 올라가면서 상위 객체의 프로퍼티와 메서드에 접근할 수 있는것을 프로토타입체인 이라고 합니다.

// 부모 객체 생성자 함수
function Parent(){
	this.aaa = 'parent'
    this.bbb = function(){
      console.log('Parent 함수')
    }
}

// 자식 객체 생성자 함수
function Child(){
	this.name = 'Child'
}

//프로토 타입 체인 설정
Child.prototype = new Parent();

// 객체 인스턴스 생성
var child = new Child();

console.log(child.name); //Child;
console.log(Child.prototype.aaa); // parent

Child.prototype.func = function(){
   console.log("추가 함수")
}

Child.prototype.func(); //추가함수
Child.prototype.bbb(); //Parent 함수

이 처럼 프로토타입 체인은 상속을 구현하는 중요한 개념으로, 객체 지향 프로그래밍에서 다양한 상속 패턴과 객체간의 관계를 구성하는데 사용된다. 코드의 재사용성과 확장성을 높일 수 있으며 객체간의 계층 구조를 유지하며서 프로퍼티와 메서드를 공유할 수 있다.



Object.getPrototypeOf()


생성자 함수의 prototype 프로퍼티가 새 인스턴스의 프로토타입 관계를 설정하기 위해 사용된 반면에 ES5의 Object.getPrototypeOf() 함수는 현재 객체의 프로토타입을 가져오는데 사용할 수 있다.

Object.getPrototypeOf(u) === User.prototype; // true
  • 인스턴스가 상속받은 함수의 prototype을 가져올 수 있다.



_ proto _

몇몇 실행 환경에서는 비표준 메커니즘인 특별한 _ _ proto _ _ 프로퍼티를 제공한다. 이 프로퍼티는 ES5 의 Object.getPrototypeOf()를 지원하지 않는 환경에서 유용하다.

u.__proto__ === User.prototype // true

이처럼 자바스크립트에서 클래스는 기본저긍로 생성자 함수(User)와 클래스의 인스턴스 간에 메서드를 공유하기 위해서 사용되는 프로토타입 객체 (User.prototype)의 조합이다. 즉 User.prototype은 인스턴스 간에 공유되는 메서드의 내부 구현체다.



기억할 점

  • C.prototypenew C() 로 생성된 객체의 프로토타입을 결정한다.
  • Object.getPrototype(obj)는 객체의 프로토타입을 가져오기 위한 표준 ES5함수이다.
  • obj. _ _ proto_ _ 는 객체의 프로토타입을 가져오기 위한 비표준 메커니즘이다.
  • 클래스는 생성자 함수와 연관된 프로토타입으로 구성된 설계 패턴이다.
profile
frontend developer

0개의 댓글