Strategies for prototype detection - gregswindle/eslint-plugin-crc GitHub Wiki
Contrary to ES6, ES7, and ES8 keywords and nomenclature, JavaScript has no classes. JavaScript is a prototype-based language in which properties and methods are defined on the Objects' constructor functions, not the object instances themselves.
Prototypes are the mechanism by which JavaScript objects inherit features from one another, and they work differently than inheritance mechanisms in classical object-oriented programming languages...each object has a prototype object, whose constructor defines its properties and methods. The object's prototype acts as a template object. An object's prototype object may also have a prototype object, which it inherits methods and properties from, and so on. This is often referred to as a prototype chain, and explains why different objects have properties and methods defined on other objects available to them. [1]
Please reference the MDN article "Object prototypes" for an overview of object prototypes and how they differ from Classes and their instances.
In JavaScript runtime contexts, we can discover an object's prototype using either Object.getPrototypeOf
or Reflect.getPrototypeOf
. These methods are not available for execution during static code analysis, however, so we need to define strategies for identifying an object's prototype based on a program's abstract syntax tree.
The article "JSClassFinder: A Tool to Detect Class-like Structures in JavaScript" prescribes two definitions for identifying what the authors call "classes":[2]
Note: In the following definitions, the variable A (object attributes) is synonymous with the official ECMAScript term
property
.
An object is a tuple (C
, A, M), where
-
C
is the object name, - A = {a1, a2, . . . , ap} are the attributes defined by the object, and
- M = {m1, m2, . . . , mq} are the methods.
Moreover, an object (C
, A, M), defined in a program P, must respect the following conditions:
-
P must have a function with name
C
. -
P must include at least one expression of type
new C()
orObject.create(C.prototype)
. -
For each a ∈ A,
- The function
C
must include an assignmentthis.a = Exp
or -
P must include an assignment
C.prototype.a = Exp
.
- The function
-
For each m ∈ M,
- function
C
must include an assignmentthis.m = function {Exp}
or -
P must include an assignment
C.prototype.m = function {Exp}
.
- function
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:
-
P includes an assignment
C2.prototype = new C1()
. -
P includes an assignment
C2.prototype = Object.create(C1.prototype)
. -
P includes an assignment
C2.prototype.constructor = C2
. [3]
ECMAScript 2015 (aka ECMAScript 6) introduced the Class
syntax, which is a form of syntactic-sugar around Object prototypes. When using Classes
, we express prototypal inheritance with the extends
keyword [4], e.g.,
class CrcModelMarkdownFormatter extends CrcModelFormatter {
// stuff here
}
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:
-
P includes a declaration
class C2 extends C1
.
ECMAScript 6 also introduced the static method Object.setPrototypeOf
to enable prototypal inheritance.
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 P includes an expression Object.setPrototypeOf(C2.prototype, C1)
.
3. Identifying objects' prototypes in an (Espree-based) abstract syntax tree (AST)
Let's shift out gazes away from the fields of Academe and cast them down to our keyboards.
View the full example's abstract syntax tree on AST Explorer. You can also review the full source code in es5-object-prototypes.js, which is the fixture for eslint-plugin-crc
's Mocha specifications.
// _P_ must have a function with name `C`.
// FunctionDeclaration
function Person() {
this.canTalk = true;
};
// VariableDeclaration > VariableDeclarator > FunctionExpression
var Person = function () {
this.canTalk = true;
};
// Person === Program / body[2] / ExpressionStatement / AssignmentExpression / left:MemberExpression / object:MemberExpression / object: Identifier
// prototype === Program / body[2] / ExpressionStatement / AssignmentExpression / left:MemberExpression / object:MemberExpression / object:prototype
Person.prototype.greet = function() {
if (this.canTalk) {
console.log('Hi, I am ' + this.name);
}
};
// Employee === Program / body[3] / VariableDeclaration / declarations[0]:VariableDeclarator
var Employee = function(name, title) {
Person.call(this);
this.name = name;
this.title = title;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.greet = function() {
if (this.canTalk) {
console.log('Hi, I am ' + this.name + ', the ' + this.title);
}
};
var bob = new Employee('Bob', 'Builder');
bob.greet();
// Hi, I am Bob, the Builder