Array.prototype和链选择框插件之间的冲突

时间:2014-08-25 08:30:47

标签: javascript jquery html arrays

Chained select box小提琴

Chained select box and an Array.prototype function小提琴

我有一个链式选择框和一个Array.prototype函数将两个数组组合成一个相关的数组。它们用于不同的用途和不相关的。但是当它们在脚本中放在一起时,它会让undefined is not a function指向此部分this.forEach作为错误来源。我最终用这个(Example fiddle)替换了Array.prototype函数:

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

我只是好奇为什么链式选择框与Array.prototype功能之间存在冲突?

以下是代码:

$(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));

动态下拉框脚本

(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);

1 个答案:

答案 0 :(得分:1)

问题是由于dynamicDropdown代码使用这种语法迭代数组而引起的:

for (prop in array)

包括数组的所有可迭代属性,而不仅仅是数组元素。在您的情况下,这将包括您添加到数组的associate方法,因为它是可迭代的,这将导致问题。

这是一个典型的例子,说明为什么不应该使用for (prop in array)语法迭代数组,而是使用

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

或者

array.forEach()

它也是您在完成后直接向Array.prototype添加内容时所冒的风险的典型示例。


您可以通过不扩展Array对象的原型(您已经发现的选项)来解决这个问题。或者,您可以使用Object.defineProperty()使新方法不可迭代,而不是直接分配给原型(只要您只使用IE9 +支持就可以了),所以{{1中的错误代码库没有在数组迭代中看到你的新方法。

您可以像这样使用dynamicDropdown使该方法不可迭代:

Object.defineProperty()