Is there a preferred method of transferring 'intentionally generic' String or Array JavaScript methods to other objects?

StackOverflow https://stackoverflow.com/questions/17094710

Question

In the midst of doing some debugging, I came upon a situation in which it would be preferable to generalize the toUpperCase method. Here are a few ways that I came up with:

//Local
foo = [];
foo.toUpperCase = String(foo).toUpperCase;
foo.push("a");
foo.toUpperCase();

//Global
foo = [];
window.toUpperCase = function (obj) {return String(obj).toUpperCase();}
foo.push("a");
toUpperCase(foo);

//Prototype
foo = [];
Array.prototype.toUpperCase = String.prototype.toUpperCase;
foo.push("a");
foo.toUpperCase();

//Constructor Prototype
foo = [];
Array.prototype.constructor = String.prototype.toUpperCase;
foo.push("a");
foo.constructor();

//toString override
var foo = [];
foo.push("a");
var bar = String(foo);
foo.toString = function() { return bar.toUpperCase(); }
foo.toString();

Most of the String and Array methods have this disclaimer in the spec:

Therefore, it can be transferred to other kinds of objects for use as a method.

Is there a conventional approach to implementing this type of abstraction?

Was it helpful?

Solution

In case you need toUpperCase function to work only with arrays then you could extend Array class like that:

Array.prototype.toUpperCase = function () {
    return String(this).toUpperCase();
};

After that you can write:

var foo = [];
foo.push('a');
foo.toUpperCase();

OTHER TIPS

You don't need to do anything:

String.prototype.toUpperCase.call(anyObject)

If you want to shorten that, you'll need to bind call to toUpperCase:

var toUpperCase = Function.call.bind(String.prototype.toUpperCase);

Static methods attached to a global object is the previously implemented solution (less code, more data).

Generic methods have to be written so that they require this to only have a minimal set of methods. For example, most generic array methods only need this to provide length and indexed access.

There's a drawback when using Array.prototype.slice to convert arguments in an array: it prevents the browser JavaScript engine from performing optimizations

The recommended approach is to use Array.slice generic method – Array generic methods are specially implemented to be used on other types – but it is not a part of the ECMAScript specification

/**
 * Implementation of standard Array methods (introduced in ECMAScript 5th
 * edition) and shorthand generics (JavaScript 1.8.5)
 *
 * Copyright (c) 2013 Alex K @plusdude
 * http://opensource.org/licenses/MIT
 */
(function (global, infinity, undefined) {
    /*jshint bitwise:false, maxlen:95, plusplus:false, validthis:true*/
    "use strict";

    /**
     * Local references to constructors at global scope.
     * This may speed up access and slightly reduce file size of minified version.
     */
    var Array = global.Array;
    var Object = global.Object;
    var Math = global.Math;
    var Number = global.Number;

    /**
     * Converts argument to an integral numeric value.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-9.4
     */
    function toInteger(value) {
        var number;

        // let number be the result of calling ToNumber on the input argument
        number = Number(value);
        return (
            // if number is NaN, return 0
            number !== number ? 0 :

            // if number is 0, Infinity, or -Infinity, return number
            0 === number || infinity === number || -infinity === number ? number :

            // return the result of computing sign(number) * floor(abs(number))
            (0 < number || -1) * Math.floor(Math.abs(number))
        );
    }

    /**
     * Returns a shallow copy of a portion of an array.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.10
     */
    function slice(begin, end) {
        /*jshint newcap:false*/
        var result, elements, length, index, count;

        // convert elements to object
        elements = Object(this);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // calculate begin index, if is set
        if (undefined !== begin) {

            // convert to integer
            begin = toInteger(begin);

            // handle -begin, begin > length
            index = 0 > begin ? Math.max(length + begin, 0) : Math.min(begin, length);
        } else {
            // default value
            index = 0;
        }
        // calculate end index, if is set
        if (undefined !== end) {

            // convert to integer
            end = toInteger(end);

            // handle -end, end > length
            length = 0 > end ? Math.max(length + end, 0) : Math.min(end, length);
        }
        // create result array
        result = new Array(length - index);

        // iterate over elements
        for (count = 0; index < length; ++index, ++count) {

            // current index exists
            if (index in elements) {

                // copy current element to result array
                result[count] = elements[index];
            }
        }
        return result;
    }

    /**
     * Returns the first index at which a given element
     * can be found in the array.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.14
     */
    function indexOf(target, begin) {
        /*jshint newcap:false*/
        var elements, length, index;

        // convert elements to object
        elements = Object(this);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // calculate begin index, if is set
        if (undefined !== begin) {

            // convert to integer
            begin = toInteger(begin);

            // handle -begin, begin > length
            index = 0 > begin ? Math.max(length + begin, 0) : Math.min(begin, length);
        } else {
            // default value
            index = 0;
        }
        // iterate over elements
        for (; index < length; ++index) {

            // current index exists, target element is equal to current element
            if (index in elements && target === elements[index]) {

                // break loop, target element found
                return index;
            }
        }
        // target element not found
        return -1;
    }

    /**
     * Returns the last index at which a given element
     * can be found in the array.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.15
     */
    function lastIndexOf(target, begin) {
        /*jshint newcap:false*/
        var elements, length, index;

        // convert elements to object
        elements = Object(this);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // calculate begin index, if is set
        if (undefined !== begin) {

            // convert to integer
            begin = toInteger(begin);

            // handle -begin, begin > length - 1
            index = 0 > begin ? length - Math.abs(begin) : Math.min(begin, length - 1);
        } else {
            // default value
            index = length - 1;
        }
        // iterate over elements backwards
        for (; -1 < index; --index) {

            // current index exists, target element is equal to current element
            if (index in elements && target === elements[index]) {

                // break loop, target element found
                return index;
            }
        }
        // target element not found
        return -1;
    }

    /**
     * Executes a provided function once per array element.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18
     */
    function forEach(callback, scope) {
        /*jshint newcap:false*/
        var elements, length, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = 0; index < length; ++index) {

            // current index exists
            if (index in elements) {

                // execute callback
                callback.call(scope, elements[index], index, elements);
            }
        }
    }

    /**
     * Tests whether all elements in the array pass the test
     * implemented by the provided function.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.16
     */
    function every(callback, scope) {
        /*jshint newcap:false*/
        var elements, length, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = 0; index < length; ++index) {

            // current index exists
            if (index in elements &&

            // callback returns false
            !callback.call(scope, elements[index], index, elements)) {

                // break loop, test failed
                return false;
            }
        }
        // test passed, controversy began..
        return true;
    }

    /**
     * Tests whether some element in the array passes the test
     * implemented by the provided function.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.17
     */
    function some(callback, scope) {
        /*jshint newcap:false*/
        var elements, length, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = 0; index < length; ++index) {

            // current index exists
            if (index in elements &&

            // callback returns true
            callback.call(scope, elements[index], index, elements)) {

                // break loop, test passed
                return true;
            }
        }
        // test failed
        return false;
    }

    /**
     * Creates a new array with all elements that pass the test
     * implemented by the provided function.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.20
     */
    function filter(callback, scope) {
        /*jshint newcap:false*/
        var result = [], elements, length, index, count;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = count = 0; index < length; ++index) {

            // current index exists
            if (index in elements &&

            // callback returns true
            callback.call(scope, elements[index], index, elements)) {

                // copy current element to result array
                result[count++] = elements[index];
            }
        }
        return result;
    }

    /**
     * Creates a new array with the results of calling a provided function
     * on every element in this array.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.19
     */
    function map(callback, scope) {
        /*jshint newcap:false*/
        var result = [], elements, length, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = 0; index < length; ++index) {

            // current index exists
            if (index in elements) {

                // copy a return value of callback to result array
                result[index] = callback.call(scope, elements[index], index, elements);
            }
        }
        return result;
    }

    /**
     * Apply a function against values of the array (from left-to-right)
     * as to reduce it to a single value.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.21
     */
    function reduce(callback, value) {
        /*jshint newcap:false*/
        var elements, isset, length, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // status of the initial value
        isset = undefined !== value;

        // convert length to unsigned 32 bit integer
        length = elements.length >>> 0;

        // iterate over elements
        for (index = 0; index < length; ++index) {

            // current index exists
            if (index in elements) {

                // initial value is set
                if (isset) {

                    // replace initial value with a return value of callback
                    value = callback(value, elements[index], index, elements);
                } else {
                    // current element becomes initial value
                    value = elements[index];

                    // status of the initial value
                    isset = true;
                }
            }
        }
        // make sure the initial value exists after iteration
        requireValue(isset);
        return value;
    }

    /**
     * Apply a function against values of the array (from right-to-left)
     * as to reduce it to a single value.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.22
     */
    function reduceRight(callback, value) {
        /*jshint newcap:false*/
        var elements, isset, index;

        // convert elements to object
        elements = Object(this);

        // make sure callback is a function
        requireFunction(callback);

        // status of the initial value
        isset = undefined !== value;

        // index of the last element
        index = (elements.length >>> 0) - 1;

        // iterate over elements backwards
        for (; -1 < index; --index) {

            // current index exists
            if (index in elements) {

                // initial value is set
                if (isset) {

                    // replace initial value with a return value of callback
                    value = callback(value, elements[index], index, elements);
                } else {
                    // current element becomes initial value
                    value = elements[index];

                    // status of the initial value
                    isset = true;
                }
            }
        }
        // make sure the initial value exists after iteration
        requireValue(isset);
        return value;
    }

    /**
     * Returns true if an argument is an array, false if it is not.
     * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.3.2
     */
    function isArray(value) {
        return "[object Array]" === Object.prototype.toString.call(value);
    }

    /**
     * Tests if an argument is callable and throws an error if it is not.
     * @private
     */
    function requireFunction(value) {
        if ("[object Function]" !== Object.prototype.toString.call(value)) {
            throw new Error(value + " is not a function");
        }
    }

    /**
     * Throws an error if an argument can be converted to true.
     * @private
     */
    function requireValue(isset) {
        if (!isset) {
            throw new Error("reduce of empty array with no initial value");
        }
    }

    /**
     * Tests implementation of standard Array method.
     * @private
     */
    function supportsStandard(key) {
        var support = true;

        // a method exists
        if (Array.prototype[key]) {
            try {
                // apply dummy arguments
                Array.prototype[key].call(undefined, /test/, null);

                // passed? implemented wrong
                support = false;
            } catch (e) {
                // do nothing
            }
        } else {
            support = false;
        }
        return support;
    }

    /**
     * Tests implementation of generic Array method.
     * @private
     */
    function supportsGeneric(key) {
        var support = true;

        // a method exists
        if (Array[key]) {
            try {
                // apply dummy arguments
                Array[key](undefined, /test/, null);

                // passed? implemented wrong
                support = false;
            } catch (e) {
                // do nothing
            }
        } else {
            support = false;
        }
        return support;
    }

    /**
     * Assigns method to Array constructor.
     * @private
     */
    function extendArray(key) {
        if (!supportsGeneric(key)) {
            Array[key] = createGeneric(key);
        }
    }

    /**
     * Creates generic method from an instance method.
     * @private
     */
    function createGeneric(key) {
        /** @public */
        return function (elements) {
            var list;

            if (undefined === elements || null === elements) {
                throw new Error("Array.prototype." + key + " called on " + elements);
            }
            list = Array.prototype.slice.call(arguments, 1);
            return Array.prototype[key].apply(elements, list);
        };
    }

    /**
     * Assign ECMAScript-5 methods to Array constructor,
     * and Array prototype.
     */
    var ES5 = {
        "indexOf": indexOf,
        "lastIndexOf": lastIndexOf,
        "forEach": forEach,
        "every": every,
        "some": some,
        "filter": filter,
        "map": map,
        "reduce": reduce,
        "reduceRight": reduceRight
    };
    for (var key in ES5) {
        if (ES5.hasOwnProperty(key)) {

            if (!supportsStandard(key)) {
                Array.prototype[key] = ES5[key];
            }
            extendArray(key);
        }
    }
    Array.isArray = Array.isArray || isArray;

    /**
     * Assign ECMAScript-3 methods to Array constructor.
     * The toString method is omitted.
     */
    [
        "concat",
        "join",
        "slice",
        "pop",
        "push",
        "reverse",
        "shift",
        "sort",
        "splice",
        "unshift"

    ].forEach(extendArray);

    /**
     * Test the slice method on DOM NodeList.
     * Support: IE < 9
     */
    /*jshint browser:true*/
    if (document) {
        try {
            Array.slice(document.childNodes);
        } catch (e) {
            Array.prototype.slice = slice;
        }
    }

}(this, 1 / 0));

/*globals define*/
// Assumes all supplied String instance methods already present
// (one may use shims for these if not available)
(function() {
  'use strict';

  var i,
    // We could also build the array of methods with the following, but the
    //   getOwnPropertyNames() method is non-shimable:
    // Object.getOwnPropertyNames(String).filter(function(methodName) {
    //   return typeof String[methodName] === 'function';
    // });
    methods = [
      'quote', 'substring', 'toLowerCase', 'toUpperCase', 'charAt',
      'charCodeAt', 'indexOf', 'lastIndexOf', 'startsWith', 'endsWith',
      'trim', 'trimLeft', 'trimRight', 'toLocaleLowerCase',
      'toLocaleUpperCase', 'localeCompare', 'match', 'search',
      'replace', 'split', 'substr', 'concat', 'slice'
    ],
    methodCount = methods.length,
    assignStringGeneric = function(methodName) {
      var method = String.prototype[methodName];
      String[methodName] = function(arg1) {
        return method.apply(arg1, Array.prototype.slice.call(arguments, 1));
      };
    };

  for (i = 0; i < methodCount; i++) {
    assignStringGeneric(methods[i]);
  }
}());

References

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top