如何在多次调用后从对象中选择一个随机属性而不重复?

时间:2014-08-17 15:25:36

标签: javascript

我试图从包含电影对象的对象中挑选一张随机电影。我需要能够反复调用该函数,直到每部电影都被使用为止。

我有这个函数,但它不起作用,因为即使内部函数调用自身,外部函数也没有返回,因为结果不是唯一的。

var watchedFilms = [];
$scope.watchedFilms = watchedFilms;

var getRandomFilm = function(movies) {
  var moviesLength = Object.keys(movies).length;

  function doPick() {
    var pick = pickRandomProperty(movies);
    var distinct = true;
    for (var i = 0;i < watchedFilms.length; i += 1) {
      if (watchedFilms[i]===pick.title) {
        distinct = false;
        if (watchedFilms.length === moviesLength) {
          watchedFilms = [];
        }
      }
    }

    if (distinct === true) {
      watchedFilms.push(pick.title);
      return pick;
    }

    if (distinct === false) {
      console.log(pick.title+' has already been picked');
      doPick();
    }
  };
  return doPick();
}

2 个答案:

答案 0 :(得分:1)

T.J。克劳德已经给出了一个很好的答案,但我想展示一种使用OO解决问题的另一种方法。

您可以创建一个包装数组的对象,并确保每次都返回一个随机的未使用项。我创建的版本是循环的,这意味着它无限循环集合,但如果你想停止循环,你可以只跟踪选择的电影数量,并在达到电影总数后停止。

function CyclicRandomIterator(list) {
    this.list = list;
    this.usedIndexes = {};
    this.displayedCount = 0;
}

CyclicRandomIterator.prototype.next = function () {
    var len = this.list.length,
        usedIndexes = this.usedIndexes,
        lastBatchIndex = this.lastBatchIndex,
        denyLastBatchIndex = this.displayedCount !== len - 1,
        index;

    if (this.displayedCount === len) {
        lastBatchIndex = this.lastBatchIndex = this.lastIndex;
        usedIndexes = this.usedIndexes = {};
        this.displayedCount = 0;
    }

    do index = Math.floor(Math.random() * len);
    while (usedIndexes[index] || (lastBatchIndex === index && denyLastBatchIndex));

    this.displayedCount++;
    usedIndexes[this.lastIndex = index] = true;

    return this.list[index];
};

然后你可以简单地做一些事情:

var randomMovies = new CyclicRandomIterator(Object.keys(movies));

var randomMovie = movies[randomMovies.next()];

请注意,如果您在项目中循环,我实施的优势在于,即使在新周期开始时,同一项也不会连续两次返回。

答案 1 :(得分:0)

更新:您已经说过可以修改film个对象,这样可以简化操作:

var getRandomFilm = function(movies) {
  var keys = Object.keys(movies);
  var keyCount = keys.length;
  var candidate;
  var counter = keyCount * 2;

  // Try a random pick
  while (--counter) {
    candidate = movies[keys[Math.floor(Math.random() * keyCount)]];
    if (!candidate.watched) {
      candidate.watched = true;
      return candidate;
    }
  }

  // We've done two full count loops and not found one, find the
  // *first* one we haven't watched, or of course return null if
  // they've all been watched
  for (counter = 0; counter < keyCount; ++counter) {
    candidate = movies[keys[counter]];
    if (!candidate.watched) {
      candidate.watched = true;
      return candidate;
    }
  }

  return null;
}

这样做的好处是,如果您使用相同的movies对象调用它并不重要。

注意安全阀。基本上,随着观看电影的数量接近电影的总数,我们随机选择候选人的几率变得更小。因此,如果我们在循环两次迭代次数之后我们没有做到这一点,那么我们放弃并选择第一个,如果有的话。


原创 (不修改电影对象)

如果你无法修改电影对象,你仍然需要watchedFilms数组,但这很简单:

var watchedFilms = [];
$scope.watchedFilms = watchedFilms;

var getRandomFilm = function(movies) {
  var keys = Object.keys(movies);
  var keyCount = keys.length;
  var candidate;

  if (watchedFilms.length >= keyCount) {
    return null;
  }

  while (true) {
    candidate = movies[keys[Math.floor(Math.random() * keyCount)]];
    if (watchedFilms.indexOf(candidate) === -1) {
      watchedFilms.push(candidate);
      return candidate;
    }
  }
}

请注意,与您的代码一样,这假定每次都使用相同的 getRandomFilm对象调用movies