본문 바로가기

FRONT-END/JAVASCRIPT

자바스크립트의 프로토타입

반응형

자바스크립트는 객체지향 개념을 지원하기 위해 프로토타입을 사용한다.


프로토타입

prototype의 사전적 의미 '원형'

자바스크립트에 투영해서 무엇의 원형을 나타내는지 생각하면된다.

자바에서 객체를 class로 정의하지만

자바스크립트에서는 function으로 정의한다.


function Person(name, age);

this.name = name;

this.age = age;

}


var tomas  = new Person("tomas", 15);

alert(tomas.name);


this


함수호출하는 방법

1. 일반 함수로의 호출

2. 멤버 함수로의 호출

3. call() 함수를 이용한 호출

4. apply() 함수를 이용한 호출


1)

function say(something){

alert(something);

}

say("Hello world!"); 


2)

var unikys = {

say: function(something){

alert(something);

}

};


unikys.say("Hello world!");


3,4)

function say(something){

alert(something);

}


say.call(undefined, "Hello world!");

say.apply(undefined, ["Hello world!"]);



함수호출 방법에 따른 this

function whatsThis() {

return this.toString();

}


var unikys = {

what : whatsThis,

toString : function (){

return "[object unikys]";

}

};


console.log(whatsThis()); //[object Window]

console.log(unikys.what()); //[object unikys]

console.log(whatsThis.call()); //[object Window]

console.log(whatsThis.apply(unikys)); //[object unikys]

console.log(unikys.what.call(undefined));  //[object Window]

console.log(unikys.what.apply(unikys)); //[object unikys]


일반적으로 함수가 호출될때 내부적으로 call()함수를 변형되어서 처리 되는데, 이때 call() 함수의 첫번째 인자를 undefined로 넘겨주어 this의 기본값으로  window가 들어가게 된다.

call()이나 apply()함수를 이용한 함수 호출에서는 첫번째 인자로 설정한 객체가 this로 설정되며, 인자가 넘어가지 않을 때는 일반 함수 호출과 같이 글로벌 객체인 window가 this로 설정된다.


멤버함수 호출인 경우에는 내부적으로 call()함수를 호출할 때 첫번째 인자로 멤버함수를 보유한 객체를 넘겨준다. 첫번째 인자로 unikys가 넘어가기 때문에 this는 unikys가 된다.

하지만 같은 함수라도 멤버함수가 호출되는 방법이 다르면 this는 또 변경된다.

var newWhat = unikys.what;

console.log(newWhat()); //[object Window]

이렇게 this는 함수나 스코프 기반으로 결정되는 것이 아니라 호출방법에 따라 변경된다.

이러한 점은 특히 콜백함수를 인자로 넘기는 등의 동작이 일반적인 자바스크립트에서는 중요하게 알고 넘어가야 하는 개념이다.


OrdinaryCallBindThis ( FcalleeContextthisArgument )#

When the abstract operation OrdinaryCallBindThis is called with function object Fexecution context calleeContext, and ECMAScript value thisArgument, the following steps are taken:

  1. Let thisMode be the value of F's [[ThisMode]] internal slot.
  2. If thisMode is lexical, return NormalCompletion(undefined).
  3. Let calleeRealm be the value of F's [[Realm]] internal slot.
  4. Let localEnv be the LexicalEnvironment of calleeContext.
  5. If thisMode is strict, let thisValue be thisArgument.
  6. Else,
    1. If thisArgument is null or undefined, then
      1. Let globalEnv be calleeRealm.[[GlobalEnv]].
      2. Let globalEnvRec be globalEnv's EnvironmentRecord.
      3. Let thisValue be globalEnvRec.[[GlobalThisValue]].
    2. Else,
      1. Let thisValue be ! ToObject(thisArgument).
      2. NOTE ToObject produces wrapper objects using calleeRealm.
  7. Let envRec be localEnv's EnvironmentRecord.
  8. Assert: The next step never returns an abrupt completion because envRec.[[ThisBindingStatus]] is not "initialized".
  9. Return envRec.BindThisValue(thisValue).

thisArgument가 null 또는 undfined라면 this 값을 글로벌 this값으로 설정한다.

일반적으로 글로벌 변수인 window 객체로 thisArgument를 설정하지 않는 일반 함수나 첫 번째 인자를 null 또는 undfined로 call(), apply()함수를 호출하게 될때 this가 window 객체로 설정되는 이유이다.


new

자바스크립트에서는 new 키워드 뒤에 객체의 생성자가 바로 오게 되어있다.

이 생성자를 통해서 객체의 생성과 초기화가 한꺼번에 일어난다.


[[Construct]] ( argumentsListnewTarget)#

The [[Construct]] internal method for an ECMAScript Function object F is called with parameters argumentsList and newTargetargumentsList is a possibly empty Listof ECMAScript language values. The following steps are taken:

  1. Assert: F is an ECMAScript function object.
  2. Assert: Type(newTarget) is Object.
  3. Let callerContext be the running execution context.
  4. Let kind be F's [[ConstructorKind]] internal slot.
  5. If kind is "base", then
    1. Let thisArgument be ? OrdinaryCreateFromConstructor(newTarget"%ObjectPrototype%").
  6. Let calleeContext be PrepareForOrdinaryCall(FnewTarget).
  7. Assert: calleeContext is now the running execution context.
  8. If kind is "base", perform OrdinaryCallBindThis(FcalleeContextthisArgument).
  9. Let constructorEnv be the LexicalEnvironment of calleeContext.
  10. Let envRec be constructorEnv's EnvironmentRecord.
  11. Let result be OrdinaryCallEvaluateBody(FargumentsList).
  12. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  13. If result.[[Type]] is return, then
    1. If Type(result.[[Value]]) is Object, return NormalCompletion(result.[[Value]]).
    2. If kind is "base", return NormalCompletion(thisArgument).
    3. If result.[[Value]] is not undefined, throw a TypeError exception.
  14. Else, ReturnIfAbrupt(result).
  15. Return ? envRec.GetThisBinding().


처음에 함수 객체 F의 내부적인 생성자 함수가 호출되면 this를 newTarget의 prototype을 가지는 객체로 새로 생성한 다음 여기서 생성된 객체를 this로 설정한다.

함수호출을 위한 다양한 실행환경을 설정한 다음. 새로 생성된 객체를 this로 정의하여 F를 생성자 함수로 호출된 이자를 사용하여 새로운 객체를 초기화한다.

그리고 초기화된 this값을 함수 호출의 결과로 반환하는 것으로 자바스크립트에서의 객체 생성과 초기화를 마친다.


prototype

다른 객체들과 공유되는 속성을 제공하는 객체이다.

생성자가 객체를 생성할 때, 객체는 내부적으로 생성자의 prototype 속성을 활용하여 속성들의 레퍼런스를 참조한다.

생성자의 prototype 속성은 constructor.prototype과 같은 표현식으로 프로그램 내에서 접근이 가능하고, 객체의 prototype에 추가된 속성은 상속 받는 객체들까지 함께 공유된다.

또 다른 방법으로 명시적으로 어떠한 prototype을 사용할지 결정할 수 있는 Object.create함수를 활용하여 새로운 객체를 생성할 수 있다.

prototype#

object that provides shared properties for other objects

NOTE

When a constructor creates an object, that object implicitly references the constructor's prototype property for the purpose of resolving property references. The constructor's prototype property can be referenced by the program expression constructor.prototype, and properties added to an object's prototype are shared, through inheritance, by all objects sharing the prototype. Alternatively, a new object may be created with an explicitly specified prototype by using the Object.create built-in function.


표준을 보면 생성자를 통해 객체를 생성할 때 생성자의 prototype 속성을 내부적으로 참조하며, 이렇게 생성된 객체들 간의 prototype은 공유된다고 한다.

이를 상속하는 모든 객체에도 공유된다.

생성자의 속성인 prototype 또한 하나의 객체이다.


function Person(name, age){

this.name = name;

this.age = age;

}


Person.prototype.getName = function(){

return this.name;

};

Person.prototype.getAge = function(){

return this.age;

};


var tomas = new Person("tom",16);

var mike = new Person("mike", 17);


console.log(tomas.getName());

console.log(tomas.getAge());

console.log(mike.getName());

console.log(mike.getAge());


Peson.prototype 코드는 constructor.prototype 코드로 접근할수 있다.

즉 Person이라는 생성자의 prototype 속성을 설정하고 있는 것이다.

이후 새로 생성된 tomas 와 mike 객체는 내부적으로 prototype 객체를 참조하여 prototype 객체가 가지고 있는 getName()과 getAge() 함수를 사용할 수 있다.

생성자를 통해서 생성한 객체들이 prototype을 공유한다.


자바스크립트에서 프로토타입은 또 다른 객체(Object)이다.


이미 생성자를 통해 생성된 객체라도 나중에 생성자의 프로토타입에 새로운 속성을 추가할 수 있고 추가된 속성은 모든 객체에 공유한다.

Person.prototype.introduce = function (){

console.log("my name is " + this.name + " and "+ this.age +" year old");

}

tomas.introduce(); // my name is tom and 16 year old


프로토타입에 함수가 아닌 변수도 추가하여 공유할 수 있다.

Person.prototype.gender = "male";

console.log(tomas.gender);

console.log(mike.gender);


mike 객체의 gender 변경시

mike.gender = "female";

console.log(mike.gender);

console.log(tomas.gender);

console.log(Person.prototype.gender);



프로토타입과 생성자


MakeConstructor (F [ , writablePrototypeprototype ])#

The abstract operation MakeConstructor requires a Function argument F and optionally, a Boolean writablePrototype and an object prototype. If prototype is provided it is assumed to already contain, if needed, a "constructor" property whose value is F. This operation converts F into a constructor by performing the following steps:

  1. Assert: F is an ECMAScript function object.
  2. Assert: F has a [[Construct]] internal method.
  3. Assert: F is an extensible object that does not have a prototype own property.
  4. If the writablePrototype argument was not provided, let writablePrototype be true.
  5. If the prototype argument was not provided, then
    1. Let prototype be ObjectCreate(%ObjectPrototype%).
    2. Perform ! DefinePropertyOrThrow(prototype"constructor", PropertyDescriptor{[[Value]]: F, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: true }).
  6. Perform ! DefinePropertyOrThrow(F"prototype", PropertyDescriptor{[[Value]]: prototype, [[Writable]]: writablePrototype, [[Enumerable]]: false, [[Configurable]]: false}).
  7. Return NormalCompletion(undefined).

prototype에 constructor 속성으로 F를 설정하고

함수 객체인 F에 prototype의 속성을 설정한다.

따라서 F 객체는 prototype을 가지게 되고, prototype객체는 생성자인 F객체의를 참조하는 순환구조를 가지게 된다.


console.dir(Person);

console.dir(tomas.constructor);


function Person이 F이고, Person.prototype 속성에 대입한 객체가 proto

F에서는 prototype이라는 속성으로 접근

proto에서는 constructor라는 속석으로 F에 접근할수 있는 순환구조이다.


new Person()으로 객체를 생성할 때

proto라는 프로토타입에 정의된 getName()과 getAge() 함수는 새로운 Perso 객체에 들어가지 않고

계속 proto에 남아있다.

그리고 새로운 Person 객체에 F를 호출해서 초기화하면 F안에서 this.name과 this.age를 설정함으로써, 초기화된 Person 객체가 name과 age 속성을 가지게 된다.


새로운 Person 객체와 proto 사이를 내부 링크 Implicit link로 연결한데는 이유가 있다.

새로 생성된 Person 객체안에서 this.getName이나 this.getAge와 같은 코드로 proto 안에 있는 속성들에 접근 할수 있다.

하지만 똑같이 this.getName으로 속성값을 수정하려면 proto의 getName이 수정되는 것이 아니라 this가 가리키는 새로운 Person 객체에 새로운 속성을 부여되기 때문이다.



프로토타입 변수값을 수정

function Person() {}; 

var toams = new Person(), 

mike = new Person(); 

Person.prototype.gender = "male";

mike .gender = "female";

console.log(mike.gender); // female


프로토타입에 있는 변수값을 직접 변경

function Person() {}; 

Person.prototype.gender = "male"; 

var tomas = new Person(), 

mike = new Person(); 

console.log(tomas.gender); // male

console.log(mike.gender); // male


Person.prototype.gender = "female"; 


console.log(tomas.gender); // female

console.log(mike.gender); // female



객체 내의 속성 탐색순서

객체가 가지는 변수에 접근하려면, 일단 객체 자체의 속성부터 찾은 다음, 있으면 그 속성을 참조하고 없으면 자신의 프로토타입에 저장된 속성들을 검사한다.

그리고 거기에도 없으면 undefined를 반환한다.

여기서 프로토타입은 다른 객체가 될 수 있으므로 프로토타입을 다른 F와 프로토타입을 가지는 객체로 설정하면, 그것이 바로 상속의 기본적인 형태가 된다.

자바스크립트에서 모든 객체는 프로토타입이라는 다른 객체(또는 null)을 가리키는 내부링크를 가지고 있따. 한 개의 프로토타입 또한 프로토타입을 가지고 있고, 이것이 반복되다 null을 프로토타입으로 가지는 객체에서 끝난다. 이와 같은 객체들의 연쇄를 프로토타입 체인(prototype chain)이라고 부른다.


프로토타입 체인

function Car() { 

this.wheel = 4; 

this.beep = "BEEP!"; 

};

Car.prototype.go = function () { 

alert(this.beep); 

}; 

function Truck() { 

this.wheel = 6; 

this.beep = "HONK!"; 

}; 

Truck.prototype = new Car();


function SUV() { 

this.beep = "WANK!"; 

}; 


SUV.prototype = new Car(); 


var truck = new Truck(), 

suv = new SUV();


console.log(truck.wheel); // 6

console.log(suv.wheel);  // 4

console.log(truck.beep); //HONK!

console.log(suv.beep); //WANK!

truck.go();

suv.go();


truck.go();

1. truck 객체에 go 속성이 있는지 검사

2. Truck.prototype(new Car())에 go속성이 있는지 검사

3. new Car()에 go 속성이 없으면,  new Car()로 생성된 객체의 프로토타입인 Car.prototype에 go 속성이 있는지 검사

4. Car.prototype.go 속성 참조


객체의 속성을 접근할 때 객체와 프로토타입을 재귀로 검사하는 단계를 거쳐서 속성을 참조한다.

이처럼 속성이 프로토타입을 따라서 참조하게 되면 다시 재조명해야 할 함수가 하나 있다.

바로 Obejct 객체에 기본으로 들어있어서 모든 객체가 가지고 있는 hasOwnProperty()함수이다.

이 함수를 사용하면 접근하려는 속성이 현재 객체에 포함된 것인지 아닌지를 구분한다.

외부에서 봤을 때는 똑같이 접근할지라도 해당 속성이 객체 자체의 속성인지 프로토타입 체인에 있는 속성인지 구분할 수 있다.


hasOwnProperty() 함수


function Person(name, blog) { 

this.name = name; 

this.blog = blog; 

Person.prototype.getName = function () { 

return this.name; 

}; 

Person.prototype.getBlog = function () { 

return this.blog; 

};

var unikys = new Person("unikys", "unikys.tistory.com");


for (var prop in unikys) { 

console.log("unikys[" + prop + "] = " + unikys[prop]);

}


결과

unikys[name] = unikys

unikys[blog] = unikys.tistory.com

unikys[getName] = function () { 

return this.name; 

}

unikys[getBlog] = function () { 

return this.blog; 

}


unkiys의 속성인 name과 blog만 출력하는 것이 아니라 프로토타입에 있는 getName, getBlog 까지 모두 출력된다.


for (var prop in unikys) { 

if(unikys.hasOwnProperty(prop)){

console.log("unikys[" + prop + "] = " + unikys[prop]);

}

}

프로토타입에서 가져오는 속성들을 걸러낼수 있다.




프로토타입의 장단점


생성자에서 모든 속성을 설정하는 경우

function Person(name, blog) { 

this.name = name; 

this.blog = blog;

this.getName = function() {

return this.name;

};

this.getBlog = function() {

return this.blog;

};

}


var unikys = new Person("unikys", "unikys.blog.com");

console.log(unikys.getName());

console.log(unikys.getBlog());



생성자를 사용하여 많은 객체를 중복해서 사용하려면 프로토타입이 좋고, 생성자를 사용해서 객체를 조금만 생성한다면 그냥 속성을 부여하는 것이 좋다.

왜냐하면 프로토타입은 모든 객체가 한 객체를 공유하고 있어서 메모리를 하나만 사용하지만, 생성자 안에서 속성으로 부여하는 방식은 객체를 생성할 때마다 새로운 function을 생성하기 때문이다.

따라서 객체를 여러개 생성해야 하는 때는 프로토타입을 사용하는 방법이 메모리상 유리하다.


또한 실시간으로 여러 객체의 공통 속성의 내용을 수정하고자 할 때, 프로토타입을 사용했다면 프로토타입 속성만 수정하면 모든 객체에 수정한 내용이 반영되지만, 생성자 안에서 속성을 부여했다면 루프를 돌면서 모든 객체를 다시 설정해야 하는 문제가 있다.


또 다른 문제점이 있다면 hasOwnProperty()함수에서 변수와 속성을 구분할 수 있는 기능이 무의미해질수 있다는 점이다.

hasOwnProperty() 함수를 사용하더라도 unikys.getName()과 unikys.getBlog() 함수는 생성자에서 직접 설정한 속성이라서 출력된다.

따라서 hasOwnProperty() 함수 때문에 일반적으로 생성자 안에서 객체 속성들만 부여하는 경우가 많고, 프로토타입에는 함수 속성을 부여하는 경우가 많다.


단점

프로토타입의 활용법이해하기 힘들다.

프로토타입 체인을 따라서 검색하는 속성 탐색시간이 늘어난다.


따라서 해당 객체에 자주 접근해서 참조해야하는 속성이라면 프로토타입에 있는 값을 기본값으로만 활용하고, 객체 자체의 속성으로 새로 추가하여 프로토타입 체인 탐색을 최소화하는 것이 프로토타입과 프로토체인을 활용하는 현명한 방법이다.



출처-속깊은 자바스크립트 양성익 지음

반응형