Array.prototype和链选择框插件之间的冲突 [英] Conflict between Array.prototype and a chain select box plugin
问题描述
链式选择框小提琴
我有一个链式选择框和一个Array.prototype
函数将两个数组组合成一个关联的数组,它们用途不同且无关.但是,将它们放到脚本中时,它会给我undefined is not a function
指出这部分this.forEach
是错误的来源.我最终用这个函数替换了Array.prototype
函数(示例小提琴):
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
}, {})
}
我很好奇为什么链式选择框和Array.prototype
函数之间存在冲突?
I'm just curious why there is a conflict between the chained select box and the Array.prototype
function?
代码如下:
$(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);
(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);
推荐答案
之所以引起此问题,是因为dynamicDropdown
代码使用以下这种语法在数组上进行迭代:
The issue is caused because the dynamicDropdown
code iterates over an array with this type of syntax:
for (prop in array)
包括数组的所有可迭代属性,而不仅仅是数组元素.在您的情况下,这将包括您添加到数组的associate
方法,因为它是可迭代的,并且会引起问题.
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.
这是一个经典示例,说明了为什么绝不应该使用for (prop in 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++)
或者这个:
array.forEach()
或更现代的:
for (item of array)
,它也是将事物直接添加到Array.prototype
时所承担的风险的经典示例.
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.
您可以通过完全不扩展Array
对象的原型(已发现的选项)来解决该问题.或者,您可以通过使用Object.defineProperty()
来使新方法不可迭代,而不是直接分配给原型(只要您只支持IE9 +就可以了),这样dynamicDropdown
库中的错误代码就不会在其数组迭代中查看您的新方法.
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.
您可以像这样使用Object.defineProperty()
使该方法不可迭代:
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;
}
});
这篇关于Array.prototype和链选择框插件之间的冲突的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!