如何改进此JavaScript DOM操作数据结构/算法?

如何改进此JavaScript DOM操作数据结构/算法?,javascript,algorithm,data-structures,Javascript,Algorithm,Data Structures,目标 我有一个DOM,上面有大约70个元素(有一些内容的div)。我需要移动和切换这些div的显示很多,也很快。速度是最重要的事情之一。移动和切换这些div的触发器是一个搜索查询,有点类似,只是我移动和切换的所有DOM元素都是第一次加载的(因此不再调用服务器) 实施 我通过以下方式实现了这一点:在DOM旁边,我传入了一个JavaScript对象数组,该数组表示div及其属性,如位置、内容等。这个数组就像DOM的镜像。当用户开始键入时,我开始在数组中循环,并计算每个div/对象需要对其执行的操作。

目标

我有一个DOM,上面有大约70个元素(有一些内容的div)。我需要移动和切换这些div的显示很多,也很快。速度是最重要的事情之一。移动和切换这些div的触发器是一个搜索查询,有点类似,只是我移动和切换的所有DOM元素都是第一次加载的(因此不再调用服务器)

实施

我通过以下方式实现了这一点:在DOM旁边,我传入了一个JavaScript对象数组,该数组表示div及其属性,如位置、内容等。这个数组就像DOM的镜像。当用户开始键入时,我开始在数组中循环,并计算每个div/对象需要对其执行的操作。实际上,我在这个数组上循环了几次:首先检查是否需要查看div/对象,然后查看对象,然后检查是否需要查看内容,然后查看内容

我在这些循环中做的一件事是为DOM操作设置标志。据我所知,与我正在做的其他事情(循环、读取和写入对象属性等)相比,读取和操作DOM是JavaScript中速度较慢的操作之一。我还做了一些分析,证实了这个假设。因此,在每个角落,我都试图防止“触摸”DOM以提高性能。在我的算法结束时,我再次循环,执行所有必要的DOM操作,并重置标志以表示它们已被读取。为了实现跨浏览器兼容性,我使用jQuery实际执行DOM操作(选择、移动、切换)。我不使用jQuery在数组上循环

问题

我现在的问题是,我认为我的代码和数据结构有点难看。我有这个 具有大量属性和标志的大型多维数组。我反复使用函数调用函数来循环它。当遇到问题时,我(仍然)可以稍微轻松地调试东西,但感觉不对

问题

这类问题有没有设计模式或通用解决方案?我怀疑我可以在数组和DOM之间实现某种智能耦合,这样我就不必显式地设置标志和执行DOM操作,但我不知道这种耦合应该如何工作,也不知道这是一个好主意还是让事情变得复杂

在解决这个问题时,我是否忽略了其他数据结构或算法原理

谢谢

更新 按照要求,我添加了代码,大约700行。注意:我没有污染全局名称空间,这些函数是在闭包中定义和使用的

/**
 * Applies the filter (defined by the currentQuery and to the cats array)
 *
 * -checks whether matching is needed
 * -if needed does the matching
 * -checks whether DOM action is needed
 * -if needed executes DOM action
 *
 * cats is an array of objects representing categories
 * which themselves contain an array of objects representing links
 * with some attributes
 *
 * cats = (array) array of categories through which to search
 * currentQuery = (string) with which to find matches within the cats
 * previousQuery = (string) with previously-typed-in query
 *
 * no return values, results in DOM action and manipulation of cats array
 */
function applyFilter(cats,currentQuery, previousQuery) {
    cats = flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery);
    cats = matchCats(cats,currentQuery);
    cats = flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery);
    cats = matchLinks(cats,currentQuery);
    cats = flagIfDisplayToggleNeeded(cats);
    if ( currentQuery.length > 0 ) {
        cats = flagIfMoveNeeded(cats);
    } else {
        // move everything back to its original position
        cats = flagMoveToOriginalPosition(cats);
    }

    // take action on the items that need a DOM action
    cats = executeDomActions(cats);
}

/**
* Sets a flag on a category if it needs matching, parses and returns cats
*
* Loops through all categories and sets a boolean to signal whether they 
* need matching.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/ 
function flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery) {
    var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);

    // check if matching is necessary for categories
    for (var i = 0; i < cats.length; i++) {
        cats[i].matchingNeeded = isMatchingNeededForCat(
            cats[i].matches
            ,newQueryIsLonger
            ,currentQuery.length
            ,cats[i].noMatchFoundAtNumChars
        );
    }
    return cats;
}

/**
* Whether the new query is longer than the previous one
*
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (boolean) true/false
*/
function isNewQueryLonger(currentQuery, previousQuery) {
    if (previousQuery == false) {
        return true;
    }

    return currentQuery.length > previousQuery.length
}

/**
* Deduces if a category needs to be matched to the current query
*
* This function helps in improving performance. Matching is done using 
* indexOf() which isn't slow of itself but preventing even fast processes
* is a good thing (most of the time). The function looks at the category,
* the current and previous query, then decides whether
* matching is needed.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a 
*   category when it switches from being matched to being not-matched. The
*   number indicates the number of characters in the first query that did
*   not match the category. This helps in performance because we don't need
*   to recheck the categoryname if it doesn't match now and the new query is
*   even longer.
*
* returns (boolean) true/false
*/
function isMatchingNeededForCat(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
    if (typeof(currentlyMatched) == 'undefined') {
        // this happens the first time we look at a category, for all 
        // categories this happens with an empty query and that matches with
        // everything
        currentlyMatched = true;
    }

    if (currentlyMatched && newQueryIsLonger) {
        return true;
    }

    if (!currentlyMatched && !newQueryIsLonger) {
        // if currentlyMatched == false, we always have a value for
        // noMatchFoundAtNumChars

        // matching is needed if the first "no-match" state was found 
        // at a number of characters equal to or bigger than 
        // queryLength
        if ( queryLength < noMatchFoundAtNumChars ) {
            return true;
        }
    }

    return false;
}

/**
* Does matching on categories for all categories that need it.
*
* Sets noMatchFoundAtNumChars to a number if the category does not match.
* Sets noMatchFoundAtNumChars to false if the category matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchCats(cats,currentQuery) {
    for (var i = 0; i < cats.length; i++) {
        if (cats[i].matchingNeeded) {
            cats[i].matches = categoryMatches(cats[i],currentQuery);

            // set noMatchFoundAtNumChars
            if (cats[i].matches) {
                cats[i].noMatchFoundAtNumChars = false;
            } else {
                cats[i].noMatchFoundAtNumChars = currentQuery.length;
            }
        }
    }
    return cats;
}

/**
* Check if the category name matches the query
*
* A simple indexOf call to the string category_name
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function categoryMatches(category,query) {
    catName = category.category_name.toLowerCase();
    if (catName.indexOf(query) !== -1 ) {
        return true;
    }

    return false;
}

/**
* Checks links to see whether they need matching
*
* Loops through all cats, selects the non-matching, for every link decides
* whether it needs matching
*
* cats = (array) an array with all the category-objects in it
* currentQuery = the currently typed-in query
* previousQuery = the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/
function flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery) {
    var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);
    for (var i = 0; i < cats.length; i++) {
        if (!cats[i].matches) { // only necessary when cat does not match
            for (var k = 0; k < cats[i].links.length; k++) {
                cats[i].links[k].matchingNeeded = isMatchingNeededForLink(
                    cats[i].links[k].matches
                    ,newQueryIsLonger
                    ,currentQuery.length
                    ,cats[i].links[k].noMatchFoundAtNumChars
                );
            }
        }
    }
    return cats;
}

/**
* Checks whether matching is needed for a specific link
*
* This function helps in improving performance. Matching is done using 
* indexOf() for every (relevant) link property, this function helps decide
* whether that *needs* to be done. The function looks at some link 
* properties, the current and previous query, then decides whether
* matching is needed for the link.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a 
*   link when it switches from being matched to being not-matched. The
*   number indicates the number of characters in the first query that did
*   not match the link. This helps in performance because we don't need
*   to recheck the link properties in certain circumstances.
*
* return (boolean) true/false
*/
function isMatchingNeededForLink(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
    if (typeof(currentlyMatched) == 'undefined') {
        // this happens to a link the first time a cat does not match and
        // we want to scan the links for matching
        return true;            
    }

    if (currentlyMatched && newQueryIsLonger) {
        return true;
    }

    if (!currentlyMatched && !newQueryIsLonger) {
        // if currentlyMatched == false, we always have a value for
        // noMatchFoundAtNumChars

        // matching is needed if the first "no-match" state was found 
        // at a number of characters equal to or bigger than 
        // queryLength
        if ( queryLength < noMatchFoundAtNumChars ) {
            return true;
        }
    }

    return false;
}

/**
* Does matching on links for all links that need it.
*
* Sets noMatchFoundAtNumChars to a number if the link does not match.
* Sets noMatchFoundAtNumChars to false if the link matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchLinks(cats,currentQuery) {
    for (var i = 0; i < cats.length; i++) {
        // category does not match, check if links in the category match
        if (!cats[i].matches) {
            for (var k = 0; k < cats[i].links.length; k++) {
                if (cats[i].links[k].matchingNeeded) {
                    cats[i].links[k].matches = linkMatches(cats[i].links[k],currentQuery);
                }

                // set noMatchFoundAtNumChars
                if (cats[i].links[k].matches) {
                    cats[i].links[k].noMatchFoundAtNumChars = false;
                } else {
                    cats[i].links[k].noMatchFoundAtNumChars = currentQuery.length;
                }
            }
        }
    }
    return cats;
}    

/**
* Check if any of the link attributes match the query
*
* Loops through all link properties, skips the irrelevant ones we use for filtering
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function linkMatches(link,query) {
    for (var property in link) {
        // just try to match certain properties
        if (
                !( // if it's *not* one of the following
                    property == 'title'
                    || property == 'label'
                    || property == 'url'
                    || property == 'keywords'
                    || property == 'col'
                    || property == 'row'
                )
        ){
            continue;
        }

        // if it's an empty string there's no match
        if( !link[property] ) {
            continue;
        }

        var linkProperty = link[property].toLowerCase();
        if (linkProperty.indexOf(query) !== -1){
            return true;
        }

    }
    return false;
}

/**
* Flags if toggling of display is needed for a category.
*
* Loops through all categories. If a category needs some DOM
* action (hiding/showing) it is flagged for action. This helps in 
* performance because we prevent unnecessary calls to the DOM (which are 
* slow).
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfDisplayToggleNeeded(cats) {
    for (var i = 0; i < cats.length; i++) {
        // this happens the first time we look at a category
        if (typeof(cats[i].currentlyDisplayed) == 'undefined') {
            cats[i].currentlyDisplayed = true;
        }

        var visibleLinks = 0;
        // a cat that matches, all links need to be shown
        if (cats[i].matches) {
            visibleLinks = cats[i].links.length;
        } else {
            // a cat that does not match
            for (var k = 0; k < cats[i].links.length; k++) {
                if (cats[i].links[k].matches) {
                    visibleLinks++;
                }
            }            
        }

        // hide/show categories if they have any visible links
        if (!cats[i].currentlyDisplayed && visibleLinks > 0 ) {
            cats[i].domActionNeeded = 'show';
        } else if( cats[i].currentlyDisplayed && visibleLinks == 0 ){
            cats[i].domActionNeeded = 'hide';
        }           
    }
    return cats;
}

/**
* Flags categories to be moved to other position.
*
* Loops through all categories and looks if they are distributed properly. 
* If not it moves them to another position. It remembers the old position so
* it can get the categories back in their original position.
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfMoveNeeded(cats) {
    var numCats, numColumns, displayedCats, i, moveToColumn, tmp;

    numColumns = getNumColumns(cats);
    numDisplayedCats = getNumDisplayedCats(cats);        
    columnDistribution = divideInPiles(numDisplayedCats, numColumns);

    // optional performance gain: only move stuff when necessary
    // think about this some more

    // we convert the distribution in columns to a table so we get columns
    // and positions
    catDistributionTable = convertColumnToTableDistribution(columnDistribution);

    // sort the categories, highest positions first
    // catPositionComparison is a function to do the sorting with
    // we could improve performance by doing this only once
    cats = cats.sort(catPositionComparison);

    for (i = 0; i < cats.length; i += 1) {
        if( categoryWillBeDisplayed(cats[i]) ){
            tmp = getNewPosition(catDistributionTable); // returns multiple variables
            catDistributionTable = tmp.catDistributionTable;
            cats[i].moveToColumn = tmp.moveToColumn;
            cats[i].moveToPosition = tmp.moveToPosition;
        } else {
            cats[i].moveToColumn = false;
            cats[i].moveToPosition = false;
        }
    }
    return cats;
}

/**
* A comparison function to help the sorting in flagIfMoveNeeded()
*
* This function compares two categories and returns an integer value 
* enabling the sort function to work.
*
* cat1 = (obj) a category
* cat2 = (obj) another category
*
* returns (int) signaling which category should come before the other
*/
function catPositionComparison(cat1, cat2) {
    if (cat1.category_position > cat2.category_position) {
        return 1; // cat1 > cat2
    } else if (cat1.category_position < cat2.category_position) {
        return -1; // cat1 < cat2
    }

    // the positions are equal, so now compare on column, if we need the 
    // performance we could skip this
    if (cat1.category_column > cat2.category_column) {
        return 1; // cat1 > cat2
    } else if (cat1.category_column < cat2.category_column) {
        return -1; // cat1 < cat2
    }

    return 0; // position and column are equal
}

/**
* Checks if a category will be displayed for the currentQuery
*
* cat = category (object) 
*
* returns (boolean) true/false
*/
function categoryWillBeDisplayed(cat) {
    if( (cat.currentlyDisplayed === true  && cat.domActionNeeded !== 'hide')
        ||
        (cat.currentlyDisplayed === false && cat.domActionNeeded === 'show')
    ){
        return true;
    } else {
        return false;
    }
}

/**
 * Gets the number of unique columns in all categories
 *
 * Loops through all cats and saves the columnnumbers as keys, insuring
 * uniqueness. Returns the number of
 *
 * cats = (array) of category objects
 *
 * returns (int) number of unique columns of all categories
 */
function getNumColumns(cats) {
    var columnNumber, uniqueColumns, numUniqueColumns, i;

    uniqueColumns = [];
    for (i = 0; i < cats.length; i += 1) {
        columnNumber = cats[i].category_column;
        uniqueColumns[columnNumber] = true;
    }

    numUniqueColumns = 0;
    for (i = 0; i < uniqueColumns.length; i += 1) {
        if( uniqueColumns[i] === true ){
            numUniqueColumns += 1
        }
    }
    return numUniqueColumns;
}

/**
 * Gets the number of categories that will be displayed for the current query
 *
 * cats = (array) of category objects
 *
 * returns (int) number of categories that will be displayed
 */
function getNumDisplayedCats(cats) {
    var numDisplayedCats, i;

    numDisplayedCats = 0;
    for (i = 0; i < cats.length; i += 1) {
        if( categoryWillBeDisplayed(cats[i]) ){
            numDisplayedCats += 1;
        }
    }
    return numDisplayedCats;
}

/**
 * Evenly divides a number of items into piles
 *
 * Uses a recursive algorithm to divide x items as evenly as possible over
 * y piles.
 *
 * items = (int) a number of items to be divided
 * piles = (int) the number of piles to divide items into
 *
 * return an array with numbers representing the number of items in each pile
 */
function divideInPiles(items, piles) {
    var averagePerPileRoundedUp, rest, pilesDivided;
    pilesDivided = [];

    if (piles === 0) {
        return false;
    }

    averagePerPileRoundedUp = Math.ceil(items / piles);
    pilesDivided.push(averagePerPileRoundedUp);
    rest = items - averagePerPileRoundedUp;

    if (piles > 1) {
        pilesDivided = pilesDivided.concat(divideInPiles(rest, piles - 1)); // recursion
    }

    return pilesDivided;
}

/**
 * Converts a column distribution to a table
 *
 * Receives a one-dimensional distribution array and converts it to a two-
 * dimensional distribution array.
 *
 * columnDist (array) an array of ints, example [3,3,2]
 *
 * returns (array) two dimensional array, rows with "cells"
 * example: [[true,true,true],[true,true,true],[true,true,false]]
 * returns false on failure
 */
function convertColumnToTableDistribution(columnDist) {
    'use strict';
    var numRows, row, numCols, col, tableDist;

    if (columnDist[0] === 'undefined') {
        return false;
    }

    // the greatest number of items are always in the first column
    numRows = columnDist[0];
    numCols = columnDist.length;
    tableDist = []; // we 

    for (row = 0; row < numRows; row += 1) {
        tableDist.push([]); // add a row
        // add "cells"
        for (col = 0; col < numCols; col += 1) {
            if (columnDist[col] > 0) {
                // the column still contains items
                tableDist[row].push(true);
                columnDist[col] -= 1;
            } else {
                tableDist[row][col] = false;
            }
        }
    }
    return tableDist;
}

/**
* Returns the next column and position to place a category in.
*
* Loops through the table to find the first position that can be used. Rows
* and positions have indexes that start at zero, we add 1 in the return 
* object.
*
* catDistributionTable = (array) of rows, with positions in them
*
* returns (object) with the mutated catDistributionTable, a column and a 
* position
*/
function getNewPosition(catDistributionTable) {
    var numRows, row, col, numCols, moveToColumn, moveToPosition;

    numRows = catDistributionTable.length;

    findposition:
    for (row = 0; row < numRows; row += 1) {
        numCols = catDistributionTable[row].length;
        for ( col = 0; col < numCols; col += 1) {
            if (catDistributionTable[row][col] === true) {
                moveToColumn = col;
                moveToPosition = row;
                catDistributionTable[row][col] = false;
                break findposition;
            }
        }
    }

    // zero-indexed to how it is in the DOM, starting with 1
    moveToColumn += 1;
    moveToPosition += 1;

    return {
        'catDistributionTable'  : catDistributionTable
        ,'moveToColumn'         : moveToColumn
        ,'moveToPosition'       : moveToPosition
    };
}

/**
* Sets the target position of a category to its original location
*
* Each category in the DOM has attributes defining their original position.
* After moving them around we might want to move them back to their original
* position, this function flags all categories to do just that.
*
* cats = (array) of category objects
*
* All of the possible return values
*/
function flagMoveToOriginalPosition(cats) {
    for (i = 0; i < cats.length; i += 1) {
        cats[i].moveToColumn = cats.category_column;
        cats[i].moveToPosition = cats.category_position;
    }
    return cats;
}

/**
* Execute DOM actions for the items that need DOM actions
*
* Parses all categories, executes DOM actions on the categories that
* require a DOM action.
*
* cats = (array) an array with all the category-objects in it
*
* no return values
*/
function executeDomActions(cats) {
    for (var i = 0; i < cats.length; i++) {
        var category_id = cats[i].category_id;

        // toggle display of columns
        if (cats[i].domActionNeeded == 'show') {
            showCategory(category_id);
            cats[i].currentlyDisplayed = true;
        }

        if (cats[i].domActionNeeded == 'hide') {
            hideCategory(category_id);
            cats[i].currentlyDisplayed = false;

        }
        cats[i].domActionNeeded = false;

        // for every currentlyDisplayed category move it to new location
        // if necessary
        if (cats[i].currentlyDisplayed && cats[i].moveToColumn !== false) {
            cats[i] = moveCat(cats[i]);
        }
    }
    return cats;
}

/**
* Show a certain category
*
* category_id = (int) the id of the category that needs to be shown
*
* no return values
*/
function showCategory(category_id) {
    $('#' + category_id).show();
}

/**
* Hide a certain category
*
* category_id = (int) the id of the category that needs to be hidden
*
* no return values
*/
function hideCategory(category_id) {
    $('#' + category_id).hide();
}

/**
 * Moves a category to the position set in its attributes
 *
 * A category can have attributes defining the column and position (or row)
 * this function moves the category to the correct column and position.
 *
 * cat = (object) category
 *
 * returns (object) category
 */
function moveCat(cat) {
    var columnSelector, catSelector;
    columnSelector = '#column' + cat.moveToColumn + ' .column_inner' + ' .hiddenblocks';
    catSelector = '#' + cat.category_id;
    $(columnSelector).prepend($(catSelector));

    // reset target coordinates
    cat.moveToColumn = false;
    cat.moveToPosition = false;

    return cat;
}
/**
*应用筛选器(由currentQuery和cats数组定义)
*
*-检查是否需要匹配
*-如果需要,是否进行匹配
*-检查是否需要DOM操作
*-如果需要,执行DOM操作
*
*cats是一组表示类别的对象
*其本身包含表示链接的对象数组
*有一些属性
*
*cats=(数组)要搜索的类别数组
*currentQuery=(字符串),用于在cats中查找匹配项
*previousQuery=(字符串)与以前在查询中键入的
*
*无返回值,导致DOM操作和对cats数组的操作
*/
函数applyFilter(CAT、currentQuery、previousQuery){
cats=flagifmatchingisneedforcats(cats、currentQuery、previousQuery);
cats=matchCats(cats,currentQuery);
cats=flagifmatchingisneedforlinks(cats、currentQuery、previousQuery);
cats=匹配链接(cats,currentQuery);
cats=flagIfDisplayToggleNeeded(猫);
如果(currentQuery.length>0){
猫=需要的旗子(猫);
}否则{
//将所有内容移回其原始位置
cats=旗杆移动至起始位置(cats);
}
//对需要DOM操作的项执行操作
猫=执行的动作(猫);
}
/**
*如果类别需要匹配,则在类别上设置标志,解析并返回CAT
*
*循环遍历所有类别,并设置一个布尔值来表示它们是否
*需要匹配。
*
*cats=(数组)包含所有类别对象的数组
*currentQuery=(字符串)当前键入的查询
*previousQuery=(字符串)以前键入的查询
*
*返回(数组)cats,可能处于不同的状态
*/ 
cats(cats、currentQuery、previousQuery)需要函数标记IfMatching{
var newQueryIsLonger=isnewquerylanger(currentQuery,previousQuery);
//检查类别是否需要匹配
对于(变量i=0;ipreviousQuery.length
}
/**
*推断类别是否需要与当前查询匹配
*
*此功能有助于提高性能。匹配是使用
*indexOf()本身并不慢,但会阻止甚至更快的进程
*(大多数时候)这是件好事。函数查看类别,
*当前查询和上一个查询,然后决定
*需要匹配。
*
*currentlyMatched=(布尔值)关于布尔值是否与上一个查询匹配
*NewQuerySlonger=(布尔值)新查询是否更长
*queryLength=(int)l
function linkMatches(link,query) {
    if (link["ConcatenatedLCasedProperties"].indexOf(query) !== -1){
        return true;
    }
    return false;
}
function divideInPiles(items, piles) {
    var result = [];
    var perPile = Math.floor(items/piles);
    var leftOver = items % piles;
    if(piles == 0) 
        return false;
    for(var x=0; x<piles; x++) 
        result.push(perPile + (--leftOver >= 0 ? 1: 0));
    return result;
}