The Principles of Object-Oriented JavaScript

By Nicholas C. Zakas

Primitive and Reference Types

  • Primitive types – are stored as simple data types (boolean, number, string, null, undefined)
  • Reference types – are stored as objects, which are just references to locations in memory. Reference values are instances of reference types and are synonymous with objects (unordered list of properties consisting of a name and a value). When the value is a function, it is referred to as a method.
  • Variable object – scoped data, primitive values are stored directly on the variable object where reference values are placed as a pointer in the variable object – serving as a reference to a location in memory where the object is stored. A value is copied when a primitive is assigned to a variable.
  • The primitives boolean, string, and number also have methods but are NOT objects. JavaScript makes them look this way for a consistent experience for development by using primitive wrapper types. These wrapper types use autoboxing, a quick instantiate and destroy of JS primitives for temporary use in processing values.
  • Bracket notation – allows object property access via string literals or variables vs dot notation. This can be extremely helpful when dynamically deciding which property to access
    • EX. var array = []; var method = “push”; array[method];
  • instanceof – can correctly identify a reference type against its inheritance (inherited base is Object)
  • In ECMAScript 5 Array.isArray() was added as JavaScript values can be passed between Frames and each page has its own global context (instanceof Array in one Frame differs from the Array in another Frame)

Functions

  • The defining characteristic of a Function vs an Object is an internal property named [[Call]].
  • There are two literal forms:
    • function declaration – function add(num1, num2) { return num1 + num2; }
    • function expression – var add = function(num1, num2){ return num1 + num2; };
    • A function declaration and expression are identical except for the fact that
      • An expression lacks a name (making it an anonymous function)
      • An expression has a semicolon that concludes the assignment of the function to a variable (just like any other assignment needing a semicolon to conclude it).
      • A declaration is hoisted to the top of the context. Function hoisting occurs because the function name is known ahead of time (same as variable names known ahead of time).
  • All parameters are stored in an array-like arguments structure, which is automatically available inside any function (along with this). Named parameters exist mostly for convenience and do not limit the number of arguments a function can accept.
  • The arguments.length property indicates the function’s arity, or the # of expected parameters.
  • Every scope in JS has a this object that represents the calling object for the function.
  • Remember that Functions are objects which means they too can have methods.
  • You can use call() and apply() to have a function execute with a specific this argument in addition to custom parameters. Both behave identically except that apply() takes only a single additional argument which is an array of parameters as opposed to an arbitrary amount of parameters
  • Because JavaScript has no concept of a class, Functions and Objects are used to achieve aggregation and inheritance.

Understanding Objects

  • Unlike class-based languages that lock down objects based on class definition, JavaScript objects are dynamic meaning they can change at any point during runtime.
  • When a property is assigned to an object, JavaScript uses an internal [[Put]] method which creates an own property indicator. This indicates that the specific instance of the object owns that property. The property is stored directly on the instance. *Own properties are distinct from prototype properties
  • When a new value is assigned to an existing property an internal [[Set]] operation replaces the current value with the new one
  • Use the in operator to determine if a property exists in an object. Take note however that in checks for both own properties and prototype properties. Use hasOwnProperty to exclude prototype properties.
    • Ex. if(“name” in personObject){ //do something }
  • To remove a property from an object use delete, this calls an internal [[Delete]] operation. A successful operation returns true.
  • All properties added to an object by default are enumerable – that is they can be iterated over using a for-in loop (this includes both own properties and prototype properties). Most native methods are not enumerable and you can check this on an object using propertyIsEnumerable
  • There are two different types of properties:
    • data properties – contain a value
    • accessor properties – don’t contain a value but instead define a function to call when the property is read (getter) or written to (setter). Accessor properties require a getter, setter, or both. If you only include one, the property becomes read/write only depending on if it was get/set. You only really need to use an accessor property if an action (get) or calculation (set) needs to be accomplished prior. You can create one with an object literal like so:
      • var person = { name: “Bob”, get name(){ return this.name; }, set name(v) { name = v; }};
  • [[Enumerable]] and [[Configurable]] are the two property attributes shared between data and accessor properties. You use Object.defineProperty() to change property attributes.
  • Objects have an internal [[Extensible]] attribute (indicating it can be modified (true by default))

Constructors and Prototypes

  • Constructor – a function that is used with the new operator to create an object. As a result Object, Array, and Function are constructors. Custom constructors are just custom functions whose name is capitalized.
  • Every object instance has a constructor property containing a reference to the constructor function that created it. For object literals and the Object constructor, the constructor property is set to Object. Custom constructors have a constructor property pointing back to the custom constructor function.
  • The this object is automatically created by new when a constructor is called, and it is an instance of the constructor’s type. new is also responsible for producing the return value so the function itself doesn’t need to return anything.
    • You can explicitly call return inside a constructor. If the return value is an object it returns instead of the newly created object instance (primitive values returned are ignored)
  • Prototype – a recipe for an object that is used during the creation of new instances where all instances share the same prototype object and thus its properties/methods
  • A core benefit of prototypes is that you can define a method once on the prototype of a custom object and each instance uses its own this for context when using the methods defined on the prototype.
  • An instance tracks its prototype via the internal property [[Prototype]] which is a pointer to the prototype object the instance is using. When using new the constructor’s prototype property is assigned to the internal [[Prototype]] property of the newly created object
  • When a property is read on an object a search ensues:
    • For an own property match
    • Then its prototype object’s property match via [[Prototype]]
    • Then that prototype’s prototype object’s property match (repeats down the chain until Object.prototype is hit)
    • Then undefined
  • An own property that matches a prototype property is known as a shadow property (the own version is used instead of the prototype version)
  • If assigning an object literal to a custom object’s prototype, the constructor property is overwritten to point to Object vs the custom object. So in the literal ensure constructor: YourCustomObjectName is reset, as the Prototype defines the constructor pointer which in turn is used by instances
  • Though possible, don’t modify the prototype of built-in types

Inheritance

  • Prototype chaining is the built-in inheritance model in JS. An object inherits from its prototype, while that prototype in turn inherits from its prototype, and so on; this is the prototype chain
  • All objects inherit from Object.prototype automatically and thus have these properties: hasOwnProperty, propertyIsEnumerable, isPrototypeOf, valueOf, toString
  • valueOf is called on an object whenever an operator is used (and it simply returns the object instance)
  • toString is called as a fallback to valueOf when the returned value is a reference value vs primitive
  • The simplest form of inheritance if to specify what object should be a new object’s prototype. You can use Object.create(pPrototype, pPrototypeDescriptors); to create a new object with a specific prototype object
  • Object.create(null) creates a naked object which is perfect for a lookup hash (with no naming collisions with inherited property names)
  • instanceof uses the prototype chain when looking up an instance’s type
  • Constructor stealing is calling the supertype constructor from the subtype constructor using call/apply
  • For pseudoclassical inheritance you:
    • Modify the constructor’s prototype (right after defining a new class – custom function object)
    • Call the supertype constructor from within the subtype constructor (call/apply)
  • Combining constructor stealing and prototype chaining is the most common way to achieve inheritance between custom types in JS (commonly referred to as pseudoclassical inheritance)

Object Patterns

  • Closures are simply functions that access data outside their own scope. If these functions are nested within an outer function they then have access to the properties defined in that outer function
  • The revealing module pattern utilizes this closure concept but goes a step further by returning an object literal that reveals the properties/methods of the outer function in which it is encapsulated
  • The mixin pattern accomplishes psuedoinheritance by copying the enumerable properties of a supplier object onto a receiver object via a for-in loop & bracket notation (receiver[property] = supplier[property])
  • scope-safe constructors for functions execute differently based on whether new was used or not