Review of object basics.
Prototypes and inheritance.
Use of class
syntax.
Property meta-information.
Monkey patching.
An object is merely a named collection of name-value pairs (which include functions). Values are referred to as object properties.
> x = { a: 9 } //Object literal notation { a: 9 } > x.a 9 > delete x.a true > x {} > delete x.a //false returned only for non-config true //property in non-strict mode >
> x = { a: 9, //anon function is value for f f: function(a, b) { return a + b; }, g(a, b) { return a*b; }, //sugared syntax neg: a => -a, } > x.g(3, 4) 12 > x.neg(2) -2 > x = 'a' 'a' > { [x]: 42 } //dynamic property name. { a: 42 } > { x } //prop named 'x' with value x { x: 'a' } >
const c1 = { x: 1, y: 1, toString: function() { return `${this.x} + ${this.y}i` }, magnitude: function() { return Math.sqrt(this.x*this.x + this.y*this.y); } }
const c2 = { x: 3, y: 4, toString: function() { return `${this.x} + ${this.y}i` }, magnitude: function() { return Math.sqrt(this.x*this.x + this.y*this.y); } } console.log(`${c1.toString()}: ${c1.magnitude()}`); console.log(`${c2.toString()}: ${c2.magnitude()}`);
$ nodejs ./complex1.js 1 + 1i: 1.4142135623730951 3 + 4i: 5
Note that each complex number has its own copy of the toString()
and magnitude()
functions.
complexFns = { toString: function() { return `${this.x} + ${this.y}i` }, magnitude: function() { return Math.sqrt(this.x*this.x + this.y*this.y); } }
//use complexFns as prototype for c1 const c1 = Object.create(complexFns); c1.x = 1; c1.y = 1; //use complexFns as prototype for c2 const c2 = Object.create(complexFns); c2.x = 3; c2.y = 4; console.log(`${c1.toString()}: ${c1.magnitude()}`); console.log(`${c2.toString()}: ${c2.magnitude()}`);
Each object has an internal [[Prototype]]
property.
When looking up a property, the property is first looked
for in the object; if not found then it is looked for
in the object's prototype; if not found there, it is
looked for in the object's prototype's prototype.
The lookup continues up the prototype chain until
the property is found or the prototype is null
.
Note that the prototype chain is only used for property lookup. When a property is assigned to, the assignment is made directly in the object; the prototype is not used at all.
Prototype can be accessed using Object.getPrototypeOf()
or __proto__
property (supported by most browsers,
being officially blessed by standards, but is
no longer recommended).
The Object
class has many useful
methods. Some particularly useful ones:
Returns new object with prototype proto
.
Assign source
properties to target
, with later source properties
overwriting earlier ones. Returns target
.
All non-inherited property names.
Enumerable keys, values and key-value pairs.
Every function has a prototype
property. The Function
constructor
initializes it to something which looks like { constructor: this }
.
Any function which is invoked preceeded by the new
prefix
operator is being used as a constructor.
Within the body of a function invoked as a constructor, this
refers to a newly created object instance with [[prototype]]
internal property set to the prototype
property of the function.
Hence the prototype
property of the function provides access to
the prototype for the object instance; specifically, assigning to
a property of the function prototype
is equivalent to assigning
to the object prototype.
By convention, constructor names start with an uppercase letter.
function Complex(x, y) { this.x = x; this.y = y; } Complex.prototype.toString = function() { return `${this.x} + ${this.y}i` }; Complex.prototype.magnitude = function() { return Math.sqrt(this.x*this.x + this.y*this.y); }; const c1 = new Complex(1, 1); const c2 = new Complex(3, 4); console.log(`${c1.toString()}: ${c1.magnitude()}`); console.log(`${c2.toString()}: ${c2.magnitude()}`);
Normally a constructor function does not explicitly return a value. In that case, the return value is set to a reference to the newly created object.
However, if the return value is explicitly set to an object (not a primitive), then that object is return'd from the constructor.
Makes it possible to have constructor hide instance variables using closure.
Makes it possible to have a constructor share instances by not returning the newly created instance.
Can use constructor return value to cache object instances to avoid creating a new instance unnecessarily.
const cache = { }; //... is pseudo-code, not rest function SomeInstance(id, ...) { if (cache[id]) return cache[id]; //construct new instance as usual ... cache[id] = this; }
We could implement classical inheritance using a pattern like
Child.prototype = Object.create(Parent.prototype)
. Hence
Child
will inherit properties from Parent
.
Older code may use a pattern like Child.prototype = new
Parent()
. Problems include the fact that the Parent
constructor is run (which may have undesirable side-effects)
and how do we handle arguments to the Parent
constructor
if it expects arguments.
Note that we create a new object for Child's prototype rather than
simply Parent
as we do not want assignments to Child.prototype
to affect Parent
.
Since the constructor is stored in an object's prototype,
in both cases we need to fix up the constructor:
Child.prototype.constructor = Child
.
Problematic in that we need to apply this pattern. Could wrap
within a function inherit()
, but still messy (see Crockford).
Also, classical inheritance is generally problematic.
Added in es6 to make programmers coming in from other languages more comfortable.
Create a new class using a class declaration.
Create a new class using a class expression.
Inheritance using extends
.
Static methods.
Can extend builtin classes.
Private instance and static fields and methods (since ES2022).
Static initialization blocks (since ES2022).
Very thin layer around prototype-based inheritance. See this for tradeoffs.
class Shape { constructor(x, y) { this.x = x; this.y = y; } //possibly poor design static distance(s1, s2) { const xDiff = s1.x - s2.x; const yDiff = s1.y - s2.y; return Math.sqrt(xDiff*xDiff + yDiff*yDiff); } }
class Rect extends Shape { constructor(x, y, w, h) { super(x, y); this.width = w; this.height = h; } area() { return this.width*this.height; } } class Circle extends Shape { constructor(x, y, r) { super(x, y); this.radius = r; } area() { return Math.PI*this.radius*this.radius; } }
const shapes = [ new Rect(3, 4, 5, 6), new Circle(0, 0, 1), ]; shapes.forEach((s) => console.log(s.x, s.y, s.area())); console.log(Shape.distance(shapes[0], shapes[1]));
$ ./shapes.js 3 4 30 0 0 3.141592653589793 5 $
> a = { x: 22 } { x: 22 } > Object.getOwnPropertyDescriptors(a) { x: { value: 22, writable: true, enumerable: true, //loop for...in configurable: true } } //change descr; delete > Object.defineProperty(a, 'y', {}) { x: 22 }
> Object.getOwnPropertyDescriptors(a) { x: { value: 22, writable: true, enumerable: true, configurable: true }, y: { value: undefined, writable: false, enumerable: false, configurable: false } }
> delete(a['x']) true > Object.getOwnPropertyDescriptors(a) { y: { value: undefined, writable: false, enumerable: false, configurable: false } } > delete(a['y']) false > Object.getOwnPropertyDescriptors(a) { y: { value: undefined, writable: false, enumerable: false, configurable: false } } >
> obj = { get len() { return this.value.length; } } { len: [Getter] } > obj.value = [1, 2] [ 1, 2 ] > obj.len 2 > obj.value = [1, 2, 3] [ 1, 2, 3 ] > obj.len 3
Use property x
as proxy for property _x
while counting # of
changes to property x
.
> obj = { nChanges: 0, get x() { return this._x; }, set x(v) { if (v !== this._x) this.nChanges++; this._x = v; } }
> obj.x undefined > obj.x = 22 22 > obj.nChanges 1 > obj.x = 42 42 > obj.nChanges 2 > obj.x = 42 42 > obj.nChanges 2 >
#
prefixed names are private in JS. Added in ES2022:
> class C { publicField = 22; #privateField = 33; static staticField; static #privateStatic = 55; #privateMethod() { return this.#privateField; } get someField() { return this.#privateMethod(); } static { console.log('static initialization block'); C.staticField = 44; } } static initialization block undefined
> c = new C() C { publicField: 22 } > c.someField 33 > c.staticField undefined > C.staticField 44 > C.#privateStatic; C.#privateStatic; ^ Uncaught: SyntaxError: Private field '#privateStatic' must be declared in an enclosing class >
TypeScript has allowed private
keyword in class declarations for
many years.
Cannot define const
within a class
; following results in
a syntax error:
class C { static const constant = 42; }
Use following pattern:
class C { static #C = 42; static get constant() { return C.#C; } } console.log(C.constant);
For both ==
and ===
, objects are equal only if they have the same
reference.
> {} == {} false > {} === {} false > x = {} {} > y = x {} > x == y true > x === y true >
Arrays are like objects except:
It has an auto-maintained length
property (always set to 1 greater
than the largest array index).
Arrays have their prototype set to Array.prototype
( Array.prototype
has its prototype set to Object.prototype
, hence arrays inherit
object methods).
for (let v in object) { ... }
Sets v to successive enumerable properties in object including inherited properties.
No guarantee on ordering of properties; specifically, no guarantee
that it will go over array indexes in order. Better to use plain
for
or for-of
.
Will loop over enumerable properties defined within the object as well as those inherited through the prototype chain.
If we want to iterate only over local properties, use
getOwnPropertyNames()
or hasOwnProperty()
to filter.
> a = { x: 1 } { x: 1 } > b = Object.create(a) //a is b's prototype {} > b.y = 2 2 > for (let k in b) { console.log(k); } y x undefined > for (let k in b) { if (b.hasOwnProperty(k)) console.log(k); } y undefined
> names = Object.getOwnPropertyNames(b) [ 'y' ] > for (let k in names) { console.log(k); } 0 undefined for (k of names) { console.log(k); } y undefined >
> x = {a : 1, b: 2 } { a: 1, b: 2 } > Object.defineProperty(x, 'c', { value: 3}) //not enumerable { a: 1, b: 2 } > x.c 3 > for (let k in x) { console.log(k); } a b undefined > x.c 3 >
Built-in types can be changed at runtime: monkey-patching.
> ' abcd '.trim() 'abcd' > ' abcd '.ltrim() //trim only on left TypeError: " abcd ".ltrim is not a function > String.prototype.ltrim = String.prototype.ltrim || //do not change function() { return this.replace(/^\s+/, ''); } [Function] > ' abcd '.ltrim() 'abcd ' >
> const oldFn = String.prototype.replace undefined > String.prototype.replace = function(a1, a2) { const v = oldFn.call(this, a1, a2); console.log(`${this}.replace(${a1}, ${a2})=>${v}`); return v; } [Function] > ' aabcaca'.replace(/aa+/, 'x') aabcaca.replace(/aa+/, x)=> xbcaca ' xbcaca' > ' aabcaca'.replace(/a/g, (x, i) => String(i)) aabcaca.replace(/a/g, (x, i) => String(i))=> 12bc5c7 ' 12bc5c7' >