kota's memex

traditional constructors

// Create an object with a specific prototype
let protoRabbit = {
	speak(line) {
		console.log(`The ${this.type} rabbit says '${line}'`)
	}
}
let killerRabbit = Object.create(protoRabbit)
killerRabbit.type = "killer"
killerRabbit.speak("SKREEEE!")
// The killer rabbit says 'SKREEEE!'

Prototypes can be thought of as similar to classes in other languages. They're useful for defining properties and methods which are passed on to instances. Properties that differ on a per instance level, such as the above rabbit's type value, need to be stored in the objects directly. In javascript constructor functions are used to create an instance of a given prototype and set it's default values. The new keywork makes defining these type of functions easier.

If you put the new keywork in front of a function call, the function is treated as a constructor. This means that an object with the right prototype is automatically created, bound to this in the function, and returned at the end of the function. By convention, the names of constructors are capitalized so they can quickly be distinguished from other functions.

function Rabbit(type) {
	this.type = type
}

Rabbit.prototype.speak = function(line) {
	console.log(`The ${this.type} rabbit says '${line}'`)
}

let weirdRabbit = new Rabbit("weird")

One quirk is that the prototype associated with a contructor is actually different a property prototype of the constructor. The constructor itself will have the prototype of Function.prototype.

console.log(Object.getPrototypeOf(Rabbit) ==
  Function.prototype)
// true

console.log(Object.getPrototypeOf(weirdRabbit) ==
  Rabbit.prototype)
// true

ES6 class notation

class Rabbit {
	constructor(type) {
		this.type = type;
	}
	speak(line) {
		console.log(`The ${this.type} rabbit says '${line}'`)
	}
}

let killerRabbit = new Rabbit("killer")
let blackRabbit = new Rabbit("black")

Any number of methods may be written inside the declaration's braces. The one named constructor is treated specially. It provides the actual constructor function, which will be bound to the name Rabbit. The others are packaged into that constructor's prototype. Class declarations currently allow only methods (properties that hold functions) to be added to the prototype. This makes it inconvenient to add non-function properties. You can of course add them by manipulating the prototype after you've defined the class.

overriding derived properties

When you add a property to an object it will be added to the object itself regardless of if it was present in the prototype.

getters, setters, and statics

Interfaces often consist mostly of methods, but it's okay to include properties that hold non-function values. It is not even necessary for such an object to computer and store such a property in the instance. Even properties that are accessed directly may hide a method call. Such methods are called getters, and the are defined by writing get in front of the method name in an object expression or class declaration.

let varyingSize = {
	get size() {
		return Math.floor(Math.random() * 100)
	},
}

console.log(varyingSize.size)
console.log(varyingSize.size)

A setter allows doing something similar when the property is written into and static can be used to store a method in the constructor which in the below example is used to create an "alternative" constructor:

class Temperature {
	constructor(celsius) {
		this.celsius = celsius
	}
	get fahrenheit() {
		return this.celsius * 1.8 + 32
	}
	set fahrenheit(value) {
		this.celsius = (value - 32) / 1.8
	}

	// Allow creating a temperature from fahrenheit.
	static fromFahrenheit(value) {
		return new Temperature((value - 32) / 1.8)
	}
}

let temp = new Temperature(22)
console.log(temp.fahrenheit) // 71.6
temp.fahrenheit = 86
console.log(temp.celsius) // 30
let ftemp = Temperature.fromFahrenheit(22)
console.log(ftemp.fahrenheit) // 22

inheritance

The use of the word extends indicates that this class shouldn't be directly based on the default Object prototype, but on some other class (the super class Matrix shown in js looping). To initialize a SymentrixMatrix instance, the constructor calls its super-class's constructor through the super keyword.

// SymentrixMatrix is a Matrix that is always symmetrical. Values stored at x,y
// are always the same as values at y,x.
class SymentrixMatrix extends Matrix {
	constructor(size, element = (x, y) => undefined) {
		super(size, size, (x, y) => {
      // To ensure the matrix is symmetrical, the constructor wraps the content
      // method to swap the coords below the diagonal.
			if (x < y) return element(y, x)
			else return element(x, y)
		})
	}

	set(x, y, value) {
    // We're redefining set, but using the super-class's set method in the new
    // one by calling super.set().
		super.set(x,y,value)
		if (x != y) {
			super.set(y, x, value)
		}
	}
}

let symmetrical = new SymentrixMatrix(5, (x,y) => `${x},${y}`)
console.log(symmetrical.get(2,3))

instanceof

This operator returns true/false if an object was derived from a particular class.

console.log([1] instanceof Array) // true