'class'에 해당되는 글. 2건

  1. 2008/01/01 Javascript에서 Scope (3) - this, prototype, new (2)
  2. 2007/12/31 Javascript에서 Scope (2) - 응용


 이번에는 Javascript의 기본 문법에 있는 this, new, prototype이 어떻게 Scope에 영향을 주는지 알아보겠습니다.


#1. This는 Evalute되는 순간이 중요

각각의 경우에 aa값이 어떻게 출력될 지 예상 해보세요.

var aa = 10;

function check1() { this.aa.print("check1"); };

var obj = {

    thisref: this,

    aa: 20,

    check1: check1,

    check2: function() { this.aa.print("obj.check2"); },

    innerObj: {

        thisref: this,

        aa: 30,

        check1: check1,

        check3: function() { this.aa.print("obj.innerObj.check3"); }

        }

    };

 

check1();

obj.check2();

obj.check1();

obj.innerObj.check3();

obj.innerObj.check1();

(obj.thisref == window).print("obj.thisref == window");

> check1: 10
> obj.check2: 20
> check1: 20
> obj.innerObj.check3: 30
> check1: 30
> obj.thisref == window: true

 this는 Object내의 Method로 된 함수 내에서 현재 Object를 참조하는 역할을 합니다.
this는 함수가 어디서 선언 되었느냐 보다는 함수가 어느 Object에서 Evaluation 되었느냐가 중요하며, Evaluation되는 순간의 현재 Object를 참조 합니다.

 아무런 한정자가 없는 함수나 변수는 가장 최상위 Object인 window Object의 하위 Node에 속하게 됩니다. 따라서 check1()으로 실행될 때는 마치 window.check1()으로 호출되는 것과 같은 효과를 가지게 되며 this.aa는 window.aa가 되어서 10의 값을 가집니다.
 obj.check2()와 obj.innerObj.check3() 경우의 this는 현재 자신의 Object를 참조 합니다.
 obj.check1()과 obj.innerObj.check1()의 경우가 약간 혼동을 주는 케이스 입니다. 분명 이 함수들은 동일한 Context를 가지는 check1을 호출하게 되지만 Evalutation되는 순간 참조하는 Object가 각각 다르기 때문에 다른 this값을 가지게 됩니다. ( 만약 this.aa가 아닌 aa라면 check1을 호출하는 세 함수는 같은 값(10)을 가집니다. )
 경우에 따라서는 임의로 참조하는 Object를 변경하고 싶을 때가 있습니다. 이럴 때는 call()이나 apply()를 사용하면 됩니다.


#2. new는 Function의 prototype으로 Object를 만든다

Javascript에서는 Class를 만드는데 prototypenew를 제공합니다. new operator를 이용하면, Function type의 변수를 가지고 Object를 생성할 수 있습니다. 이때, 생성되는 Object는 Function Object의 prototype property의 값을 기본값으로 가지고 있습니다.

var aa = 10;

var check = function() { this.aa.print("check()"); };

var func = function() { this.check(); }

func.aa = 20;

func.check = function () { this.aa.print("func.check()"); };

func.prototype.aa = 30;

func.prototype.check = function () {
                          this.aa.print("func.prototype.check()"); };

 

func();

func.aa.print("func.aa");

func.check();

var newFunc = new func();

newFunc.aa.print("(new func).aa");

newFunc.check();

> check(): 10
> func.aa: 20
> func.check(): 20
> func.prototype.check(): 30
> (new func).aa: 30
> func.prototype.check(): 30

헷갈리지 말아야 할 부분은 new operator는 prototype 부분을 기반으로 하여서 Object를 만들어 준다는 것입니다. Function type도 Object type에 기반하기 때문에 다른 값들을 추가할 수 있는데 이는 new 로 생성된 object와는 아무런 연관이 없습니다.



#3. 참조가 바뀌는 전환점

prototype으로 지정된 값들은 여러 개의 Object가 생성이 되었을 때, 같은 값은 공유하는 것일까요? 각각 다른 값을 가지고 있는 것 일까요?

var func = function(name) { this.name = name; }

func.prototype.aa = 10;

func.prototype.check = function () { this.aa.print(this.name); };

 

newFunc1 = new func("o1");

newFunc2 = new func("o2");

func.prototype.aa = 20;

newFunc1.check();

newFunc2.check();

newFunc1.aa = 30;

func.prototype.aa = 40;

newFunc1.check();

newFunc2.check();

> o1: 20
> o2: 20
> o1: 30
> o2: 40

위의 결과 처럼 새로 생성한 Object에서 물려받은 값을 변경하지 않을 때에는 원래의 prototype의 값을 참조하다가 생성한 Object에서 값을 변경하면 이 참조가 깨지게 됩니다. 이는 변수 뿐만 아니라 함수도 적용되는 것이므로 객체의 상속이나 Polymorphism을 구현할 때 알고 있으면 좋습니다.



#4. this를 바꾸는 Call과 Apply

Call()과 Apply()는 파라미터로 Array를 받는지 여부만 차이가 있고 기능은 같은 함수 입니다. 이 함수들은 호출되는 순간의 this를 바꾸는데 사용합니다.

function check(str) { this.aa.print(str); };
var
obj = {

    aa: 10,

    check: check

    };

var func = function() { this.aa = 20; };

func.prototype.check1 = function(str) { obj.check(str); };

func.prototype.check2 = obj.check;

 

var newFunc = new func();

newFunc.check1("indirect");

newFunc.check2("direct");

newFunc.check2.call(obj, "call");

check.apply(newFunc, ["apply"]);

> indirect: 10
> direct: 20
> call: 10
> apply: 20


그럼 즐~Javascript  !!
Posted by U_Seung


이번에는 지난번에 다룬 기본기를 바탕으로 하여서 OOP개념에 기초가 되는 간단한 Class와 Object를 만들어 보았습니다.


#1. 아주 기본적인 Counter

function MakeIncCounter(initialNumber)
{
     return function() { return ++initialNumber; };
}

counter1 = MakeIncCounter(20);
counter2 = MakeIncCounter(10);
counter1();
counter2();
counter1();
counter1().print("Counter1");
counter2().print("Counter2");

> Counter1: 23
> Counter2: 12

1씩 단조 증가하는 Counter를 만들어 보았습니다.
여기에서 재미있게 볼 부분은 initialNumber 부분 입니다. C/C++에서는 함수가 끝나면 함수 내에서 사용하던 변수들이 모두 소멸되지만 Javascript에서는 참조하고 있는 곳이 있다면 함수가 종료 되었다 하더라도 해당 Context의 변수가 소멸되지 않습니다.




#2. 기능이 조금 있는 Counter

앞의 Counter는 단순히 1씩 밖에 더하지 못하는 Counter였습니다. 여기에 기능을 조금 더 추가한다면 다음과 같이 할 수 있겠습니다.

function MakeCounter(initialNumber, steps)
{
     initialNumber = initialNumber || 0;
    
steps = steps || 1;
    
return {
    
     StartingValue: initialNumber,
    
     GetValue: function() { return initialNumber },
    
     Inc: function() { initialNumber += steps; },
    
     Dec: function() { initialNumber -= steps; }
    
};
}

counter = MakeCounter(20, 3);
counter.Inc();
counter.Dec();
counter.Dec();
counter.StartingValue.print("Counter(S)");
counter.GetValue().print("Counter(C)");

> Counter(S): 20
> Counter(C): 17

여기에서는 GetStartingValue와 GetValue의 차이점을 보실 수 있습니다. StaringValue는 MakeCounter()가 return할 때의 initialNumber의 값을 받고, GetValue는 initialValue의 현재 값을 받습니다.



#3. 가독성 높이기

약간의 코드의 가독성을 고려한다면 아래와 같이 작성할 수도 있겠습니다.

function MakeCounter(initialNumber, steps)
{
     initialNumber = initialNumber || 0;
     steps = steps || 1;
     var name;
     var currentCounter = initialNumber;

    
// Methods
     function SetName(value) { name = value; }
     function GetValue() { return currentCounter; }
     function SetValue(value) { currentCounter = value; }
     function GetSteps() { return steps; }
     function Inc() { currentCounter += steps; }
     function Dec() { currentCounter -= steps; }
     function Print() { currentCounter.print(name); }
     return {
          "StartingValue": initialNumber,
          "SetName": SetName,
          "GetValue": GetValue,
          "SetValue": SetValue,
          "GetSteps": GetSteps,
          "Print": Print,
          "Inc": Inc,
          "Dec": Dec
     };
}

counter1 = MakeCounter(100, 20);
counter1.SetName("C1");
counter2 = MakeCounter();
counter2.SetName("C2");
counter2.SetValue(20);

counter1.Dec();
counter1.Dec();
counter2.Inc();
counter2.Inc();

counter1.Print();
counter2.Print();

> C1: 60
> C2: 22


약간 그럴싸한 Class가 만들어 졌습니다. 하지만 Method를 통한 Member variable의 접근은 가능하지만 Property를 통한 접근은 불가능한 상태 입니다. Member variable이 모두 private이라고 생각한다면 문제가 없겠지만 public한 경우도 있을 수 있으니 이를 확장 해보도록 하겠습니다.


#4. This Object 만들기.

 Object의 안과 바깥에서 공통적으로 접근할 수 있도록 하기 위해서 thisObj라는 Object를 생성하고, 여기에 Properties와 Methods를 추가하는 방법을 택하였습니다.

function MakeCounter(initialNumber, steps)

{

     initialNumber = initialNumber || 0;

     steps = steps || 1;

    

     var thisObj = {};

    

     // Properties

     thisObj.StartingValue = initialNumber;

     thisObj.name = "";

     thisObj.currentCounter = initialNumber;

 

     // Methods

     function SetName(value) { thisObj.name = value; }

     function GetValue() { return thisObj.currentCounter; }

     function SetValue(value) { thisObj.currentCounter = value; }

     function Inc() { thisObj.currentCounter += steps; }

     function Dec() { thisObj.currentCounter -= steps; }

     function Print() { thisObj.currentCounter.print(thisObj.name); }

     var methods = {

          "SetName": SetName,

          "GetValue": GetValue,

          "SetValue": SetValue,

          "Print": Print,

          "Inc": Inc,

          "Dec": Dec

     };

    

     for (var name in methods)

         thisObj[name] = methods[name];

     return thisObj;

}

counter = MakeCounter(100, 20);
counter.Inc();
counter.currentCounter -= 15;
counter.currentCounter.print("Counter");

> Counter: 105


 이제 정말 Class다운 모양을 갖춘 것 같습니다. 아쉽게도 Javascript의 언어적인 한계상 C++등의 Compiler단에서 지원하는 private, protected등의 Encapsulation을 깔끔하게 적용하기에는 다소 무리가 있지만 그런대로 OOP 프로그램을 할 수준은 아닌가 생각이 듭니다. 물론 private member와 private static member를 구현한 유명한 Technique[각주:1]들이 있는데 별로 권장할 만한 방법은 아닌 것 같습니다..



#5. Class 기능 확장하기.

OOP에서 가장 기본적인 개념 중 하나가 상속(Inheritance) 입니다. 이 것도 좀 흉내를 내본다면 아래와 같이 할 수 있습니다. 새로 만든 ImprovedCounter는 기존의 Counter가 가진 단순 Inc(), Dec()에 parameter를 지정하여서 특정값을 증가하거나 감소 할 수 있도록 하였습니다.

function MakeImprovedCounter(initialNumber, steps)

{

     var thisObj = MakeCounter(initialNumber, steps);

     thisObj.SetName("Improved");

    

     thisObj._superInc = thisObj.Inc;

     thisObj.Inc = function(steps) {

         if (steps == undefined) thisObj._superInc();

         else {

             thisObj.currentCounter += steps;

         }

     }

    

     thisObj._superDec = thisObj.Dec;

     thisObj.Dec = function(steps) {

         if (steps == undefined) thisObj._superDec();

         else {

             thisObj.currentCounter -= steps;

         }

     }

    

     thisObj.Initialize = function() {

          thisObj.SetValue( thisObj.StartingValue );

     }

    

     return thisObj;

}

var imCounter = MakeImprovedCounter(100, 20);
imCounter.Dec();
imCounter.Initialize();
imCounter.Inc();
imCounter.Inc(-10);
imCounter.Print();

> Improved: 110


이상 여기까지 만들어본 Class들은 Scope에 대한 이해를 돕기 위해서 Javascript에서 지원한 prototype, this, new의 개념을 사용하지 않고 만들었습니다. 하지만 실제 프로그래밍을 한다면 Javascript에서 지원하는 좋은 문법들을 적절히 도입하고, Prototype.js와 같은 라이브러리를 사용하는 것이 바람직 합니다. (참조: Defining classes and inheritance - prototype.js)


Javascript로 OOP를 구현하는 데 참조할 만한 글은....

Object Oriented Programming in Javascript
Classical Inheritance in JavaScript
Classes in Jscript - Part I
Classes in JScript – Part II: Instance Properties / Methods & Class Properties / Methods
Classes in JScript – Part III: Class Hierarchy and Data Encapsulation


Posted by U_Seung