Source: modules/mvvm/observable_array.js

/**
 * ObservableArray is able to notify its change through events of four basic
 * operations including 'insert', 'remove', 'replace', 'reset'.
 * ObservableArray implements most used methods including 'push', 'pop', and
 * 'splice'. Due to the syntax limitation it provides 'get' and 'set' for
 * manuplication values stored in the array. 'reset' is also provided for
 * replacing te entire array with another one. In addition to these methods,
 * methods including 'forEach', 'map', 'every', 'some', 'indexOf',
 * 'lastIndexOf', 'reduce', and 'reduceRight'.
 *
 * ObservableArray creation:
 * It can be initialized by an ordinary array. If the array is not given, an
 * empty is used by default.
 *
 * @example
 *   var observableArray = ObservableArray([1, 2, 3]);
 *
 * Events:
 * Information regarding the operation is provided in the event detail. Please
 * check the following example:
 *
 * @example
 *   var observableArray = ObservableArray([1, 2, 3]);
 *   var handler = {
 *     handleEvent: function(event) { console.log(JSON.stringify(event)); }
 *   }
 *   observableArray.addEventListener('insert', handler);
 *   observableArray.addEventListener('remove', handler);
 *   observableArray.addEventListener('replace', handler);
 *   observableArray.addEventListener('reset', handler);
 *
 *   observableArray.push(4);
 *   // observableArray is [1, 2, 3, 4]
 *   // event.type is 'insert'
 *   // event.detail is {index: 3, count: 1, items: [4]}
 *
 *   observableArray.pop();
 *   // observableArray is [1, 2, 3]
 *   // event.type is 'remove'
 *   // event.detail is {index: 3, count: 1, items: [4]}
 *
 *   observableArray.set(2, 4);
 *   // observableArray is [1, 2, 4]
 *   // event.type is 'replace'
 *   // event.detail is {index: 2, oldValue: 3, newValue: 4}
 *
 *   observableArray.reset([1, 2, 3]);
 *   // observableArray is [1, 2, 3]
 *   // event.type is 'reset'
 *   // event.detail is {items: [1, 2, 3]}
 *
 *   observableArray.splice(2, 1, [4, 5, 6]);
 *   // observableArray is [1, 2, 4, 5, 6]
 *   // First event:
 *   // event.type is 'remove'
 *   // event.detail is {index: 2, count: 1, items: [3]}
 *   // Second event:
 *   // event.type is 'insert'
 *   // event.detail is {index: 2, count: 3, items: [4, 5, 6]}
 *
 * @module modules/mvvm/observable_array
 */
define(function(require) {
  'use strict';

  var Module = require('modules/base/module');
  var EventEmitter = require('modules/base/event_emitter');
  var Observable = require('modules/mvvm/observable');

  var Events = ['insert', 'remove', 'replace', 'reset'];
  var ReadOnlyMethods = ['forEach', 'map', 'every', 'some', 'indexOf',
                         'lastIndexOf', 'reduce', 'reduceRight'];

  /**
   * @class ObservableArray
   * @requires module:modules/base/module
   * @requires module:modules/base/event_emitter
   * @requires module:modules/mvvm/observable
   * @returns {ObservableArray}
   */
  var ObservableArray = Module.create(function ObservableArray(array) {
    this.super(Observable).call(this);
    this.super(EventEmitter).call(this, Events);

    this._array = array || [];
    this._length = this._array.length;
  }).extend(Observable, EventEmitter);

  /**
   * An observable property indicating the length of the array.
   *
   * @access public
   * @type {Number}
   */
  Observable.defineObservableProperty(ObservableArray.prototype, 'length', {
    readonly: true
  });

  /**
   * An observable property representing the inner array.
   *
   * @access public
   * @type {Array}
   */
  Observable.defineObservableProperty(ObservableArray.prototype, 'array', {
    readonly: true
  });

  ReadOnlyMethods.forEach(function(op) {
    ObservableArray.prototype[op] = function() {
      return this._array[op].apply(this._array, arguments);
    };
  });

  ObservableArray.prototype.push = function(item) {
    this._array.push(item);
    this._length = this._array.length;

    this._emitEvent('insert', {
      index: this._array.length - 1,
      count: 1,
      items: [item]
    });
  };

  ObservableArray.prototype.pop = function() {
    if (!this._array.length) {
      return null;
    }

    var item = this._array.pop();
    this._length = this._array.length;

    this._emitEvent('remove', {
      index: this._array.length,
      count: 1,
      items: [item]
    });

    return item;
  };

  ObservableArray.prototype.splice = function(index, count) {
    if (arguments.length < 2) {
      return [];
    }

    // Fix the arguments based on the behavior of the real spice function.
    if (index >= this._length) {
      index = this._length - 1;
    } else if (index < 0) {
      index = this._length + index;
    }
    if (count < 0) {
      count = 0;
    }

    var addedItems = Array.prototype.slice.call(arguments, 2);
    var removedItems = this._array.splice.apply(this._array, arguments);
    this._length = this._array.length;

    if (removedItems.length) {
      this._emitEvent('remove', {
        index: index,
        count: count,
        items: removedItems
      });
    }

    if (addedItems.length) {
      this._emitEvent('insert', {
        index: index,
        count: addedItems.length,
        items: addedItems
      });
    }

    return removedItems;
  };

  /**
   * Set a value to the field of a specfied index.
   *
   * @access public
   * @param {Number} index
   * @param {Object} value
   */
  ObservableArray.prototype.set = function(index, value) {
    if (index < 0 || index >= this._array.length) {
      this.throw('set: out of range');
    }

    var oldValue = this._array[index];
    this._array[index] = value;
    this._emitEvent('replace', {
      index: index,
      oldValue: oldValue,
      newValue: value
    });
  };

  /**
   * Get the value from the field of a specfied index.
   *
   * @access public
   * @param {Number} index
   * @returns {Object}
   */
  ObservableArray.prototype.get = function(index) {
    return this._array[index];
  };

  /**
   * Replace the entire array with another one.
   *
   * @access public
   * @param {Array} array
   */
  ObservableArray.prototype.reset = function(array) {
    this._array = array || [];
    this._length = this._array.length;
    this._emitEvent('reset', {
      items: this._array
    });
  };

  return ObservableArray;
});