All questions

Backbone Model

Premium

Backbone Model

Implement a Model class in the style of Backbone.js — an object that stores a bag of attributes and lets other code subscribe to changes. When an attribute's value changes, the model notifies its subscribers; when nothing changed, it stays silent. This is the observer pattern at the heart of classic MVC front ends: a view registers a listener on the model, and re-renders itself whenever the data it displays actually changes — without the model needing to know the view exists.

Signature

class Model {
  constructor(attrs?: Record<string, unknown>);     // no change events fire here

  get(key: string): unknown;                          // value, or undefined if absent
  set(key: string, value: unknown, options?: { silent?: boolean }): this;
  set(attrs: Record<string, unknown>, options?: { silent?: boolean }): this;

  on(event: string, callback: Function): void;        // subscribe
  off(event?: string, callback?: Function): void;      // unsubscribe
  trigger(event: string, ...args: unknown[]): void;    // fire manually

  previous(key: string): unknown;                      // value before the last change
  changedAttributes(): Record<string, unknown> | false; // keys changed in the last set
  toJSON(): Record<string, unknown>;                  // shallow copy of attributes
}

For each key whose value actually changes, set fires a change:<key> event with (model, newValue). After all per-key events, it fires one general change event with (model). A change:<key> or change event fires only when at least one value changed. set returns this so calls chain.

Examples

const m = new Model({ name: 'Ada' });

m.on('change:name', (model, value) => console.log('name ->', value));
m.on('change', (model) => console.log('something changed'));

m.set('name', 'Grace');
// logs: "name -> Grace"
// logs: "something changed"
// Setting a key to the value it already holds fires NOTHING.
const m = new Model({ count: 0 });
m.on('change', () => console.log('changed'));
m.set('count', 0);   // (no output — 0 === 0)
m.set('count', 1);   // logs: "changed"
// The object form sets many keys but fires the general change only once.
const m = new Model({ a: 1, b: 2 });
m.on('change:a', () => console.log('a'));
m.on('change:b', () => console.log('b'));
m.on('change', () => console.log('general'));
m.set({ a: 10, b: 20 });
// logs: "a", "b", "general"  — one general event, not two

// Silent set updates state but stays quiet.
m.set({ a: 99 }, { silent: true });   // (no output); m.get('a') === 99

Notes

  • No events on construction. Passing attrs to new Model(attrs) seeds the values directly. It must not fire any change event — there are no listeners yet, and seeding is not a change.
  • Equality is strict (===). A key "changed" only when its new value is not === to its current value. set('count', 0) when count is already 0 fires nothing. Object identity counts: setting a key to a different object with the same contents does fire, because the references differ.
  • State updates before callbacks fire. A change:<key> listener that calls model.get(key) must read the new value, not the old one. Apply the change to the internal store first, then invoke listeners.
  • Multi-key set batches one general change. Each changed key gets its own change:<key>, but the whole set produces exactly one general change, regardless of how many keys changed.
  • off has three modes. off(event, cb) removes one listener; off(event) removes every listener for that event; off() removes everything.
  • Don't worry about nested models, collections, validation hooks, attribute defaults, or async — those are out of scope (a few are sketched in the solution's Going further).

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium