2015년 5월 6일 수요일

[JavaScript] 자바스크립트 프로토타입(Prototype) 객체 이해하기

프로토타입(Prototype)이란?


프로토타입(Prototype), 이름에서 알 수 있듯 프로토타입이란 객체의 기본형인 객체를 뜻한다. 모든 객체는 프로토타입 객체를 가지며 프로토타입의 프로퍼티에 접근이 가능하다. 또한 이 프로토타입 객체도 객체인만큼 포로토타입 객체를 가진다. 즉 가기장 기본 Object 객체를 제외한 모든 객체들은 프로토타입 객체를 가지며 모든 객체의 최상위 프로토타입은 기본 Object.prototype 객체가 된다. 즉, 모든 객체들은 1개의 객체 인스턴스와 해당 인스턴스의 속성들을 공유하며 상속한다는 말이 된다.

이 프로토타입(Prototype)이 중요한 이유는 OOP의 상속 컨셉을 자바스크립트에서 구현할 수 있게하기 때문이다. 만약 객체가 뭔지 모른다면, 우선 이 글(클릭하기)을 읽고 와주기를 바란다.

우선 객체의 프로토타입은 ECMAScript 에서 지원하는 표준 메소드인 Object.getPrototypeOf() 메소드를 통해 접근할 수 있으며, 이 메소드는 프로토타입을 얻어(get)오는 메소드이므로 이메소드를 통해서 프로토타입을 수정하는것은 불가능하다.

*하지만 곧 ECMAScript 6에서 Object.setPrototypeOf() 메소드가 생긴다.

위의 표준 메소드와 별개로 Chrome, Internet Explorer, Mozilla FireFox 등에서는 __proto__따프로퍼티를 이용해 객체의 프로토타입에 접근할 수 있다.


var ob1 = {};
var ob2 = {};

console.log(ob1.__proto__); //Object
console.log(Object.getPrototypeOf(ob2)); //Object
console.log(ob1.__proto__ == Object.getPrototypeOf(ob2)); //true
//기본으로 생성되는 객체는 모두 같은 Object 프로토타입 객체 인스턴스을 참조한다.

여기서 ob1과 ob2 객체에 프로토타입을 설정해준다면


var protoObj = {
  getFirstName: function() {
    return this.firstName;
  }
}
var ob1 = {};
var ob2 = {};
ob1.__proto__ = protoObj;
ob2.__proto__ = protoObj;

ob1.firstName = "Jason";
ob2.firstName = "Natali";
console.log(ob1.getFirstName()); //Jason
console.log(ob2.getFirstName()); //Natali

protoObj.getFirstName = function(){
  return "Function changed";
};

console.log(ob1.getFirstName()); //Function changed
console.log(ob2.getFirstName()); //Function changed
console.log(ob1.__proto__==ob2.__proto__); //true
console.log(protoObj.__proto__ == Object.prototype); //true

이렇게 다른 언어의 객체 상속과 같은 결과를 낳게 된다. 또한 두 객체 모두 같은 프로토타입 객체 인스턴스를 참조하고 있으므로 주어진 프로토타입 객체의 수정은 해당 프로토타입 객체를 참조하는 모든 객체에 적용된다.

위 객체들의 최종 관계도를 UML 다이어그램으로 나타내자면 아래와 같다.

프로토타입 객체 인스턴스와 객체 인스턴스들의 관계도

앞서 말했듯, 모든 사용자 생성 객체는 생성될 때 Object.prototype 객체를 프로토타입으로 가진다.
그리고 ob1과 ob2는 같은 protoObj 인스턴스를 프로토타임으로 가지니, protoObj 인스턴스에 생기는 변화는 obj1과 obj2에 모두 적용된다.

그리고 Object.prototype에는


Object.prototype.constructor
Object.prototype.hasOwnProperty()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.toLocaleString()
Object.prototype.toString()

와 같이 생성되는 객체에 기본으로 탑재되는 메소드들이 프로퍼티로 저장되 있다.

*참고로 Object.prototype 객체 프로퍼티들의 속성은
Writable : false
Enumerable : false
Configurable : false
이니 어떤 방법을 써서도 코드 프로퍼티 리스트나 프로퍼티 수정이 불가능하다.


그렇다면 Object.prototype에서 Object란 무었일까? 이를 이해하기 위해서는 우선 constructor에 대해 알아야하니 모른다면 이 글(클릭하기)을 읽고 오자.

프로토타입과 생성자 함수(Constructor)


우리가 Constructor 라 부르는 함수들은 function 객체로 여러가지 프로퍼티를 가지는데, 그 중 prototype 이라는 프로퍼티가 존재한다. 이 prototype이라는 프로퍼티는 해당 생성자 함수에 의해 성성되는 객체들에 부여되는 프로토타입 객체를 참조한다.

*참고로 이 prototype 프로퍼티는 함수(Function) 객채의 프로퍼티이고, Object.getPrototypeOf 메소드로 얻어지는 프로토타입 객체는 함수(Function) 객체의 프로토타입이다. 자바스크립트에서는 함수도 객체라는것을 잊지 말자


function Foo(){};

var foo = new Foo();
var too = new Foo();

console.log(foo.__proto__ == Foo.prototype); //true
console.log(foo.__proto__ == Object.getPrototypeOf(Foo)); //false
console.log(Object.getPrototypeOf(Foo) == Function.prototype); //true
console.log(foo.__proto__ == too.__proto__); //true
console.log(Foo == Foo.prototype.constructor); //true

함수 Foo의 프로토타입 객체가 바로 Function.prototype 객체이며, 이는 Foo.prototype 프로퍼티가 참조하는 객체와는 다른 객체이다.

그리고 해당 함수의 prototype 프로퍼티가 참조하는 객체의 constructor 프로퍼티는 해당 함수를 참조한다.
관계도를 그린다면 아래와 같이 된다.

Constructor Foo로 생성되는 객체 인스턴스들과 프로토타입의 관계도

생성자 함수 Foo에 의해 생성된 객체 footoo는 모두 함수 Fooprototype 프로퍼티가 참조하는 객체 Foo.prototype을 프로토타입으로 두고 있다. 즉 Foo.prototype의 수정은 footoo 객체 모두에 영향을 끼치게 된다.

*단, 아래의 경우처럼 프로토타입을 새로이 어싸인(Assign)할 경우에는 해당 객체가 참조하는 프로토타입만이 바뀔 뿐, 원 프로는는토타입이나 같은 프로타입을 가진 다른 객체들은 아무런 영향을 받지 않는다.


function Foo() {};

var foo = new Foo();
var too = new Foo();

var newPrototype = new Object();

foo.__proto__ = newPrototype;

console.log(Foo.prototype == foo.__proto__); //false
console.log(Foo.prototype == too.__proto__); //true
console.log(foo.__proto__ == too.__proto__); //false

위의 코드가 실행되면 관계도는 아래처럼 변한다.

생성된 객체에 새로운 프로토타입 어싸인(Assign) 후 관계도

객체 foo에 새로이 어싸인된 프로토타입은 constructor로 기본 Object 함수를 참조하고 있는데, 이는 newPrototype이 사용자에 의해 생성된, 즉 Object() 생성자 함수를 이용해 생성되었기 때문이다.

저 constructor도 프로퍼티 이기 때문에
foo.__proto__.constructor, 또는 newPrototype 객체 생성시 newPrototype.constructor 프로퍼티를 이용해 원하는 생성자 함수를 어싸인해주면 된다.

Property Lookup(프로퍼티 검색)과 프로퍼티 overriding


어떤 객채이든 프로퍼티 검색은 해당 객체의 본체에서부터 시작된다. 아래의 코드를 보자.


var fooProto = {
  write: function() {
    return "write";
  },
  read: function() {
    return "read";
  }
};

function Foo() {
  this.write = function() {
    return "No write";
  }
}
Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.write()); //No write
console.log(foo.read()); //read

too = new Foo();
too.read = function() {
  return "No read";
}

console.log(too.write()); //No write
console.log(too.read()); //No read

delete too.read; //too.read 프로퍼티 삭제

console.log(too.read()); //read

객체의 프로퍼티 호출이 일어나게 되면 자바스크립트 엔진은 가장 먼져 호출이 된 해당 객체(base object)에서 프로퍼티를 검색한다. 그리고, 만약 찾지 못하면 해당 객체의 프로토타입 객체에서 프로퍼티를 검색하고, 그리고도 찾지못다면 프로토타입의 프로토타입 객체까지 검색을 이어간다. 이 검색은 프로퍼티가 발견되거나, 또는 더 이상 검색을 이어갈 프로토타입이 없을 때(Object.prototype까지 도달할 떄)까지 계속 이어진다. 즉 하위 객체에 해당 프로퍼티가 존재한다면 프로퍼티 Overriding이 가능하다는 것이다.

이 때문에 위의 코드에서도 foo.write() 메소드는 Foo함수에서 정의한대로 실행되고, foo.read() 메소드는 Foo함수 생성자의 프로토타입 프로퍼티가 참조하는 fooProto 객체에서 정의한대로 실행되는 것이다. 이는 too.write(), too.read()의 실행결과에서도 잘 알 수 있다.

또 위의 코드를 보면 too.read 프로퍼티를 삭제하게되면 too.read() 메소드가 프로토타입 객체에서 정의한대로 작동한다. 즉, 프로토타입 객체를 직접 호출하지 않고는 하위 객체에서 프로토타입의 프로퍼티를 수정하는 것이 불가능하다.

Object.prototype.hasOwnProperty() 메소드와 in Operator(연산자)


hasOwnProperty() 메소드는 해당 객체에  파라미터로 주어진 이름을 가진 프로퍼티가 있는지를 확인한다.

앞서 말했듯, 자바스크립트 엔진은 프로퍼티 호출이 발생하면, 해당 객체에서 먼져 프로퍼티를 검색한 후, 찾지 못한다면 프로토타입으로 검색을 이어간다. 이 때문에, 특정 프로퍼티가 호출되어도 개발자들은 해당 프로퍼티가 프로토타입에 속한 프로퍼티인지, 하위 객체에 속한 프로퍼티인지 알 수 없게 되는데, hasOwnProperty() 메소드는 그 점을 해결해주는 메소드 이다.

사용법은 간단하다. 확인을 원하는 객체와 프로퍼티 이름으로 메소드를 호출하면 Boolean(true||false) 값을 리턴한다.

hasOwnProperty()와는 달리 in 연산자는 해당 객체에서 간접적으로 접근이 가능한 모든 프로퍼티에 대해 true 값을 반환단다.


var fooProto = {
  a: "Letter A",
  b: "Letter B",
};

function Foo() {
  this.b = "Letter B";
  this.c = "Letter C";
}

Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.hasOwnProperty("a")); //false
console.log("a" in foo); //true
console.log(foo.hasOwnProperty("b")); //true
console.log("b" in foo); //true
console.log(foo.hasOwnProperty("c")); //true
console.log("c" in foo); //true

Object.prototype.isPrototypeOf() 메소드


isPrototypeOf 메소드는 해당 객체가 파라미터로 주어진 객체의 프로토타입인지를 확인하는 메소드 이다.

물론 Object.getPrototypeOf() 메소드 또는 __proto__ 프로퍼티와 '=' 연산자(equal operand)를 이용해서도 확인이 가능하지만, 이 방법을 대체할 수 있게 해주는 메소드 이다.


var fooProto = {
  a: "Letter A",
  b: "Letter B",
};

function Foo() {
  this.b = "Letter B";
  this.c = "Letter C";
}

Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.isPrototypeOf(fooProto)); //false

//아래 3가지 확인법들은 모두 같은 결과를 반환한다.
console.log(fooProto.isPrototypeOf(foo)); //true
console.log(fooProto == Object.getPrototypeOf(foo)); //true
console.log(fooProto == foo.__proto__); //true


댓글 없음:

댓글 쓰기