JavaScript logo

How to identify JavaScript inheritance

eslint-plugin-crc generates CRC Models by evaluating static source code that has been transformed into abstract syntax trees (ASTs). This document defines inheritance mechanisms with source code examples, references the sample source code's AST generated with the espree JavaScript parser in AST Explorer, and provides esquery selectors that identify inheritance.

Table of contents

1. Prototype-based inheritance

JavaScript is an object-based language based on prototypes, rather than being class-based. Because of this different basis, it can be less apparent how JavaScript allows you to create hierarchies of objects and to have inheritance of properties and their values.

Details of the object model. (n.d.). Retrieved August 01, 2017, from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model

Despite ES2105's implementation of class, extends, super, static keywords, JavaScript did not suddenly become a class-based language. Programmers who hope to corral JavaScript complexity must first understand that JavaScript implements two essential OOP features -- inheritance and polymorphism -- with prototype chains instead of classes. Indeed, it is vital to identify and define all possible OOP techniques in JavaScript in order to generate Class-Responsibilities-Collaborators model reports.

2. JavaScript objects defined

JavaScript is a loosely-typed or dynamically typed language. When we refer to JavaScript objects, we mean just that: fundamental objects.

In their simplest form, JavaScript objects are merely "collections" of properties.

const o = {a: 1}

// The newly created object o has Object.prototype as its [[Prototype]]
// o has no own property named 'hasOwnProperty'
// hasOwnProperty is an own property of Object.prototype.
// So o inherits hasOwnProperty from Object.prototype
// Object.prototype has null as its prototype.
// o ---> Object.prototype ---> null

const b = ['yo', 'whadup', '?']

// Arrays inherit from Array.prototype
// (which has methods indexOf, forEach, etc.)
// The prototype chain looks like:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2
}

// Functions inherit from Function.prototype
// (which has methods call, bind, etc.)
// f ---> Function.prototype ---> Object.prototype ---> null

An object method is simply a property whose value is a function:

const o = {
  a: 1,
  f: function () {
    return 2
  }
}

When we declare functions that contain this expressions, these functions can be used as constructors:

function Polygon(height, width) {
  this.height = height
  this.width = width
}

3. Prototypal inheritance

Unlike class-based object-oriented (OO) languages, Javascript "inherits" properties, methods, and constructors by linking to a hierarchy of prototypes. JavaScript uses object prototypes to effect inheritance by traversing a hierarchy of prototypes. This hierarchy is called the prototypal chain.

Every object in Javascript has a prototype. When a messages reaches an object, JavaScript will attempt to find a property in that object first, if it cannot find it then the message will be sent to the object’s prototype and so on. This works just like single parent inheritance in a class based language.

JavaScript inheritance via prototypal chains

Porto, S. (n.d.). Sebastian's Blog: A Plain English Guide to JavaScript Prototypes. Retrieved August 03, 2017, from http://sporto.github.io/blog/2013/02/22/a-plain-english-guide-to-javascript-prototypes/

In order to generate accurate CRC Models that engineers can use for refactoring, we must first:

  1. Define the ways objects can establish prototypal chains (and thereby "inherit" other properties and functions), and
  2. Create queries that will identify inheritance whenever it occurs in abstract syntax trees (ASTs).

4. Property inheritance

There are four ways to effect property inheritance in JavaScript.

Property inheritance Selector
1. Function.prototype.call() FunctionDeclaration > BlockStatement > ExpressionStatement > CallExpression
2. Function.prototype.apply() FunctionDeclaration > BlockStatement > ExpressionStatement > CallExpression
3. this.propertyName assignment ...
4. Function.prototype.propertyName assignment ...

4.1. Annotated source code example

// Option 1: Function.prototype.call()
function Polygon(height, width) {
  this.height = height
  this.width = width
}

function Square(length) {
  Polygon.call(this, length, length)

  this.area = function() {
    return this.height * this.width
  }
}

let s = new Square(2)

console.log(s.area())
// => 4


// Option 2: Function.prototype.apply()
Polygon = function(height, width) {
  this.height = height
  this.width = width
}

Square = function(length) {
  // apply is nearly identical to call, except for its
  // second argument, which should be an Array
  Polygon.apply(this, [length, length])

  this.area = function() {
    return this.height * this.width
  }
}

s = new Square(3)
console.log(s.area())
// => 9

// Option 3: this.propertyName AssignmentExpression
const PersonName = function (familyName = null, givenName = null) {
  this.familyName = familyName
  this.givenName = givenName
}

function Contact(options) {
  this.name = new PersonName(options.familyName, options.givenName)
}

let contact = new Contact({
  familyName: 'Swindle',
  givenName: 'Gregory'
})
console.log(PersonName.prototype.isPrototypeOf(contact.name))
// => true

// Option 4: Function.prototype.propertyName AssignmentExpression
function Address() {}

Contact.prototype.address = new Address()

contact = new Contact({
  familyName: 'Swindle',
  givenName: 'Gregory'
})

console.log(Object.getPrototypeOf(contact.address))
// => Address {}

4.2. Property inheritance identification

4.2.1. By Function.prototype.call()

const espree = require('espree')
const esquery = require('esquery')

const settings = {
  ecmaVersion: 6,
  range: false,
  loc: false,
  tokens: false
}

const omitAllBy = (expr, predicate) => {
  for (let prop in expr) {
    if (predicate(expr[prop])) {
        delete expr[prop]
    } else if (typeof expr[prop] === 'object') {
        omitAllBy(expr[prop], predicate)
    }
  }
}

const format = (expression) => {
  const expr = Object.assign({}, expression)
  omitAllBy(expr, Number.isInteger)
  omitAllBy(expr, (val) => typeof val === 'boolean')
  return expr
}

const code = `
function Polygon(height, width) {
  this.height = height
  this.width = width
}
function Square(length) {
  Polygon.call(this, length, length)

  this.area = function() {
    return this.height * this.width
  }
}`

const ast = espree.parse(code, settings)
const selector = esquery.parse('FunctionDeclaration > BlockStatement > ExpressionStatement > CallExpression')

const callExpression = format(esquery.match(ast, selector).pop())

JSON.stringify(callExpression, null, 2)
/* =>
[
  {
    "type": "CallExpression",
    "callee": {
      "type": "MemberExpression",
      "computed": false,
      "object": {
        "type": "Identifier",
        "name": "Polygon"
      },
      "property": {
        "type": "Identifier",
        "name": "call"
      }
    },
    "arguments": [
      {
        "type": "ThisExpression"
      },
      {
        "type": "Identifier",
        "name": "length"
      },
      {
        "type": "Identifier",
        "name": "length"
      }
    ]
  }
]
*/

4.2.2. By Function.prototype.apply()

const espree = require('espree')
const esquery = require('esquery')

const settings = {
  ecmaVersion: 6,
  range: false,
  loc: false,
  tokens: false
}

const omitAllBy = (expr, predicate) => {
  for (let prop in expr) {
    if (predicate(expr[prop])) {
        delete expr[prop]
    } else if (typeof expr[prop] === 'object') {
        omitAllBy(expr[prop], predicate)
    }
  }
}

const format = (expression) => {
  const expr = Object.assign({}, expression)
  omitAllBy(expr, Number.isInteger)
  omitAllBy(expr, (val) => typeof val === 'boolean')
  return expr
}

const code = `
function Polygon(height, width) {
  this.height = height
  this.width = width
}
function Square(length) {
  Polygon.apply(this, [length, length])

  this.area = function() {
    return this.height * this.width
  }
}`

const ast = espree.parse(code, settings)
const selector = esquery.parse('FunctionDeclaration > BlockStatement > ExpressionStatement > CallExpression')

const callExpression = format(esquery.match(ast, selector).pop())

JSON.stringify(callExpression, null, 2)

4.2.3. By this.propertyName assignment

function PersonName(options) {
  const opts = options || {}
  this.givenName = opts.givenName
  this.familyName = opts.familyName
  this.middleName = opts.middleName
}
function Person(name) {
  this.name = new PersonName(name)
}

function PersonName(options) {
  const opts = options || {}
  this.givenName = opts.givenName
  this.familyName = opts.familyName
  this.middleName = opts.middleName
}
function Person() {
  this.name = null
}

let p = new Person()
p.name = new PersonName({
  givenName: 'Johann',
  familyName: 'von Hautkopft of Ulm',
  middleName: 'Gambolputty de von Ausfern-schplenden-schlitter-crasscrenbon-fried-digger-dingle-dangle- dongle-dungle-burstein-von-knacker-thrasher-apple-banger-horowitz- ticolensic-grander-knotty-spelltinkle-grandlich-grumblemeyer- spelterwasser-kurstlich-himbleeisen-bahnwagen-gutenabend-bitte-ein- nurnburger-bratwustle-gernspurten-mitz-weimache-luber-hundsfut- gumberaber-shonedanker-kalbsfleisch-mittler-aucher'
})

4.2.4. By Function.prototype.propertyName assignment

5. Method inheritance

Method inheritance
1. this.methodName = FunctionExpression
2. Function.prototype.methodName = FunctionExpression

5.1. Annotated source code example

5.2. Method inheritance identification

5.2.1. By this.methodName = FunctionExpression

5.2.2. By Function.prototype.methodName = FunctionExpression

6. Constructor inheritance

Constructor functions can be inherited three ways.

Constructor inheritance Selector
1. Fxn2.prototype = new Fxn1() ExpressionStatement > [left.property.name="prototype"][right.type="NewExpression"]
2. Fxn2.prototype = Object.create(Fxn1.prototype) AssignmentExpression[left.property.name="prototype"][right.type="CallExpression"],[right.callee.object.name="Object"][callee.property.name="create"][arguments] [object][property.name="prototype"]
3. Fxn2.prototype.constructor = Fxn2 AssignmentExpression[left.property.name="constructor"][right.type="Identifier"]

6.1. Annotated source code example

function Polygon(height, width) {
  this.height = height
  this.width = width
}

function Square(sideLength) {
  // Inherit Polygon's properties.
  // Without Polygon.call (or Polygon.apply),
  // s.hasOwnProperty('height') => false
  Polygon.call(this, sideLength, sideLength)
}

// Inherit Polygon's constructor
// Omitting this results in
// s instanceof Polygon => false

// Option 1: by NewExpression
Square.prototype = new Polygon()

// Option 2: by Object.create
Square.prototype = Object.create(Polygon.prototype)

// Option 3: by constructor assignment
Square.prototype.constructor = Polygon

let p = new Polygon(2, 2)
// p is an object with own properties 'height' and 'width'
// p.[[Prototype]] is the value of Polygon.prototype when new Polygon is executed

let s = new Square(2)
// s is an object with own properties 'height' and 'width'
// s.[[Prototype]] is the value of Polygon.prototype when new Square is executed

s instanceof Square
// => true
s instanceof Polygon
// => true
p instanceof Square
// => false
Polygon.prototype.isPrototypeOf(s)
// => true

6.2. Constructor inheritance identification

Given the following FunctionDeclarations:

function Polygon(height, width) {
  this.height = height
  this.width = width
}

function Square(sideLength) {
  Polygon.call(this, sideLength, sideLength)
}

Identify constructor inheritance.

6.2.1. By NewExpression

// Option 1: by NewExpression
Square.prototype = new Polygon()

esquery selector:

ExpressionStatement > [left.property.name="prototype"][right.type="NewExpression"]
// =>
[
  {
    "type": "AssignmentExpression",
    "operator": "=",
    "left": {
      "type": "MemberExpression",
      "computed": false,
      "object": {
        "type": "Identifier",
        "name": "Square"
      },
      "property": {
        "type": "Identifier",
        "name": "prototype"
      }
    },
    "right": {
      "type": "NewExpression",
      "callee": {
        "type": "Identifier",
        "name": "Polygon"
      },
      "arguments": []
    }
  }
]

6.2.2. By Object.create

// Option 2: by Object.create
Square.prototype = Object.create(Polygon.prototype)

esquery selector:

AssignmentExpression[left.property.name="prototype"][right.type="CallExpression"],[right.callee.object.name="Object"][callee.property.name="create"][arguments] [object][property.name="prototype"]
// =>
[
  {
    "type": "AssignmentExpression",
    "operator": "=",
    "left": {
      "type": "MemberExpression",
      "computed": false,
      "object": {
        "type": "Identifier",
        "name": "Square"
      },
      "property": {
        "type": "Identifier",
        "name": "prototype"
      }
    },
    "right": {
      "type": "CallExpression",
      "callee": {
        "type": "MemberExpression",
        "computed": false,
        "object": {
          "type": "Identifier",
          "name": "Object"
        },
        "property": {
          "type": "Identifier",
          "name": "create"
        }
      },
      "arguments": [
        {
          "type": "MemberExpression",
          "computed": false,
          "object": {
            "type": "Identifier",
            "name": "Polygon"
          },
          "property": {
            "type": "Identifier",
            "name": "prototype"
          }
        }
      ]
    }
  }
]

6.2.3. By constructor assignment

// Option 3: by constructor assignment
Square.prototype.constructor = Polygon

esquery selector:

AssignmentExpression[left.property.name="constructor"][right.type="Identifier"]
// =>
[
  {
    "type": "AssignmentExpression",
    "operator": "=",
    "left": {
      "type": "MemberExpression",
      "computed": false,
      "object": {
        "type": "MemberExpression",
        "computed": false,
        "object": {
          "type": "Identifier",
          "name": "Square"
        },
        "property": {
          "type": "Identifier",
          "name": "prototype"
        }
      },
      "property": {
        "type": "Identifier",
        "name": "constructor"
      }
    },
    "right": {
      "type": "Identifier",
      "name": "Polygon"
    }
  }
]

7. class inheritance

ES6 implemented the reserved class keyword. Many refer to JavaScript class as "syntax sugar," since it provides a "sweet," convenient way for objects to inherit another object's properties, methods, and constructor using a familiar and succinct syntax. Others decry class, since they believe it only perpetuates misunderstanding, and implies that JavaScript is class-based.

Class inheritance Selector
extends and super ClassDeclaration[superClass]

7.1. Annotated source code example

Given the following source code:

class Polygon {
  constructor(height, width) {
    this.height = height
    this.width = width
    this.name = 'Polygon'
  }
}

class Square extends Polygon {
  constructor(length) {
    // Call the parent class's constructor with lengths
    // provided for the Polygon's width and height
    super(length, length)
    // Note: In derived classes, super() must be called before you
    // can use 'this'. Leaving this out will cause a reference error.
    this.name = 'Square'
  }

  get area() {
    return this.height * this.width
  }

  set area(value) {
    this.height = this.width = Math.sqrt(value)
    this.area = value
  }
}

We want to identify class inheritance with esquery.

7.2. Class inheritance identification by extends and super

View in the source code's abstract syntax tree in AST Explorer

const espree = require('espree')
const esquery = require('esquery')

const settings = {
  ecmaVersion: 6,
  range: false,
  loc: false,
  tokens: false
}

const omitAllBy = (expr, predicate) => {
  for (let prop in expr) {
    if (predicate(expr[prop])) {
        delete expr[prop]
    } else if (typeof expr[prop] === 'object') {
        omitAllBy(expr[prop], predicate)
    }
  }
}

const format = (expression) => {
  const expr = Object.assign({}, expression)
  omitAllBy(expr, Number.isInteger)
  omitAllBy(expr, (val) => typeof val === 'boolean')
  return expr
}

const code = `
class Polygon {
  constructor(height, width) {
    this.height = height
    this.width = width
    this.name = 'Polygon'
  }
}

class Square extends Polygon {
  constructor(length) {
    // Call the parent class's constructor with lengths
    // provided for the Polygon's width and height
    super(length, length)

    // Note: In derived classes, super() must be called before you
    // can use 'this'. Leaving this out will cause a reference error.
    this.name = 'Square'
  }

  get area() {
    return this.height * this.width
  }

  set area(value) {
    this.height = this.width = Math.sqrt(value)
    this.area = value
  }
}
`

const ast = espree.parse(code, settings)
// esquery selector "ClassDeclaration[superClass]"
const selector = esquery.parse('ClassDeclaration[superClass]')

const callExpression = format(esquery.match(ast, selector).pop())

JSON.stringify(callExpression, null, 2)

7.3. Traditional "class" inheritance

The article "JSClassFinder: A Tool to Detect Class-like Structures in JavaScript" prescribes two definitions for identifying what the authors call "classes":

An object is a tuple (C, A, M), where

  1. C is the object name,
  2. A = {a1, a2, . . . , ap} are the attributes defined by the object, and
  3. M = {m1, m2, . . . , mq} are the methods.

Note: In the following definitions, the variable A (object attributes) is synonymous with the official ECMAScript term property.

7.3.1. Definition 1

An object (C, A, M), defined in a program P, must respect the following conditions:

  1. P must have a function with name C.
  2. P must include at least one expression of type
    • new C()
    • Object.create(C.prototype)
  3. For each aA,

    • The function C must include an assignment this.a = {Exp} or
    • P must include an assignment C.prototype.a = {Exp}.
  4. For each mM,

    • function C must include an assignment this.m = function {Exp} or
    • P must include an assignment C.prototype.m = function {Exp}.

7.3.2. Definition 2: constructor assignments

Assuming that (C1, A1, M1) and (C2, A2, M2) are objects in a program P, we define that C2 is a prototype of C1 if one of the following conditions holds:

  1. P includes an assignment C2.prototype = new C1().
  2. P includes an assignment C2.prototype = Object.create(C1.prototype).
  3. P includes an assignment C2.prototype.constructor = C2.

Define:

  • Properties in constructors
  • Methods
    • On an Object's prototype
    • In a class definition (but not within its constructor)

As @jeffrose kindly pointed out on GitHub,

...one thing that might be worth calling out is how the different kinds of inheritance produce functionally equivalent but different results.

function FooBar(){
    this.bar = function(){}
}
FooBar.prototype.foo = function(){}

console.log( new FooBar().foo === new FooBar().foo ); // true
console.log( new FooBar().bar === new FooBar().bar ); // false

When doing prototypal inheritance, the class instances are sharing a function. When doing constructor inheritance, the class instances are getting their own copy.

In terms of best practices, you typically want to inherit properties via constructor inheritance and methods via prototypal inheritance.

It's also worth mentioning that the ES6 class keyword follows this advice.

// Constructor inheritance
function FooBar() {
  this.bar = function(){}
}

// Prototype inheritance
FooBar.prototype.foo = function(){}

// Class inheritance
class Snafu {
  constructor() {
    this.bar = function(){}
  }

  // Defining class functions outside the
  // class's constructor is equivalent to
  // ObjectName.proptotype.fn = function(){}
  foo() {}
}
console.log( new FooBar().foo === new FooBar().foo ); // true
console.log( new FooBar().bar === new FooBar().bar ); // false
console.log( new Snafu().foo === new Snafu().foo ); // true
console.log( new Snafu().bar === new Snafu().bar ); // false

9. References

results matching ""

    No results matching ""