본문 바로가기

Web + APP/JavaScript

JavaScript 함수와 프로토타입 체이닝 (5)

반응형
SMALL

드디어 JavaScript 함수 마지막 글입니다.

 

여기까지 읽으신 분이 있으신진 모르겠지만, 읽으셨다면 추후에 큰 도움 되실겁니다.

 

다 기억까지한다면 베스트 !


프로토타입 체이닝

프로토타입의 두 가지 의미

JS는 기존 C++이나 자바 같은 객체지향 프로그래밍 언어와는 다른 프로토타입 기반의 객체지향 프로그래밍을 지원합니다. JS에서 OOP 상속에 근간이 되는 프로토타입과 프로토타입 체이닝의 기본 개념을 알아보도록 합시다.

 

자바와 같은 객체지향 프로그래밍에서는 클래스를 정의하고 이를 통해 객체를 생성합니다. JS에는 이러한 클래스 개념이 없습니다. 대신에 객체 리터럴이나 앞서 설명했던 생성자 함수로 객체를 생성합니다. 이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체입니다.

 

즉, 상속 개념과 마찬가지로 자식 객체는 부모 객체가 가진 프로퍼티 접근이나 메서드를 상속받아 호출하는 것이 가능합니다.

 

앞서서 많이 강조했었죠? JS의 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티가 있습니다. ECMAScript에서는 이러한 링크를 암묵적 프로토타입 링크라고 부르며 이러한 링크는 모든 객체의 [[Prototype]]프로퍼티에 저장되며, 이러한 링크를 [[Prototype]] 링크라고 하겠습니다.

 

앞선 글에서 함수 객체의 prototype 프로퍼티객체의 숨은 프로퍼티인 [[Prototype]] 링크를 구분해야합니다.

 

이를 구분하기 위해서 자바스크립트의 객체 생성 규칙을 알아보도록 합시다.

 

JS에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 [[Prototype]] 링크로 연결합니다.

 

코드를 한 번 보도록 할까요.

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

var foo = new Person('foo');

console.dir(Person);
console.dir(foo);

보시면 Person() 생성자 함수의 prototype 프로퍼티와 foo 객체의 __proto__ 프로퍼티([[Prototype]])가 같은 프로토타입 객체를 가리키고 있습니다.

 

즉, JS에서 객체를 생성하는 건 생성자 함수의 역할이지만, 생성된 객체의 실제 부모 역할을 하는 건 생성자 자신이 아닌 생성자의 prototype 프로퍼티가 가리키는 프로토타입 객체임을 말합니다.

 

앞선 글에서 말했듯, __proto__ 프로퍼티는 모든 객체에 존재하는 숨겨진 프로퍼티로 객체 자신의 프로토타입 객체를 가리키는 참조 링크 정보임을 말했었죠? 이를 [[Prototype]] 프로퍼트로 정하고, 내부적으로만 사용되고 있습니다.

 

객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝

JS에서 객체는 자기 자신의 프로퍼티뿐만이 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 마치 자신의 것처럼 접근하는 게 가능합니다. 이것이 프로토타입 체이닝입니다.

var myObject = {
	name: 'foo',
    sayName: function () {
    	console.log('My name is ' + this.name);
	}
};

myObject.sayName(); // My name is foo
console.log(myObject.hasOwnProperty('name'); // true
console.log(myObject.hasOwnProperty('nickName'); // false
myObject.sayNickName(); // error

위에 코드는 어렵게 느껴지지 않을 것입니다.

 

그런데 생각해보면 myObject 객체에 hasOwnProperty() 메서드가 없음에도 결과가 정상적으로 출력됐습니다. 어떻게 이게 가능할까요?

 

객체 리터럴로 생성한 객체는 Object()라는 내장 생성자 함수로 생성된 것입니다. Object() 생성자 함수도 함수 객체이므로 prototype이라는 프로퍼티 속성이 있습니다. 따라서 앞서 설명한 자바스크립트의 규칙으로 생성한 객체 리터럴 형태의 myObject는 Object() 함수의 prototype 프로퍼티가 가리키는 Object.prototype 객체를 자신의 프로토타입 객체로 연결합니다.

 

만약, 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 [[Prototype]] 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색하는 것을 프로토타입 체이닝이라고 말합니다.

 

즉, hasOwnProperty() 메소드는 myObject 객체의  [[Prototype]] 링크를 따라 그것의 부모 역할을 하는 Object.prototype 프로토타입 객체 내에 hasOwnProperty() 메소드를 찾아 실행하는 것입니다.

 

생성자 함수로 생성된 객체의 프로토타입 체이닝

생성자 함수로 객체를 생성하는 경우 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 어뤄집니다.

 

허나 두 가지 방식 모두 같은 기본 원칙이 있는데 아래와 같습니다.

 

"자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체(부모)로 취급한다."

 

위에 Person()으로 예시든 코드를 확인하시면 위의 말의 무슨 말인지 이해되기 더 쉬우실거에요.

 

프로토타입 체이닝의 종점

자바스크립트에서 Object.prototype 객체는 프로토타입 체이닝의 종점입니다. 객체 리터럴 방식, 생성자 함수를 이용한 방식 모두 결국 Object.prototype에서 프로토타입 체이닝이 끝나는 것을 알 수 있습니다.

 

즉, Object.prototype 객체가 가진 프로퍼티와 메서드에 접근하고, 서로 공유가 가능하다는 것을 알 수 있죠.

 

기본 데이터 타입 확장

앞서 Object.prototype에 정의된 메서드들은 자바스크립트의 모든 객체의 표준 메서드라고 볼 수 있습니다. (hasOwnProperty(), isPrototypeOf())

 

이와 같은 방식으로 자바스크립트의 숫자, 문자열, 배열 등에서 사용되는 표준 메서드들의 경우, 이들의 프로토타입인 Number.prototype, String.prototype, Array.prototype 등에 정의되어 있습니다. 물론 이런 기본 내장 프로토타입 객체 또한 Object.prototype을 자신의 프로토타입으로 가지고 있어서 프로토타입 체이닝으로 연결됩니다.

 

또한 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용합니다.

 

String.prototype.testMethod = function () {
	console.log('This is the String.prototype.testMethod()');
};

var str = "this is test";
str.testMethod();

console.dir(String.prototype);

 

프로토타입도 자바스크립트 객체다

프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제하는 것이 가능합니다.

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

var foo = new Person('foo');

foo.sayHello(); // error

Person.prototype.sayHello = function() {
	console.log('Hello');
}

foo.sayHello(); // Hello

 

프로토타입 메서드와 this 바인딩

프로토타입 객체는 메서드를 가질 수 있다. 이를 프로토타입 메서드라 합시다.

 

만약 프로토타입 메서드 내부에서 this를 사용한다면 이는 어디에 바인딩 될까요?

 

결국 메서드 호출 패턴에서의 this는 그 메서드를 호출한 객체에 바인딩 됩니다.

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

Person.prototype.getName = function () {
	return this.name;
}

var foo = new Person('foo');

console.log(foo.getName()); // foo

Person.prototype.name = 'person'; // Person.prototype 객체에 name 프로퍼티 동적 추가

console.log(Person.prototype.getName()); // person

 

디폴트 프로토타입은 다른 객체로 변경이 가능하다

디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, 함수의 prototype 프로퍼티에 연결됩니다. JS에서는 이렇게 함수를 생성할 때 해당 함수와 연결되는 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능합니다.

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

console.log(Person.prototype.constructor); // Person(name)

var foo = new Person('foo');
console.log(foo.country); // undefined

Person.prototype = {
	country: 'korea',
};
console.log(Person.prototype.constructor); // Object()

var bar = new Person('bar');
console.log(foo.country); // undefined
console.log(bar.country); // korea
console.log(foo.constructor); // Person(name)
console.log(bar.constructor); // Object()

 

객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작한다.

객체의 특정 프로퍼티를 읽으려고 할 때, 프로퍼티가 해당 객체에 없는 경우 프로토타입 체이닝이 발생합니다. 반대로 객체에 있는 특정 프로퍼티에 값을 쓰려고 한다면 이 때는 프로토타입 체이닝이 일어나지 않습니다.

 

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

Person.prototype.country = 'Korea';

var foo = new Person('foo');
var bar = new Person('bar');
console.log(foo.country); // Korea
console.log(bar.country); // Korea
foo.country = 'USA';

console.log(foo.country); // USA
console.log(bar.country); // Korea

휴 드디어 끝났습니다.

 

큰 산 하나 넘었으니, 이제부턴 좀 더 가볍게 볼 수 있는 글을 올리도록 해보겠습니다.

 

이상 JavaScript 함수와 프로토타입 체이닝 (5)였습니다. ^_^

반응형
LIST