Pavel Komiagin

How to shuffle a JavaScript array

Every programmer can face the challenge of array shuffling. Of course, sometimes it is possible to use a library (e.g. underscore). But it is useful also to have the snippets for this task.

Shuffle that modifies a given array:

The array isn’t a primitive so Arrays are passed by reference. It means that array passed as a parameter will be changed when we will modify it inside the function:

// Initial array will be changed after shuffle
function mutableShuffle(arr) {
  var j;
  var temp
  
  for (var i = 0, max = arr.length; i < max; i++) {
    j = Math.floor(Math.random() * max);
    temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }
  
  return arr;
}

Shuffle that don’t modify a given array:

The function above doesn’t modify a given array. It creates, fills and returns a new array:

// Initial array will NOT be changed after shuffle
function immutableShuffle(arr) {
  var result = [];
  var j;
  var temp;
  
  for (var i = 0, max = arr.length; i < max; i++) {
    j = Math.floor(Math.random() * max);
    temp = arr[i];
    result[i] = arr[j];
    result[j] = temp;
  }
  
  return result;
}

How to use:

var mutableArr = [1, 2, 3];
console.log(mutableShuffle(mutableArr));
console.log(mutableArr); // will be changed

var immutableArr = [1, 2, 3];
console.log(immutableShuffle(immutableArr));
console.log(immutableArr); // will NOT be changed

You can try and modify it on codepen.

How to cache Backbone.js collections and models in LocalStorage

Working on project Foxford as a front-end developer I wanted to reduce the number of requests to the server. For example, we have filters containing a list of disciplines, a list of learning objectives, and a list of classes. They are used on several pages and are changed rarely so I wanted to cache the received data. Since we use Backbone, I decided to extend the models and the collections by adding the method cachedFetch like the native fetch.

Step 1

At first, write a simple wrapper for caching of JS-objects in the LocalStorage with the ability to specify the duration of the cache:

var StoreWithExpiration = function() {
  return {
    set: function(key, value, expireTime) {
      var item = {
        value: value,
        expireTime: expireTime,
        cachedAt: new Date().getTime()
      };
      try {
        localStorage.setItem(key, JSON.stringify(item));
      } catch(e) {}
    },

    get: function(key) {
      try {
        var info = JSON.parse(localStorage.getItem(key));
        if (!info || new Date().getTime() - info.cachedAt > info.expireTime)
          return null;
        return info.value;
      } catch(e) {
        localStorage.removeItem(key);
      }
    }
  };
};

Step 2

Then extend Backbone.Collection by the method cachedFetch. A lifetime of the cache can be set by parameter expireTime (default lifetime equals 1 hour). The logic is simple:

  1. If LocalStorage is unavailable then call normal fetch()
  2. If there is a valid cache then we use it, otherwise, we receive and cache new data.
var expirationStore = new StoreWithExpiration();
Backbone.Collection = Backbone.Collection.extend({

  cachedFetch: function(options) {
    options = options || {};
    var expireTime = options.expireTime || 60 * 60 * 1000;
    var cacheKey = 'bbCollection_' + this.url;

    if (!window.localStorage)
      return this.fetch(options);

    var cachedData = expirationStore.get(cacheKey);
    var cachedModels = cachedData ? cachedData.value : [];
    var success = options.success;

    if (!cachedData) {
      options.success = _.bind(function (resp) {
        if (success)
          success.call(options.context, this, resp, options);

        expirationStore.set(cacheKey, this.models, expireTime);

      }, this);
      return this.fetch(options);

    } else {

      options = _.extend({parse: true}, options);
      var method = options.reset ? 'reset' : 'set';
      this[method](cachedModels, options);

      if (success)
        success.call(options.context, this, cachedModels, options);

      this.trigger('sync', this, cachedModels, options);
      return this.sync('cachedRead', this, options);
    }
  }
});

And for the models:

Backbone.Model = Backbone.Model.extend({
  cachedFetch: function(options) {
    options = options || {};
    var expireTime = options.expireTime || 60 * 60 * 1000;
    var cacheKey = 'bbModel_' + this.url + '_' + this.id;

    if (!window.localStorage)
      return this.fetch(options);

    var cachedData = expirationStore.get(cacheKey);
    var cachedModel = cachedData ? cachedData.value : {};
    var success = options.success;

    if (!cachedData) {
      options.success = _.bind(function (resp) {
        if (success)
          success.call(options.context, this, resp, options);

        expirationStore.set(cacheKey, this.models, expireTime);

      }, this);
      return this.fetch(options);

    } else {

      options = _.extend({parse: true}, options);
      var serverAttrs = options.parse ? model.parse(cachedModel, options) : cachedModel;

      if (!model.set(serverAttrs, options))
        return false;

      if (success)
        success.call(options.context, this, cachedModel, options);

      this.trigger('sync', this, cachedModel, options);
      return this.sync('cachedRead', this, options);
    }
  }
});

Step 3

And finally, patch Backbone.Sync to support our caching:

var _sync = Backbone.sync;
Backbone.sync = function(method, model, options) {
  if (method === 'cachedRead') {

    var _getCacheKey = function() {
      return model.collection ? 'bbModel_' + model.url + '_' + model.id : 'bbCollection_' + model.url;
    };

    var _getCachedValue = function() {
      var cachedData = expirationStore.get(_getCacheKey());
      var empty = model.collection ? {} : [];
      return cachedData ? cachedData.value : empty;
    };

    if (options.success)
      options.success(_getCachedValue());

  } else {
    return _sync.apply(this, arguments);
  }
}

The full source code for this plugin you can find on github.