vapour's blog

19Apr/110

使用动态类型、对象构造而非继承

今天读了OOP The Good Parts: Message Passing, Duck Typing, Object Composition, and not Inheritance,作者提倡使用动态类型(duck typing)、对象构造来代替继承。

关于动态类型的示例代码:

function bad (foo) {
    if ( ! (foo instanceof Bar) ) {
        throw new TypeError("This is an annoying restriction!");
    }
    return foo.baz(foo.quux(10));
}

function good (foo) {
    if ( !foo.baz || !foo.quux ) {
        throw new TypeError("We need foo to have baz and quux methods.");
    }
    return foo.baz(foo.quux(10));
}

下面是作者给出使用动态类型和对象构造的一个操作DOM的示例:

var quacksLike = function (obj, definition) {
    var k, ctor;
    for ( k in definition ) {
        ctor = definition[k];
        if ( ctor === Number ) {
            if ( Object.prototype.toString.call(obj[k]) !== "[object Number]" || isNaN(obj[k]) ) {
                return false;
            }
        } else if ( ctor === String ) {
            if ( Object.prototype.toString.call(obj[k]) !== "[object String]" ) {
                return false;
            }
        } else if ( ! (obj[k] instanceof ctor) ) {
            return false;
        }
    }
    return true;
};
Object.combine = function () {
    var newObj = {},
        i = 0,
        args = Array.prototype.slice.call(arguments),
        len = args.length;
    function copyProperty(k) {
        newObj[k] = args[i][k];
    }
    for ( ; i < len; i++ ) {
        Object.getOwnPropertyNames(args[i]).forEach(copyProperty);
    }
    return newObj;
};
window.$ = (function () {
    var undef, listen, customAPI;
    
    // Private function to turn 'thing' in to an array,
    // optionally slicing the first 'n' elems off.
    function toArray(thing, n) {
        return Array.prototype.slice.call(thing, n || 0);
    }

    // Private function to partially apply the first n arguments
    // to 'fn'.
    function curry (fn) {
        var args = toArray(arguments, 1);
        return function () {
            return fn.apply(this, args.concat(toArray(arguments)));
        };
    }

    // Private function to set the attribute 'attr' of a single
    // DOM node 'node' to 'val'.
    function nodeAttributeSetter (attr, val, node) {
        node.setAttribute(attr, val);
    }

    // Private function to get the value of the attribute 'attr'
    // on the DOM node 'node'.
    function nodeAttributeGetter (attr, node) {
        return node.getAttribute(attr);
    }

    // Do the work to determine the correct method for attaching
    // event listeners only once by using 'quacksLike' and an
    // immediately invoked function.
    listen = (function () {
        var w3c = {
                addEventListener: Function
            },
            ie = {
                attachEvent: Function
            };

        if ( quacksLike(document.body, w3c) ) {
            return function (event, fn, node) {
                node.addEventListener(event, fn, false);
            };
        } else if ( quacksLike(document.body, ie) ) {
            return function (event, fn, node) {
                node.attachEvent("on" + event, fn);
            };
        } else {
            throw new TypeError("Do not know how to attach event listeners.");
        }
    }());

    // This object is a mixin to be composed with the NodeList
    // results of querying the DOM by CSS selector. It provides
    // handy methods for getting and setting attributes,
    // attaching event listeners, and performing sub-queries by
    // CSS on these elements.
    customAPI = {
        attr: function (attr, val) {
            if ( val !== undef ) {
                this.forEach(curry(nodeAttributeSetter, attr, val));
                return this;
            } else {
                return this.length > 0 ? nodeAttributeGetter(attr, this[0]) : "";
            }
        },
        on: function (event, fn) {
            this.forEach(curry(listen, event, fn));
            return this;
        },

        find: function (selector) {
            var res = [];
            this.forEach(function (node) {
                res.push.apply(res, toArray(node.querySelectorAll(selector)));
            });
            return Object.combine(res, customAPI);
        }

    };

    return function (selector, context) {
        context = context || document;
        return Object.combine(Array.prototype, context.querySelectorAll(selector), customAPI);
    };

}());

//The mini DOM library might be used like this:

// The following two forms are equivalent.
// They select the same elements.
$("div").find("p");
$("div p");

// Disable all links.
$("a").on("click", function (event) {
  event.preventDefault();
  alert("Don't leave me all alone!");
});

// Get the id of the first paragraph.
$("p").attr("id");

// Disable all input elementss.
$(input").attr("disabled", "disabled");

// Do stuff with each matched element.
$("div.foo").forEach(function (el) {
  doStuffWith(el);
});