简体   繁体   中英

Conflict between Array.prototype and a chain select box plugin

Chained select box fiddle

Chained select box and an Array.prototype function fiddle

I had a chained select box and an Array.prototype function to combine two arrays into an associated array.They are for different use and unrelated. But when they are put together in the script, it gives me undefined is not a function pointing to this part this.forEach as the source of error. I ended up replacing the Array.prototype function with this one ( Example fiddle ):

function associate(keys, values){
    return keys.reduce(function (previous, key, index) {
        previous[key] = values[index];
        return previous
    }, {})
} 

I'm just curious why there is a conflict between the chained select box and the Array.prototype function?

Here's the code:

$(document).ready(function(){

var data = [
  {
    "bigcat": "Sport",
    "cat": "mainstream",
    "choice": "football"
  },
  {
    "bigcat": "Sport",
    "cat": "mainstream",
    "choice": "basketball"
  },
  {
    "bigcat": "Sport",
    "cat": "niche",
    "choice": "MMA"
  },
  {
    "bigcat": "Sport",
    "cat": "niche",
    "choice": "wrestling"
  }
]

var $select = $('select');var $option="";

$.each(data, function (index, i) {

  $option = $("<option/>").attr("value", i.choice).text(i.bigcat + "@" +( i.cat || "") +"@" +  i.choice);

  $select.append($option); 
});


$select.dynamicDropdown({"delimiter":"@"});

});

Array.prototype.associate = function (keys) {
  var result = {};

  this.forEach(function (el, i) {
    result[keys[i]] = el;
  });

  return result;
};

var animals = ['Cow', 'Pig', 'Dog', 'Cat'];
var sounds = ['Moo', 'Oink', 'Woof', 'Miao'];
console.dir(sounds.associate(animals));

Dynamic Drop down box Script

(function($) {
  $.dynamicDropdown = {
    /**
     * Escape quotation marks and slashes
     * @param {String} String to format
     * @return {String}
     */
    escapeQuotes : function(str) {
      return str.replace(/([""\\])/g, "\\$1");
    },

    /**
     * Build a <select> box from options
     * @param {Array} Options
     * @return {jQuery}
     */
    buildSelectDropdown : function(options) {
      var select = $(document.createElement("select"));
      var option = null;

      // Add options
      for (var i in options) {

        option = $(document.createElement("option"))
          .val($.isArray(options[i]) ? i : options[i])
          .html(i)
          .appendTo(select);
      }

      return select;
    }
  };

  $.fn.dynamicDropdown = function(options) {
    var settings = {
      "delimiter" : " ?",
      "className" : "dynamic-dropdown"
    };

    $.extend(settings, options);

    return $(this).each(function() {
      /**
       * Main dropdown (this)
       * @type jQuery
       */
      var mainDropdown = $(this);

      /**
       * Position of initial value of main dropdown
       * @type Array
       */
      var initialPosition = [];

      /**
       * Main array of all elements
       * @type Array
       */
      var data = [];

      /**
       * Array of all <select> boxes
       * @type Array
       */
      var selectElements = [];

      /**
       * Flag denoting whether the dropdown has been initialized
       * @type Boolean
       */
      var isInitialized = false;

      /**
       * Prepare a dropdown for use as a dynamic dropdown
       * @param {jQuery|string} Dropdown
       * @param {jQuery|HTMLElement} Sibling
       * @param {Number} Level
       * @param {Number} Position in the main array
       * @return {jQuery}
       */
      var prepareDropdown = function(dropdown, sibling, level, position) {
        return $(dropdown)
          .addClass(settings.className)
          .data("level", level)
          .data("position", position)
          .insertAfter(sibling)
          .each(buildDynamicDropdown)
          .change(buildDynamicDropdown);
      };

      /**
       * Initialize the dynamic dropdown <select> boxes
       * @return {jQuery}
       */
      var buildDynamicDropdown = function() {
        var level = $(this).data("level") + 1;
        var position = "";

        // Get the position in the main data array
        if (!isInitialized) {
          for (var i = 0; i < level; i++) {
            position += "[\"" + initialPosition[i] + "\"]";
          }
        } else {
          position = $(this).data("position") + "[\"" + $.dynamicDropdown.escapeQuotes($(this).val()) + "\"]";

          // Remove old <select> boxes
          for (var i = selectElements.length; i > level; i--) {
            selectElements.pop().remove();
          }
        }

        var selectionOptions = eval("data" + position);

        if ($.isArray(selectionOptions)) {
          // Build the next dropdown
          selectElements.push($.dynamicDropdown.buildSelectDropdown(selectionOptions));

          if (!isInitialized) {
            $(this).val(initialPosition[level - 1]);
          }

          prepareDropdown(selectElements[selectElements.length - 1], this, level, position);
        } else if (!isInitialized) {
          // Set the final value
          $("option:contains('" + initialPosition[level - 1] + "')", selectElements[selectElements.length - 1]).attr("selected", "selected");
          isInitialized = true;
        } else {
          // Set the value
          mainDropdown.val($(this).val());
        }

        return $(this);
      };

      // Build the dynamic dropdown data
      mainDropdown.children().each(function() {
        var parts = $(this).html().split(settings.delimiter);
        var name = "data";
        var value = null;

        // Set the initial position
        if ($(this).is(":selected")) {
          initialPosition = parts;
        }

        // Build the position of the current item
        for (var i in parts) {
if(typeof parts[i] != "string") continue;
          name += "[\"" + $.dynamicDropdown.escapeQuotes(parts[i]) + "\"]";
          value = eval(name);
          if (!value) {
            // Set the level to have an empty array to be filled
            eval(name + " = [];");
          } else if (!$.isArray(value)) {
            // Add data to the array
            eval(name + " = [" + eval(name) + "];");
          }
        }

        // Set the final index to have the value
        eval(name + " = \"" + $(this).val() + "\";");
      });

      // Build the dynamic dropdown
      selectElements[0] = $.dynamicDropdown.buildSelectDropdown(data);
      prepareDropdown(selectElements[0], this, 0, "");
    }).hide();
  };
})(jQuery);

Github version:

(function($) 
{

    $.fn.dynamicDropdown = function(options) {

        var settings = {
            "delimiter" : " » ",
            "className" : "",
            "levels"    : [ 
                {'markup':"{dd}",'class':false,'id':false,'disabled':false},
                {'markup':"{dd}"}
            ]
        };

        $.extend(settings, options);

        return $(this).each(function() {

            //the original dropdown element
            var mainDropdown = $(this);

            var defaultSelection = false;


            var levels = {};

            //insert dropdown into markup, and finally place it in the DOM, attaching events, etc.
            var insertSelectDropdown = function(dd, level, sibling, position){

                var markup = settings.levels[level] && settings.levels[level].markup ? settings.levels[level].markup : '{dd}';

                //to support markup both placing the dropdown within a container and without a container, 
                //its necessary to use a little silly dom magic
                var container = $('<div>'+settings.levels[level].markup.replace('{dd}',$('<div></div>').append(dd.addClass('ddlevel-'+level)).html())+'</div>').children()['insert'+position](sibling);

                var select = container.parent().find('select.ddlevel-'+level).removeClass('ddlevel-'+level);

                if (settings.levels[level]['class']){

                    select.addClass(settings.levels[level]['class']);
                }

                if (settings.levels[level].id){

                    select.attr('id',settings.levels[level].id);
                }

                if (settings.levels[level].disabled){

                    select.prop('disabled','disabled');
                }


                return select.data('level',level).data('container',container).data('levels',dd.data('levels')).change(updateDropdowns);
            }

            //produce markup for select element
            var buildSelectDropdown = function(options, selected) {

                var select = $('<select></select>').data('levels',options);

                // Add options
                $.each(options,function(index,value){

                    var option = $('<option></option>').html(index);

                    if (typeof(value) != 'object'){

                        option.val(value);
                    }

                    if (selected && index == selected){

                        option.attr('selected','selected');
                    }

                    select.append(option);

                });

                return select;
            };


            //the event function that runs each time a select input value changes
            var updateDropdowns = function(){

                var current = $(this).children(':selected').html();

                var options = $(this).data('levels')[current];

                //a non-object means this is the end of the line, set the value
                if (typeof(options) != 'object'){

                    mainDropdown.val($(this).val());
                }
                else {
                    //remove any dds after the one that just changed
                    var dd = $(this);
                    while (dd.data('next')){

                        dd = dd.data('next');

                        dd.data('container').detach();
                    }

                    var level = $(this).data('level') + 1;

                    //add new dds
                    $(this).data('next',insertSelectDropdown(buildSelectDropdown(options, defaultSelection[level]), level, $(this).data('container').last(), 'After').change());
                }

            };

            //build levels from initial dropdown
            mainDropdown.children().each(function() {

                var options = $(this).html().split(settings.delimiter); 

                if ($(this).is(":selected")){

                    defaultSelection = options;
                }       

                var level = levels;

                for (var i=0; i < options.length; i++) {

                    if (!level[options[i]]){

                        //either an option is an object pointing to other objects/values,
                        //or some other type value, indicating that the user has made a selection
                        level[options[i]] = ((i+1)==options.length) ? $(this).val() : {};
                    }

                    level = level[options[i]];
                }

            });

            //if no default selection, use first value
            if (!defaultSelection){

                defaultSelection = mainDropdown.children().first().html().split(settings.delimiter);
            }

            insertSelectDropdown(buildSelectDropdown(levels,defaultSelection[0]), 0, mainDropdown, 'Before').change();


        //hide initial dropdown
        }).hide();
    };
})(jQuery);

The issue is caused because the dynamicDropdown code iterates over an array with this type of syntax:

for (prop in array)

which includes all iterable properties of the array, not just array elements. In your case, that will include the associate method that you added to the array because it is iterable and that will cause a problem.

For a reference on iterating arrays, see Right and Wrong Ways to Iterate an Array .

This is a classic example for why arrays should never be iterated with the for (prop in array) syntax, but rather this

for (var i = 0; i < array.length; i++)

or this:

array.forEach()

or the more modern:

for (item of array)

and it's also a classic example of the risk you take when you add things directly to the Array.prototype as you've done.


You can work-around it as you have done by not extending the prototype of the Array object at all (the option you've already discovered). Or, you could make your new method be non-iterable by using Object.defineProperty() rather than direct assignment to the prototype (as long as you're OK with only IE9+ support) so the bad code in the dynamicDropdown library won't see your new method in its array iteration.

You could use Object.defineProperty() like this to make the method be non-iterable:

Object.defineProperty(Array.prototype, "associate", {
    writable: false,
    configurable: false,
    enumerable: false,
    value: function(keys) {
        var result = {};

        this.forEach(function (el, i) {
          result[keys[i]] = el;
        });

        return result;
    }
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM