이번 포스팅에서는 JavaScript의 ProtoType에 대해서 다루어 보도록 하겠습니다.
JS에서 prototype
JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 불립니다.— 모든 객체들이 메서드와 속성들을 상속받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미입니다. 프로토타입 객체도 또다시 상위 프로토타입 객체로부터 메서드와 속성을 상속받을 수도 있고 그 상위 프로토타입 객체도 마찬가지입니다. 이를 프로토타입 체인(prototype chain)이라 부르며 다른 객체에 정의된 메서드와 속성을 한 객체에서 사용할 수 있도록 하는 근간입니다.
-mozilla.org-
위에서 말하듯이, JS에서 모든 것은 객체이며, 모든 객체는 상속을 위한 protoype object 템플릿을 가집니다.
정말로 모든 것이 객체이며, 객체들은 전부 메모리 위에 실존하는 실체(instance)입니다.
대신, prototype chain 등을 통해, 부모를 복사하는 방식이 아니라, 부모의 링크를 참조하는 형식으로 상속을 구현합니다.
이런 protoype chain을 만드는 new와 생성자가 내부적으로 어떻게 돌아가는지 차근차근 알아봅시다.
객체의 __proto__와 그 외
- 자기 자신만의 가방 : property , method
- __proto__ : 부모객체.prototype
위에서 보이듯,
JS에서 Object는 속성(property)이 메서드(method)라는 멤버를 갖습니다.
속성은 변수, 메서드는 함수라고 생각하셔도 됩니다.
(사실 JS에서는 메서드라는 개념이 없습니다. method 또한 일반적인 property와 같은 방식으로 저장됩니다.
그저 변수의 값이 아닌 함수의 포인터(그 함수에 대한 참조 또는 주소)가 저장될 뿐입니다.)
또, __proto__라는 property를 가지고 있습니다.
__proto__는 자신이 상속받은 부모의 prototype 속성 값입니다.
__proto__와 prototype에 대한 차이는 다음 단락에서 다룹니다.
다음은 함수 생성자로 객체를 생성하고 멤버를 확인하는 코드입니다.
function Obj(){
this.myProperty = "i am property",
this.myMethod = function(){
console.log("i am method")
}
}
let obj = new Obj()
console.log(obj)
/* then, print...
myMethod: ƒ ()
myProperty: "i am property"
__proto__: Object
*/
저희가 코드로 명시한 myProperty와 myMethod 이외에 __proto__라는 property가 보입니다.
왜 이런 속성이 추가되었을까요?
왜 그렇게 되는지 알기 위해서는 new로 생성자를 호출할 때, 뒷단에서 이루어지는 과정을 자세히 살펴봐야 합니다.
그럼, 생성자에 대해 자세히 알아봅시다.
How to work new? (또는 생성자(constuctor))
JS는 ECMAScript를 준수하는 언어 중 하나입니다.
따라서, 생성자에 대한 ECMAScript에 대한 규정을 봅시다.
When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:
1. Let obj be a newly created native ECMAScript object.
2. Set all the internal methods of obj as specified in 8.12.
3. Set the [[Class]] internal property of obj to "Object".
4. Set the [[Extensible]] internal property of obj to true.
5. Let proto be the value of calling the [[Get]] internal property of F with argument "prototype".
6. If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto.
7. If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.
8. Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args.
9. If Type(result) is Object then return result.
10.Return obj.
복잡한 말이 많지만 필요한 부분만 보자면,
1. "Let obj be a newly created" : 새로운 객체 obj를 만듭니다.
2. "set the [[Prototype]] internal property of obj to proto" : 함수 생성자 F의 Prototype속성 값을 obj의 __proto__ 로 설정합니다.
3. "calling the [[Call]] internal property of F" : 함수 생성자 F의 call을 호출합니다.
4. "Return obj." : 열심히 만든 obj를 반환합니다.
(JS에서 constructor는 return을 명시하지 않기 때문에 undefined가 자동으로 return 됩니다. 그리고 undefined의 Type은 Object가 아니지요. 따라서 9번은 JS의 constructor를 재정의하지 않는 이상 실행되지 않습니다.)
더 깊은 이해를 위해, 이렇게 내부적으로 돌아가는 부분을 코드로 만들어 봅시다.
다음은 함수 생성자 선언 코드입니다.
//define function constructor
function Obj(){
this.myProperty = "i am property"
this.myMethod = function(){ console.log("i am method")}
}
다음은 new 이용해 새로운 객체를 만드는 코드입니다.
let obj = new Obj()
위 코드는 다음 코드와 동일한 방식으로 동작합니다.
let obj
let newObject = Object.create(Obj.prototype), returnConstructor
returnConstructor = Obj.call(newObject)
if(typeof returnConstructor === "object"){
obj = returnConstructor
}else {
obj = newObject
}
Obj의 prototype 속성을 __proto__로 하는 새로운 객체 newObject를 만들고,
Obj의 call을 호출하며 인자로 newObject를 넘겨주어,
함수 생성자의 this를 다음 코드와 같이 newObject로 치환하고 Obj의 constructor를 실행하고,
function Obj(){
this.myProperty = "i am property"
this.myMethod = function(){ console.log("i am method")}
}
Obj.call(newObject)
//above function constructor will be transpiled with below code and executed
function Obj(){
newObject.myProperty = "i am property"
newObject.myMethod = function(){ console.log("i am method")}
}
그 반환 값을 returnConstructor에 대입합니다.
그리고 만약 함수 생성자의 반환 값이 object가 아니라면(undefined라면),
newObject 객체를 반환하여 obj에 대입합니다.
그렇지 않다면, returnConstructor를 obj에 대입합니다.
실제로는 prototype.constructor 가 쓰이지 않음에 유의해주세요!
constructor가 쓰일 때는 new Obj() 대신, new obj.constructor()를 호출할 때뿐입니다.
모든 것이 그저 JS 함수일 뿐입니다.
무슨 함수든 생성자가 될 수 있습니다.
(Function.prototype을 상속하므로. call을 부를 수 있다면 정말로 무엇이든!)
이런 과정을 거쳐 처음처럼 다음과 같은 결과가 출력됩니다.
console.log(obj)
//then print ...
myMethod: ƒ ()
myProperty: "i am property"
__proto__: Object
Prototype chain와 상속
위 과정에서 눈치채셨나요?
객체를 생성하는 생성자는 반드시 부모(==prototype)가 되는 객체가 있어야 합니다.
자바스크립트에서 함수는 속성을 가질 수 있다. 모든 함수에는 prototype이라는 특수한 속성이 있다.
-mozilla-
이것은 모든 함수가 생성자가 될 수 있음을 의미합니다.
null을 __proto__로 가지는 Object, Array와 같은 내장(built-in) 객체들도 역시 prototype을 가지고 있습니다.
Prototype chain에 대한 자세한 이해를 위해 다음 코드를 살펴봅시다.
function Obj(){
this.prop = "im prop"
}
console(Obj.prototype)
//then, print..
constructor: ƒ Obj()
__proto__: Object
let obj = new Obj()
console.log(obj.__proto__)
//then, print ..
constructor: ƒ Obj()
__proto__: Object
console.log(obj.__proto__ === Obj.prototype)
//then, print..
true
위 코드에서 알 수 있듯이,
자식 객체의 __proto__ 속성과 부모 객체의 prototype 속성은 동일한 객체를 가리키고 있습니다.
JS에서 상속을 부모의 값을 복사하는 것이 아니라, 객체에 대한 링크로 표현합니다.
call by value가 아닌 call by reference라고도 이해할 수 있습니다.
따라서, 부모 객체가 변경되면, 이를 상속받는 모든 자식 객체가 참조하는 값이 변경됩니다.
prototype chain에 대한 값 참조에 대한 다음의 예시 코드를 봅시다.
// o라는 객체가 있고, 속성 'a' 와 'b'를 갖고 있다고 하자.
let f = function () {
this.a = 1;
this.b = 2;
}
let o = new f(); // {a: 1, b: 2}
// f 함수의 prototype 속성 값들을 추가 하자.
f.prototype.b = 3;
f.prototype.c = 4;
// f.prototype = {b: 3, c: 4}; 라고 하지 마라, 해당 코드는 prototype chain 을 망가뜨린다.
// o.[[Prototype]]은 속성 'b'와 'c'를 가지고 있다.
// o.[[Prototype]].[[Prototype]] 은 Object.prototype 이다.
// 마지막으로 o.[[Prototype]].[[Prototype]].[[Prototype]]은 null이다.
// null은 프로토타입의 종단을 말하며 정의에 의해서 추가 [[Prototype]]은 없다.
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null
console.log(o.a); // 1
// o는 'a'라는 속성을 가지는가? 그렇다. 속성의 값은 1이다.
console.log(o.b); // 2
// o는 'b'라는 속성을 가지는가? 그렇다. 속성의 값은 2이다.
// 프로토타입 역시 'b'라는 속성을 가지지만 이 값은 쓰이지 않는다. 이것을 "속성의 가려짐(property shadowing)" 이라고 부른다.
console.log(o.c); // 4
// o는 'c'라는 속성을 가지는가? 아니다. 프로토타입을 확인해보자.
// o.[[Prototype]]은 'c'라는 속성을 가지는가? 가지고 값은 4이다.
console.log(o.d); // undefined
// o는 'd'라는 속성을 가지는가? 아니다. 프로토타입을 확인해보자.
// o.[[Prototype]]은 'd'라는 속성을 가지는가? 아니다. 다시 프로토타입을 확인해보자.
// o.[[Prototype]].[[Prototype]]은 null이다. 찾는 것을 그만두자.
// 속성이 발견되지 않았기 때문에 undefined를 반환한다.
위 예시 코드에서 설명하듯이,
우리가 객체. 속성 이름으로 참조를 시도하면 다음의 prototype chain과정을 거쳐 속성 값을 찾습니다.
__proto__가 null일 때까지,
__proto__를 참조해가며 "속성 이름"에 해당하는 속성을 찾습니다.
즉, 다음의 순서로 "속성이름"에 해당하는 속성을 찾아갑니다.
o
-> o.__proto__ === f.prototype
-> o.__proto__.__proto__ === Object.prototype
-> o.__proto__.__proto__.__proto__ === null
이것이 JS prototype chain을 이해해야 하는 이유입니다.
특별한 경우에만 쓰이는 constructor 속성
위 코드에서 충분히 보았듯이,
new Obj()는 Obj.prototype.constructor를 한 번도 호출하지 않습니다.
오직 Obj에 접근할 수 없는 특수한 상황에서,
obj를 통해 Obj 함수 생성자를 쓰고 싶을 때 사용하죠.
그래서 Obj.prototype.constructor의 초기값은 Obj 객체 자기 자신입니다!
우리가 Obj.prototype.constructor을 호출하지 않는다면,
우리 코드에서 Obj.prototype.constructor 는 필요하지 않습니다!
만약 class 문법을 사용하고, constructor를 명시하거나, 변경한다면 constructor 속성이 필요할 것입니다.
혼동되는 개념
- Obj.prototype : obj에게 상속시키고 싶은 객체
- obj.__proto__ : Obj.prototype를 가리키는 객체, obj의 부모(proto type)가 되는 객체
- Obj.constructor : Obj.__proto__. constructor === Object.prototype.constructor
- obj.constructor : obj.__proto__. constructor === Obj.prototype.constructor, 이것은 Obj 그 자체로 초기화되어있음.
- 속성의 가려짐(property shadowing) : prototype chain 에서 제일 얕은 속성이 더 깊은 속성들을 가림 (객체.속성으로 참조 시)
- new는 prototype의 constructor가 아닌 본 함수 객체 자체를 call함.
Note: 프로토타입 체인에서 한 객체의 메서드와 속성들이 다른 객체로 복사되는 것이 아님을 재차 언급합니다. — 위에서 보시다시피 체인을 타고 올라가며 접근할 뿐입니다.
-mozilla-
남기는 말
반드시 new가 내부적으로 어떻게 동작하는지,
ECMAScript 표준을 읽어보며 이해하시길 바랍니다.
constructor가 실제로 호출되지 않는 이유와, property shadawing이 일어나는 이유 등을 보지 않고 설명할 수 있다면,
JS의 prototype과 함수 생성자에 대해 충분히 이해하신 것입니다!
참고한 사이트:
'개발 > Web' 카테고리의 다른 글
CSS border이 안보여요! (0) | 2021.07.20 |
---|---|
JS closure (0) | 2021.03.31 |
세션과 쿠키, 상태, 로컬 스토리지 (0) | 2021.03.27 |
HTTP method 정리 (0) | 2021.03.16 |
HTTP status code, RFC 정리 (0) | 2021.03.16 |
댓글