Skip to content
This repository has been archived by the owner on Jul 18, 2019. It is now read-only.

Latest commit

 

History

History
307 lines (259 loc) · 8.94 KB

HOWTO.md

File metadata and controls

307 lines (259 loc) · 8.94 KB

redefine VS ES5

Create A Null Object

// redefine
var o = redefine.from(null);

// ES5
var o = Object.create(null);

Add Properties During Creation

// redefine
var o = redefine.from(null, {
  name: "object",
  toString: function () {
    return "Hi, I am " + this.name;
  }
});

// ES5
var o = Object.create(null, {
  name: {
    value: "object"
  },
  toString: {
    value: function () {
      return "Hi, I am " + this.name;
    }
  }
});

Add Configurable + Writable Properties During Creation

// redefine
var o = redefine.from(null, {
  name: "object",
  toString: function () {
    return "Hi, I am " + this.name;
  }
}, {
  configurable : true,
  writable: true
});

// ES5
var o = Object.create(null, {
  name: {
    configurable: true,
    writable: true,
    value: "object"
  },
  toString: {
    configurable: true,
    writable: true,
    value: function () {
      return "Hi, I am " + this.name;
    }
  }
});

Add A Getter/Setter

// redefine
var o = redefine.from({}, {
  bday: Date.now(),
  age: redefine.as({
    set: function () {
      throw 'you cannot set your age';
    },
    get: function () {
      return parseInt(
        (Date.now() - this.bday) /
        (1000 * 60 * 60 * 24 * 365)
      ); // I know, no leap year
    }
  })
});

// ES5
var o = Object.create({}, {
  bday: {
    value: Date.now()
  },
  age: {
    set: function () {
      throw 'you cannot set your age';
    },
    get: function () {
      return parseInt(
        (Date.now() - this.bday) /
        (1000 * 60 * 60 * 24 * 365)
      ); // I know, no leap year
    }
  }
});

Real World Example: An Emitter Class

This a simplified Emitter class with a logic optimized to save some memory and GC cycle.

// generic basic Emitter constructor
function Emitter(){}
function emit(callback) {
  // just a recycled function
  callback(this);
}

This is how we can define the prototype of the Emitter class.

// redefine
redefine(
  Emitter.prototype,
  {
    emit: function (type, data) {
      if (type in this._handlers) {
        this._handlers[type].forEach(emit, data);
      }
      return this;
    },
    on: function (type, handler) {
      var list = this._getList(type);
      list.indexOf(handler) < 0 && list.push(handler);
      return this;
    },
    off: function (type, handler) {
      var list = this._getList(type),
          i = list.indexOf(handler);
      if (-1 < i) {
        list.splice(i, 1);
        if (!list.length) {
          delete this._handlers[type];
          if (!Object.keys(this._handler).length) {
            delete this._handlers;
          }
        }
      }
      return this;
    },
    _getList: function (type) {
      return this._handlers[type] || (
        this._handlers[type] = []
      );
    },
    _handlers: redefine.later(function(){
      return {};
    })
  }
);

The pattern used for the _handlers property only is an inherited getter replaced on demand with a direct property access, explained in details in The Power Of Getters post.

Long story short: we can create 100 instances of Emitter and the amount of extra objects will be exactly 100 instead of 200 considering a handler created during initialization.

With this pattern, only when the _handlers object is needed is created once per instance and set as property to avoid calling the getter per each access. When listeners are removed, both Array used as type list and _handlers are removed, if empty. In order to obtain the same behavior in ES5:

// ES5
Object.defineProperties(
  Emitter.prototype,
  {
    emit: {
      value: function (type, data) {
        if (type in this._handlers) {
          this._handlers[type].forEach(emit, data);
        }
        return this;
      }
    },
    on: {
      value: function (type, handler) {
        var list = this._getList(type);
        list.indexOf(handler) < 0 && list.push(handler);
        return this;
      },
    }
    off: {
      value: function (type, handler) {
        var list = this._getList(type),
            i = list.indexOf(handler);
        if (-1 < i) {
          list.splice(i, 1);
          if (!list.length) {
            delete this._handlers[type];
            if (!Object.keys(this._handler).length) {
              delete this._handlers;
            }
          }
        }
        return this;
      }
    },
    _getList: {
      value: function (type) {
        return this._handlers[type] || (
          this._handlers[type] = []
        );
      }
    },
    _handlers: {
      get: function () {
        Object.defineProperty(
          this, "_handlers", {
            configurable: true,
            value: {}
          }
        );
        return this._handlers;
      }
    }
  }
);

Real World Example: A Safer Definition

While Object.defineProperties(object, descriptorsList) second argument loops only through hasOwnProperty(key), once it comes to property definition, and the same is for Object.defineProperty(obj, key, descriptor), the object used to described the property looks up for inherited properties too and consider them.

Even if defaults are {writable: false, enumerable: false, configurable: false}, it's easy to make every property enumerable and configurable simply doing this:

Object.prototype.configurable = true;
Object.prototype.enumerable = true;

After above malicious piece of code, if you think a constant variable defined like this cannot be changed you are wrong:

Object.defineProperty(window, "myLibrary", {value:myLibrary});

It is necessary indeed to ensure that defaults are written as well, making defaults meaningless because of inheritance, but even worse, if a malicious code will write Object.prototype.writable = true and there are getters or setters involved, these will all throw an exception because writable does not accept getters or setters.

How To Prevent Problems in ES5

In ES5 every descriptor should inherit from null otherwise no descriptor will be immune from possible attacks.

Here an example of few problems we might want to avoid:

// set up the nasty environment
Object.prototype.configurable =
Object.prototype.enumerable =
Object.prototype.writable = true;

// verify that there are actually problems
try {
  Object.defineProperty(this, "problem", {get: function () {
    return "we gonna have bad time";
  }});
} catch(o_O) {
  console.log("ouch!");
  Object.defineProperty(this, "problem", {value:true});
  console.log(this.propertyIsEnumerable("problem")); // true
  this.problem = {}; // no problems
  delete this.problem; // true again
}

Accordingly, every single descriptor should be created via Object.crete(null) so our code should look like

function descriptor(object) {
  // now don't take this wrong
  // but this function is almost
  // as big as the whole redefine.js library
  // I would not go through this pattern
  for (var
    nullDescriptor = Object.create(null),
    properties = [
      "configurable",
      "enumerable",
      "writable",
      "get",
      "set",
      "value"
    ],
    i = properties.length; i--;
    object.hasOwnProperty(properties[i]) && (
      nullDescriptor[properties[i]] = object[properties[i]]
    )
  );
  return nullDescriptor;
}

Object.defineProperty(this, "problem", descriptor({
  value: "now we talk"
}));

this.propertyIsEnumerable("problem"); // false
this.problem = {}; // nope
delete this.problem; // false
this.problem; // "now we talk"

Got it? Now ...

How To Prevent Problems in redefine.js
redefine(this, "problem", "solved");

this.propertyIsEnumerable("problem"); // false
this.problem = {}; // nope
delete this.problem; // false
this.problem; // "solved"

If you are wondering about performance there are many things to consider behind redefine.js and one of these is that performance are really good for what it offers.

While is usually slightly slower on desktop, redefine.js is almost as fast in older Android 2.3 devices where performance matters the most.

redefine.js is indeed suitable for mobile phones, even if quite old!