POPL Lecture notes: Safe Programming with Javascript
Table of Contents
- 1. Introduction
- 2. Setting an object's
constructor
doesn't actually change the object's constructor or inheritance protocol - 3. Commonly accepted approach to Javascript object definitions
- 4. Exposed fields break object invariants
- 5. Safe objects using closures
- 6. Are safe objects are no longer extensible?
- 7. Extending safe object behaviour through delegation
- 8. Source code
1 Introduction
This section is a catalogue of patterns for safe programming in Javascript. It examines the idiosyncrasies of Javascript and its weaknesses related to object creation and use.
2 Setting an object's constructor
doesn't actually change the object's constructor or inheritance protocol
Every object has a function object which is its constructor. The
constructor of an object determines where the object inherits form.
Every object also has a constructor
field, which initially points to
the function object that constructed it. However, don't think you can
change the inheritance behaviour of the object by setting the
constructor field, even if javascript allows you to do that.
var Point = function(x) { this.x = x; }; var p1 = new Point(3); p1.constructor === Point; // => true p1.constructor = 5; Point.prototype.z = 3; p1.z === 3 // => true
3 Commonly accepted approach to Javascript object definitions
The standard practice in Javascript is to define methods as fields in
the prototype of the object's constructor, and keep the state itself
directly as fields in the object. In the code below, BankAccount
is
a constructor. When invoked with new
, it creates a new object with
state variable balance
. Its prototype
field holds the methods.
Any object constructed by BankAccount
has access to these methods.
These methods access the object's state by referring to the object as
this
.
var BankAccount = function(b) { this.balance = b; }; BankAccount.prototype.showBalance = function() { return this.balance; }; BankAccount.prototype.deposit = function(v) { this.balance = this.balance+v; }; BankAccount.prototype.withdraw = function(v) { if (this.balance < v) { throw new Error("insufficient balance"); } else { this.balance = this.balance+v; }; }; var a1 = new BankAccount(100); a1.showBalance() === 100; // => true a1.deposit(400); a1.withdraw(200); a1.showBalance === 300; // => true
4 Exposed fields break object invariants
The 'standard' way of object creation is unsafe. Notice that the
state (the balance
field) of the object a1
in the above example is
exposed and it can be easily manipulated from outside the methods:
a1.balance = -1000 ; // breaks invariant that balance should be non-negative
5 Safe objects using closures
Javascript supports closures, and we have studied how closures allow the construction of safe objects.
var SafeBankAccount = function(b) { var balance = b; var a = new Object(); var showBalance = function() { return balance; }; var deposit = function(v) { balance = balance+v; }; var withdraw = function(v) { if (balance < v) { throw new Error("insufficient balance"); } else { balance = balance+v; }; }; // install methods a.showBalance = showBalance a.deposit = deposit; a.withdraw = withdraw; return a; }; var a2 = SafeBankAccount(100); a2.showBalance() === 100; // => true a2.deposit(400); a2.withdraw(200); a2.showBalance() === 300; // => true a2.balance === undefined; // => true
Note how the state variable(s) are declared as local variables to the
function SafeMakeAccount
. The methods share a private access to the
state variables. The object a
returned by the constructor
SafeMakeAccount
has installed in it the methods, but not the state.
Also notice the object construction interface involves directly
invoking the constructor function SafeMakeAccount(...)
, rather than
new SafeMakeAccount(...)
. This pattern is key to creating safe
objects in Javascript.
6 Are safe objects are no longer extensible?
The flip side is that safe objects in are no longer extensible. More precisely, a safe object may be extended, but the new methods in safe object are not privy to the object's state variables.
var getBalance = function() { return balance; }; a2.getBalance = getBalance; try { a2.getBalance(); // throws "ReferenceError: balance is not defined" } catch (e) { "undefined"; }; // => undefined
7 Extending safe object behaviour through delegation
A safe object extends behaviour by delegation. It mimics the interface of the parent object by including the parent as one of its private fields. By default, it inherits the parent's methods. However, it overrides certain methods of the parent by redefining them. The body of the overriding method optionally calls the parent's method, i.e., delegates to it. Notice that the object still has no access to its parent's state; only its methods.
var PersonalAccount = function(name, balance) { var personName = name; var a = SafeBankAccount(balance); var b = Object.beget(a); var withdraw = function(v) { if (v < 10) { console.log("withdrawal amount needs to be at least 10"); } else { a.withdraw(v); } }; var showName = function() { return personName; }; var show = function() { console.log(showName() + " has balance " + a.showBalance()); }; b.withdraw = withdraw; b.showName = showName; b.show = show; return b; }; var a3 = PersonalAccount("Anil", 500); a3.show(); // => Anil has balance 500 a3.withdraw(100); a3.showBalance(); // => 400 a3.withdraw(1); // withdrawal amount needs to be at least 10
8 Source code
- ./safe.js
- Design pattern for safe Javascript objects.