如何围绕在DOM节点上存储数据(对象,自定义属性)进行编码

时间:2012-03-16 16:30:44

标签: javascript html dom

我想写一些纯粹的javascript来更好地理解它(我在“实践中”意识到jQuery等框架更加建议和适用,但这并不是关于如何使用框架,更多关于如何使用javascript工作和最佳实践)。

无论如何,我写了一些简单的javascript代码。我想创建一组按钮组,每次从一组{on,off}有一个状态,每个状态将映射到进入该状态时要触发的相应函数。主集中的每组按钮一次只能包含处于打开状态的一个按钮。这个概念类似于单选按钮的概念。 为什么不使用单选按钮呢?从语义上来说,它只是假设某些控制元素的按钮,但无论哪种方式,我想我可以,但问题不是真的那个。

问题是,为了解决这个问题,我在Javascript中通过button向特定的id元素添加了很多自定义属性。我正在做一些研究,发现这个question和这个question,关于在DOM节点(对象)上使用自定义属性。他们似乎主张反对这种做法,甚至可以说这样做会导致潜在的内存泄漏,具体取决于浏览器的实现。

但是,对于我创建的每个按钮,我需要跟踪很多属性,如果我扩展这个脚本,我可能需要添加更多。那么将它们存储在DOM节点上的最佳方法是什么,但仍然跟踪所有内容并能够在附加函数中使用 this 等等?

对我来说,如果不至少将一个井名间隔对象的引用存储到DOM节点button元素,这对我来说并不是很明显。

我能够从这个question看到jQuery有一些方法可以做到这一点,但我想知道如何使用纯javascript来完成这项工作。

以下是我正在使用的完整示例代码:

<!DOCTYPE html>
<html>
<head>
    <title>Button Test Script</title>

<script language="javascript" type="text/javascript">

window.button_groups = {};

function isset( type ) {
    return !(type==='undefined');   
}

function debug( txt ) { 
    if( !isset(typeof console) ) {
        alert( txt );   
    } else {
        console.log(txt);
    }
}

function img( src ) {
    var t = new Image();
    t.src = src;
    return t;       
}

function turnGroupOff( group ) {
    if( isset(typeof window.button_groups[group]) ) {               
        for( var i = 0; i < window.button_groups[group].length; i++ ) {         
            if( window.button_groups[group][i].toggle == 1)
                window.button_groups[group][i].click();
        }               
    }
}
/**
 * buttonId = id attribute of <button>
 * offImg = src of img for off state of button
 * onImg = src of img for on state of button
 * on = function to be fired when button enters on state
 * off = function to be fired when button enters off state
 */
function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);
    b.offImg = img( offImg );
    b.onImg = img( onImg );
    b.on = on;
    b.off = off;
    b.img = document.createElement('img');  
    b.appendChild(b.img);
    b.img.src = b.offImg.src;
    b.group = group;

    b.toggle = 0;

    b.onclick = function() {
        switch(this.toggle) {
        case 0:                                             
            turnGroupOff( this.group );
            this.on();
            this.toggle = 1;
            this.img.src = this.onImg.src;
            break;
        case 1:
            this.off();
            this.toggle = 0;
            this.img.src = this.offImg.src;
            break;
        }       
    }

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);                    

}


function init() {

    var on = function() { debug(this.id + " turned on") };

    newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
        on,
        function() { debug(this.id + " turned off"); }
        );
    newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off)"); }
        );

    newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off2)"); }
        );
    newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off3)"); }
        );

}

window.onload = init;
</script>

</head>

<body>


<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>

</body>
</html>

更新

jQuery的事情对我的目的来说有点矫枉过正。我不需要扩展任意元素。我很清楚如何完成特定于jQuery的操作(使用任意随机命名的属性存储缓存索引整数)。

我提前知道我需要扩展哪些主机元素,以及如何;我也可以/想要在HTML端为每个属性设置id属性。

因此,受jQuery设置的启发,我决定创建一个全局缓存变量,除了我将使用DOM节点的id属性作为我的缓存键。因为它应该是一个唯一的标识符(根据定义),并且我没有计划动态改变id,所以这应该是一个简单的任务。它完全将我的Javascript对象与DOM对象分离,但它确实使我的代码看起来更加丑陋,并且很难通过对data的多次调用来阅读。我提出以下修改:

<!DOCTYPE html>
<html>
<head>
    <title>Button Test Script</title>
<script language="javascript" type="text/javascript">

window.button_groups = {};

function isset( type ) { // For browsers that throw errors for !object syntax
    return !(type==='undefined');   
}

var c = { // For browsers without console support
    log: function( o ) {
        if( isset(typeof console) ) {
            console.log(o); 
        } else {
            alert( o );
        }
    },
    dir: function( o ) { 
        if( isset(typeof console) ) {
            console.dir(o);
        }
    }
};

function img( src ) { // To avoid repeats of setting new Image src
    var t = new Image();
    t.src = src;
    return t;       
}

var cache = {};
function data( elemId, key, data ) { // retrieve/set data tied to element id

    if(isset(typeof data)) {// setting data         
        if(!isset(typeof cache[elemId]))
            cache[elemId] = {};
        cache[elemId][key] = data;

    } else { // retreiving data
        return cache[elemId][key];      
    }   

}

var button_groups = {}; // set of groups of buttons

function turnGroupOff( group ) { // turn off all buttons within a group
    if( isset(typeof window.button_groups[group]) ) {               
        for( var i = 0; i < window.button_groups[group].length; i++ ) {         
            if( data(window.button_groups[group][i].id, 'toggle') == 1)
                window.button_groups[group][i].click();
        }               
    }
}


/**
 * buttonId = id attribute of <button>
 * offImg = src of img for off state of button
 * onImg = src of img for on state of button
 * on = function to be fired when button enters on state
 * off = function to be fired when button enters off state
 */
function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);  
    data( b.id, 'offImg', img( offImg ) );
    data( b.id, 'onImg', img( onImg ) );
    data( b.id, 'on', on );
    data( b.id, 'off', off );   
    var btnImg = document.createElement('img');
    btnImg.src = data( b.id, 'offImg' ).src;
    data( b.id, 'img', btnImg  );
    b.appendChild( btnImg );
    data( b.id, 'group', group );
    data( b.id, 'toggle', 0 );

    var click = function() {
        switch(data(this.id,'toggle')) {
        case 0:                                             
            turnGroupOff( data(this.id,'group') );
            (data(this.id,'on'))();
            data(this.id,'toggle',1);
            data(this.id,'img').src = data(this.id,'onImg').src;
            break;
        case 1:
            (data(this.id,'off'))();
            data(this.id,'toggle',0);
            data(this.id,'img').src = data(this.id,'offImg').src;
            break;
        }   

    }

    b.onclick = click;

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);                    
}


function init() {

    var on = function() { c.log(this.id + " turned on") };

    newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
        on,
        function() { c.log(this.id + " turned off"); }
        );
    newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off)"); }
        );

    newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off2)"); }
        );
    newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off3)"); }
        );

}


window.onload = init;


</script>       
</head>

<body>


<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>


</body>
</html>

更新2

我发现通过使用闭包的强大功能,我真的只需要存储一个“特殊”属性,即按钮所属的组。

我将newButton函数更改为以下函数,通过闭包,无需存储其他许多东西:

function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);  
    offImg = img( offImg );
    onImg = img( onImg );
    var btnImg = document.createElement('img');
    btnImg.src = offImg.src;
    b.appendChild( btnImg );
    data( b.id, 'group', group );
    var toggle = 0;

    var click = function(event) {
        switch(toggle) {
        case 0:                                             
            turnGroupOff( data(this.id,'group') );
            if( on(event) ) {
                toggle = 1;
                btnImg.src = onImg.src;
            }
            break;
        case 1:
            if( off(event) ) {
                toggle = 0;
                btnImg.src = offImg.src;
            }
            break;
        }   

    }

    b.onclick = click;

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);    

    b = null;
}

2 个答案:

答案 0 :(得分:1)

我发现{j}设计模式的this文章可能会给你一些想法。看看Prototype Pattern,它允许您跨实例重用方法。

答案 1 :(得分:1)

您要么扩展对象(对主机对象不好),要么像jQuery那样包装对象,使用包装对象来标识哈希表中的关联数据。本质上,您散列DOM节点并在哈希表中查找关联数据。当然,您仍然需要扩展主机对象,但是只添加一个您知道在浏览器中添加相当安全的属性,而不是一组任意属性。如果检查具有关联数据的元素,您可能会看到类似element.jQuery171023696433915756643的内容,其中包含该元素的内部存储索引。如果您感兴趣,我建议您阅读jQuery源代码,尤其是data()函数

data: function( elem, name, data, pvt /* Internal Use Only */ ) {
        if ( !jQuery.acceptData( elem ) ) {
            return;
        }

        var privateCache, thisCache, ret,
            internalKey = jQuery.expando,
            getByName = typeof name === "string",

            // We have to handle DOM nodes and JS objects differently because IE6-7
            // can't GC object references properly across the DOM-JS boundary
            isNode = elem.nodeType,

            // Only DOM nodes need the global jQuery cache; JS object data is
            // attached directly to the object so GC can occur automatically
            cache = isNode ? jQuery.cache : elem,

            // Only defining an ID for JS objects if its cache already exists allows
            // the code to shortcut on the same path as a DOM node with no cache
            id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
            isEvents = name === "events";

        // Avoid doing any more work than we need to when trying to get data on an
        // object that has no data at all
        if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
            return;
        }

        if ( !id ) {
            // Only DOM nodes need a new unique ID for each element since their data
            // ends up in the global cache
            if ( isNode ) {
                elem[ internalKey ] = id = ++jQuery.uuid;
            } else {
                id = internalKey;
            }
        }

        if ( !cache[ id ] ) {
            cache[ id ] = {};

            // Avoids exposing jQuery metadata on plain JS objects when the object
            // is serialized using JSON.stringify
            if ( !isNode ) {
                cache[ id ].toJSON = jQuery.noop;
            }
        }

        // An object can be passed to jQuery.data instead of a key/value pair; this gets
        // shallow copied over onto the existing cache
        if ( typeof name === "object" || typeof name === "function" ) {
            if ( pvt ) {
                cache[ id ] = jQuery.extend( cache[ id ], name );
            } else {
                cache[ id ].data = jQuery.extend( cache[ id ].data, name );
            }
        }

        privateCache = thisCache = cache[ id ];

        // jQuery data() is stored in a separate object inside the object's internal data
        // cache in order to avoid key collisions between internal data and user-defined
        // data.
        if ( !pvt ) {
            if ( !thisCache.data ) {
                thisCache.data = {};
            }

            thisCache = thisCache.data;
        }

        if ( data !== undefined ) {
            thisCache[ jQuery.camelCase( name ) ] = data;
        }

        // Users should not attempt to inspect the internal events object using jQuery.data,
        // it is undocumented and subject to change. But does anyone listen? No.
        if ( isEvents && !thisCache[ name ] ) {
            return privateCache.events;
        }

        // Check for both converted-to-camel and non-converted data property names
        // If a data property was specified
        if ( getByName ) {

            // First Try to find as-is property data
            ret = thisCache[ name ];

            // Test for null|undefined property data
            if ( ret == null ) {

                // Try to find the camelCased property
                ret = thisCache[ jQuery.camelCase( name ) ];
            }
        } else {
            ret = thisCache;
        }

        return ret;
    }