'dynamic scope'에 해당되는 글. 2건

  1. 2007/12/31 Javascript에서 Scope (2) - 응용
  2. 2007/12/29 Javascript에서 Scope (1) - 기본 (6)


이번에는 지난번에 다룬 기본기를 바탕으로 하여서 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



Javascript가 가장 어렵다고 느껴질때가 Scope가 헷갈릴 때가 아닐까 생각이 듭니다.
웬만큼 프로그램좀 짜봤다 싶은 사람도 헷갈리기 십상인게 Javascript Scope 입니다.

시간을 내어서 간단히 정리해보았습니다. 말로 정리하는 것은 정리하는 것도 어렵고, 나중에 보기도 힘들어서 예제를 중심으로 정리 하였습니다. 다른 분들도 도움이 되시기 바랍니다.



#0. 테스트를 위한 함수 추가
Object.prototype.print =
     function (comment) {
          comment = comment || "";
          document.writeln(comment+": "+this);
     };

실전에서 사용하기에는 좋은 방법은 아니지만, 테스트 코드 작성의 편의성을 위해서 아무것이나 print()를 하면 화면에 출력되도록 하는 함수를 만들었습니다. 아래 예제에서 사용되는 함수이니 참조하세요.








#1. var로 선언하지 않은 변수는 이전 Context를 참조한다.

function ScopeTest()

{

    score = 50;

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest();

score.print("3rd");

1st: 10
2nd: 50
3rd: 50



function ScopeTest()

{

    var score = 50;

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest();

score.print("3rd");

1st: 10
2nd: 50
3rd: 10


두 결과가 다르게 나오는 것은 var가 붙고 안 붙고 차이 입니다.

Javascript는 var로 선언한 변수 경우, Execution Context(이하 줄여서 Context)[각주:1]가 생성할 때 해당 Context에 변수를 생성 합니다. 만약 해당 Context에 선언되지 않은 변수명를 사용하면, 이전 Context들을 차례로 뒤지면서 해당 변수명을 탐색 합니다. 최상위 Context에서도 변수를 발견하지 못하면 변수는 undefined 값을 가지고 되고, assignment 구문에서 사용되었다면 해당 변수명을 가진 변수를 새롭게 생성합니다. ( 이때, 변수는 최상위 Context에 생성 됩니다.)

Javascript는 함수 호출이 일어나게 되면 새로운 Context를 하나 생성합니다. 그리고 이 Context는 함수가 선언된 곳 하위에 Chian으로 차례로 연결됩니다. 위의 두 번째 예제에서는 ScopeTest()라는 함수가 호출될 때, 새로운 Context가 생성이 되었고 이는 가장 최상위 Context의 하위에 연결 되었습니다. 이 곳에서 새로운 변수 score를  선언 하였기 때문에 최상위 Context의 score와 다른 변수가 만들어 졌습니다.






#2. 이전 Context에 있는 변수를 사용 하려면 ??

function ScopeTest()

{

    score = 20

    var score = 50;

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest();

score.print("3rd");

1st: 10

2nd: 50

3rd: 10





function ScopeTest(context)

{

    context.score = 20

    var score = 50;

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest(this);

score.print("3rd");

1st: 10
2nd: 50
3rd: 20


Javascript는 해당 Context 내의 위치에서 어디라도 한번이상 변수가 선언(var)되면, Context를 생성할 때 변수도 같이 생성 합니다. (즉, 함수 내부에서 var score를 가장 아래에서 선언을 하나 가장 위에서 선언하나 동일한 효과를 지닙니다.) 따라서, 첫번째 예제는 score 변수가 새롭게 생성 도기 때문에 최상위 Context에 있는 score를 접근할 수 없는 것입니다.

 코드의 가독성을 높이기 위해서는 함수 블럭의 최상위에서 변수 선언을 하는 것이 좋습니다.
 C나 C++처럼 쓰고 싶은 상황이라면, 새 블럭마다 사용하는 변수를 모두 선언 해주는 것이 좋습니다.

이전의 Context에 있는 변수를 접근하려면 어떻게 해야 할까요? 일단 이전의 Context에 접근할 수 있는 직접적인 방법은 없습니다. 위의 두번째 예제에서는 this를 써서 접근하는 것과 같은 흉내를 내보이지만 사실상 this는 (Object를 생성하지 않은 상황이라서) window와 동일한 효과를 지니며, ScopeTest() 함수 내부에서도 역시 context.scorewindow.score, this.score와 모두 동일하다고 볼 수 있습니다. ( 여기에 대한 자세한 사항은 깊게 언급할 수 없어서 넘어 갑니다. )








#3. 함수 파라미터로 선언한 변수들..

function ScopeTest(score)

{

    score = 20;

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest(score);

score.print("3rd");

1st: 10
2nd: 20
3rd: 10


함수의 파라미터로 선언된 변수들은 함수가 호출될 때 생성되는 Context에 새롭게 생성 됩니다. 따라서 위의 예제에서는 ScopeTest() 함수가 호출되는 새로운 Context에 score 변수가 생성되기 때문에 var를 붙이지 않더라도 기존 Context의 변수와 분리됩니다.








#4. Context를 하나 더 만들기...

function ScopeTest1()

{

    var score = 20;   

    ScopeTest2();

    score.print ("2nd");

}

 

function ScopeTest2()

{

    score = 30;

}

 

var score = 10;

score.print("1st");

ScopeTest1();

score.print("3rd");

1st: 10
2nd: 20
3rd: 30

function ScopeTest1()

{

    var score = 20;   
    var
ScopeTest2 = function () { score = 30; };

    ScopeTest2();

    score.print("2nd");

}

 

var score = 10;

score.print("1st");

ScopeTest1();

score.print("3rd");

1st: 10
2nd: 30
3rd: 10


위의 두 예제가 어떻게 다른지 유심히 보시면 아실 것 같습니다. Context가 만들어 질 때, Scope Chain이 어떻게 연결되는지를 판단할 수 있어야 합니다. 중요한 것은 함수가 '어디서 호출 되느냐'가 아니라 함수가 '어디서 선언 되느냐'입니다.

두 번째 부분을 아래와 같은  InnerFunction 형태로도 나타낼 수 있습니다.

function ScopeTest1()
{
    var score = 20;
   
function ScopeTest2()
    {
        score = 30;
    }
    ScopeTest2();
    score.print("2nd");
}


좀 더 직관적으로 볼 수 있도록 도식화 해 보았습니다.
 
사용자 삽입 이미지


 위의 그림과 같이 위의 두 예제에서 ScopeTest2()에서 score 변수는 다른 Context의 것을 참조 합니다. (참조: window는 최상위 Context에 위치한 Global Object를 나타냅니다.  )






더 세부적이고, 기술적인 글은 Microsoft JScript 블로그의 Scope chain of JScript Functions을 참조하시면 도움이 되실 겁니다.


  1. ECMAScript Language Specification a-10 [본문으로]
Posted by U∙Seung