[컴][웹] jQuery 에서 OOP 형식으로 작성, class 만들기

javascript oop / javascript class



Class Template 1


아래의 template 은 DataTables 의 plugin 인 TableTools 의 source 에서 가져왔다.
var TableTools;

TableTools = function( oDT, oOpts )
{
    /* Santiy check that we are a new instance */
    if ( ! this instanceof TableTools )
    {
        alert( "Warning: TableTools must be initialised with the keyword 'new'" );
    }

    

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Public class variables
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

    /**
     * @namespace Settings object which contains customisable information for TableTools instance
     */
    this.s = {
        
        "that": this,
        "dt": dtSettings
    };



    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Public class methods
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

    /**
     * Retreieve the settings object from an instance
     *  @method fnSettings
     *  @returns {object} TableTools settings object
     */
    this.fnSettings = function () {
        return this.s;
    };


    /* Constructor logic */
    TableTools._aInstances.push( this );
    this._fnConstruct( oOpts );

    return this;
};


TableTools.prototype = {
    root : this,
    _selectBox : null,
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Public methods
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

    /**
     * Retreieve the settings object from an instance
     *  @returns {array} List of TR nodes which are currently selected
     *  @param {boolean} [filtered=false] Get only selected rows which are  
     *    available given the filtering applied to the table. By default
     *    this is false -  i.e. all rows, regardless of filtering are 
          selected.
     */
    "fnGetSelected": function ( filtered )
    {

        ...
        return out;
    },


    /**
     * Get the data source objects/arrays from DataTables for the selected rows (same as
     * fnGetSelected followed by fnGetData on each row from the table)
     *  @returns {array} Data from the TR nodes which are currently selected
     */
    "fnGetSelectedData": function ()
    {
        var out = [];
        var data=this.s.dt.aoData;
        var i, iLen;

        ...

        return out;
    }

   
};



/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Static variables
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/**
 * Store of all instances that have been created of TableTools, so one can look up other (when
 * there is need of a master)
 *  @property _aInstances
 *  @type    Array
 *  @default  []
 *  @private
 */
TableTools._aInstances = [];




/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Static methods
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

/**
 * Get an array of all the master instances
 *  @method  fnGetMasters
 *  @returns {Array} List of master TableTools instances
 *  @static
 */
TableTools.fnGetMasters = function ()
{
   ...
    return a;
};



/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * Constants
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

TableTools.buttonBase = {
    // Button base
    "sAction": "text",
    "sTitle": "",
    "sToolTip": ""
}



Class template 2


var parentFunction = function(options){
    var privateValue = 'privateValue';
    this.publicValue = 'publicValue';

    var vars = {
      privateVar: ‘original Value’,
      another: ‘test’
    }
     
    var root = this;
     
    this.construct = function(options){
        $.extend(vars , options);
    }
     
    this.childFunction = function(){
        alert(vars.privateVar);
    }
     
    this.construct(options);
}
var exampleObject = new parentFunction({ privateVar : ‘new Value’ });
exampleObject.childFunction();


위에서 root 는 나중에 inner function 등에서 parent class 에 접근하기 위해서 만들어놨다고 한다.[ref. 1]


Class.prototype

위의 방법(class template 2)의 단점은 class 를 new 할 때 마다 함수를 계속 생성해 낸다고 한다.[ref. 2]

그래서 ref.2 에서 정의하는 방법을 사용하는 것이 나을 듯 하다. 이것은 아래에서 이야기하는 TypeScript 에 의해 생성되는 부분과도 비슷하다.

function Apple (type) {
    this.type = type;
    this.color = "red";
}
 
Apple.prototype.getInfo = function() {
    return this.color + ' ' + this.type + ' apple';
};

그리고 이 방법을 사용하면 아래 "상속" 부분에서 제안하는 상속 방법을 사용하기 용이 하다.


private class

아래 "참고" 부분에도 적혀있지만, 따로 private 을 만들 수는 없다. 개인적으로 이경우는 python 의 convention 을 따르는 것이 좋을 듯 하다. 그래서 private method 에 해당하는 녀석들도 public 처럼 prototype 으로 만들어 놓고, 이름만 _(underscore) 로 시작하도록 하고, 사용하는 쪽에서 알아서 사용하도록 하는 것이다.


prototype

prototype 에 관한 설명은 ref. 4 에 설명이 되어있다. 모든 object 는 __proto__ 라는 property(단순히 변수라고 생각하자.) 를 갖는다고 한다. javascript 에서 contructor 가 호출될 때 이 __proto__ 가 prototype 을 가리키게 설정된다고 한다.



class 내부에서 handler 를 작성할 때

만약 java 에 익숙해서 java 처럼 class 내부에 event handler 를 만들고, event handler 안에서 this 를 사용하는 경우 원하는 대로 동작하지 않는다. 아래의 경우에 _defaultKeyDownHandler 는 document.getElementById(this._id) 가 가져가 버린 것이어서 _defaultKeyDownHandler 의 주인(owner)가 document.getElementById(this._id) 가 되어버린다. 그래서 this 는 document.getElementById(this._id) 가 된다.

this 가 변경되는 것에 대한 자세한 글은 see also. 1 을 참고하자.


function CandidateInput(id, url){
 this._id = id;

 document.getElementById(this._id).onkeydown = this._defaultKeyDownHandler;
}

CandidateInput.prototype._defaultKeyDownHandler = function(event){
 
 console.log(cc);
 console.log(this._id); // this 는 "document.getElementById(this._id)" 가 된다.
 
}


이런 경우에 해결책에 대해서는 ref. 3 에 좋은 대답이 있다. 위의 코드로 수정을 하자면, 아래처럼 function call 을 한 번 더 하도록 하는 것이다. 아래처럼 작성하면, function 의 주인(owner)가 여전히 class CandidateInput 이기 때문에 this 가 CandidateInput 이 된다.


function CandidateInput(id, url){
 this._id = id;
    var that = this;
 document.getElementById(this._id).onkeydown = function(event) {that._defaultKeyDownHandler(event)};
}

CandidateInput.prototype._defaultKeyDownHandler = function(event){
 
 console.log(cc);
 console.log(this._id); // this 는 "CandidateInput" 가 된다.
 
}




Class by TypeScript

그런데, 좀 잘 안되는 듯 하다.
차라리 typeScript 를 이용하는 것이 좋을 듯 하다.
http://www.typescriptlang.org/Playground/

아래는 typescript 를 통해 만든 class 를 이용한 예제이다. chrome extension 에 쓰인 코드라서 chrome 관련 API 들이 쓰였다.


// Copyright (c) 2013 Namh


var id = "a";
var pw = "W";




////////////////////////////////////////////////////////
//// TestClass class
////
var TestClass = (function(options){


    var vars = {
      another: "test",
    }
     
     
    function TestClass(){
        this.STATE_INIT = 0;
        this.STATE_LOGIN = 1;
        this.STATE_CDNPAGE = 2;

        this.state = this.STATE_INIT;
        this.rapUrl = "http://daum.net/";
        this.tabId = 0;
    }  

    TestClass.prototype.changeState= function(newState){
        this.state = newState;
    }


     
    TestClass.prototype.doLogin = function(tabId){

        this.changeState(this.STATE_LOGIN);
    }

    TestClass.prototype.goToCdnPage = function(tabId){

        this.changeState(this.STATE_CDNPAGE);
    }


    TestClass.prototype.extensionClickListener = function(tab){
        testClass.tabId = tab.id;
        testClass.state = testClass.STATE_INIT;
        
        chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
            
            // The reason of using testClass instead of this :
            // this is inner function so that the "this" does not work.
            if (tabId == testClass.tabId 
            && changeInfo.status === 'complete') {
            
                console.log("start updated listener");

                switch(testClass.state){
                    case testClass.STATE_INIT:
                        testClass.doLogin(tabId);
                        break;
                    case testClass.STATE_LOGIN:
                        testClass.goToCdnPage(tabId);
                        break;
                    default:
                        break;
                }
            }
        });
        chrome.tabs.update(tab.id, {url: testClass.rapUrl });
        
    }
     
    return TestClass;
})();


//
//  Start here ...
//
var testClass = new TestClass();
chrome.browserAction.onClicked.addListener(testClass.extensionClickListener);



참고사항

  1. private 이나 static 등을 굳이 만들 수는 있다.(즉, 작성자가 알아서 잘 써야 한다.) 
  2. static 변수는 prototype 을 이용해서 정의할 수 있다.


Static class

위의 link 글이 도움이 된다. javascript 에서 정의하는 모든 것들은 object 가 되는데, 만약 우리가
function Test(){ ... }

이런식으로 함수를 정의하면, Test class 가 만들어진 것이다. 이 class 는 기본적으로 prototype 을 갖고 있으며 우리는 Test.prototype 에 접근할 수 있게 되는 것이다. 그러므로 OOP 에서 static 함수는 간단하게

Test.myStaticMethod = function(){ ... }

이런 식으로 작성하면 된다.

Test.prototype.myMethod = function(){...}

그러면 위에서 작성하는 Test.prototype.myMethod 은 무엇일까? 이 prototype 이란 녀석은 function 을 정의하게 되면 자연적으로 생기는 변수이다.(보통 property 라고 부른다.)

자세한 사항은 아래를 참고하자.



Closure

closure 를 방금 발견(?) 했다. oop 적인 관점에서 programming 을 하기에는 closure 가 훨씬 적합해 보인다. [see also. 5 ] 예전에 TypeScript 도 봤지만, 그것보단 closure 가 좀 더 javascript 안으로 들어와 있어 훨씬 사용하기 좋은 듯 하다. 일단 아직 써보지 않았지만 구조적인 programming 에 훨씬 적합할 듯 보인다. see also. 6 에서 closure 에 대한 비판도 있으니 참고하자.

조금 써보고 난 후의 느낌은 Java 등의 oop 개발에 익숙한 사람이라면, closure 가 괜찮을 듯 하다. 자바의 package 나 import 에 해당하는 기능을 구현 해 주어서, 여러 file 로 class 를 만들고 필요한 곳에서 import 를 해서 사용하면 된다. 물론 결국 compiler 에 의해 하나로 묶이는 것이지만, 이렇게 묶을때 필요한 녀석들만 가져다가 묶어준다.

closure 는 compile 을 해야 이점이 있다. 하지만 compile 을 하지 않은 상태에서도 굳이 javascript code 를 커다란 한 파일로 만들어 놓지 않아도 되는 점은 좋다.

불편한 점은 이미 구축된 server side 에서 compile 하지 않은 code 를 이용하기가 조금 용이하지 않다. 하지만 그렇다고 해도 불가능한 것은 아니다. 자세한 부분은 ref. 5 를 참고하도록 하자.




상속 Inheritence

  1. JavaScript Inheritance Patterns by David Shariff
  2. Introduction to Object-Oriented JavaScript - JavaScript | MDN
미안하게도 어디서 가져온 코드인지를 까먹었다. 하지만 개인적으로 무척 마음에 드는 상속을 위한 code 여서 내가 사용했던 code 를 적어 놓는다.

/**********************************************************************
 *
 *  Constructor
 *
 **********************************************************************/ 
function UserNameChartParameter (subId, startDate, endDate, trafficType){
  ChartParameter.call(this, "testId", "pw", subId, startDate, endDate, trafficType);
}

/**********************************************************************
 *
 *  Inheritance
 *
 **********************************************************************/ 
UserNameChartParameter.prototype = Object.create(ChartParameter.prototype);
UserNameChartParameter.prototype.constructor = UserNameChartParameter;


Namespace


javascript 를 사용하다보니, namespace 를 사용하고 싶어졌다. 그래서 찾아보니 ref.6 에 좋은 방법이 있었다. self-executing anonymous function 을 이용하는 데 코드는 아래와 같다.(IIFE, Immediately-Invoked Function Expression)

(function( myspace, $, undefined ) {
    //Private Property
    var isHot = true;

   //Public Property
    myspace.ingredient = "Bacon Strips";

    //Public Method
    myspace.fry = function() {
        var oliveOil;

        addItem( "\t\n Butter \n\t" );
        addItem( oliveOil );
        console.log( "Frying " + myspace.ingredient );
    };

    //Private Method
    function addItem( item ) {
        if ( item !== undefined ) {
            console.log( "Adding " + $.trim(item) );
        }
    }   

    //-- static vars and methods
    var my2ndSpace = {
        igredient2 : "2nd Ingredient",
        fry: function(){
            console.log("fry");
        },
        fryAgain : function(){
            console.log("fryAgain");
        }
    }

    myspace.my2ndSpace = my2ndSpace;
    //--/ static vars and methods


}( window.myspace = window.myspace || {}, jQuery ));



CoffeeScript

지금은 이녀석을 주로 사용한다. coding 이 간결하고, 속도가 나는 느낌이랄까. 개인적으로 애용하고 있다. 위에서 얘기한 class 나 상속(extends) 같은 녀석을 제공한다.



See Also

  1. The this keyword : http://www.quirksmode.org/js/this.html
  2. javascript this : http://the-earth.tistory.com/147
  3. http://www.phpied.com/3-ways-to-define-a-javascript-class/, Singleton
  4. Custom JavaScript classes and packages using goog.provide() and goog.require()
  5. https://developers.google.com/closure/library/
  6. http://www.sitepoint.com/google-closure-how-not-to-write-javascript/
  7. Closure: The world's most misunderstood JavaScript library by gmosx, 09 Aug 2010
  8. ECMA-262-3 in detail. Chapter 7.2. OOP: ECMAScript implementation.


Reference

  1. http://www.scorchsoft.com/blog/how-to-write-object-oriented-javascript/
  2. [번역] 자바스크립트 클래스를 정의하는 3가지 방법 (3 ways to define a JavaScript class)
  3. Class methods as event handlers in javascript?
  4. The Javascript Guide to Objects, Functions, Scope, Prototypes and Closures
  5. [컴][웹] closure compiler 로 compiled javascript code 를 debugging 하는 방법
  6. How do I declare a namespace in JavaScript? - Stack Overflow

댓글 없음:

댓글 쓰기