Object literal enhancements, classes, samples
satya - 12/24/2024, 3:44:44 PM
What are object literals
- When a variable is initialized with an object definition inline
- no functions are used
- No classes are used
- Just initializing a variable with an object in line
- How do you define properties
- Methods
- Dynamic properties
satya - 12/24/2024, 3:55:00 PM
A complex example
// External variables
const deviceType = "Smartphone";
const brand = "TechCo";
const serialNumber = 12345;
const featureBase = "feature";
// Initialize the object
const smartDevice = {
// Static properties
deviceType,
brand,
serialNumber,
// Dynamic property with a static key
releaseYear: 2024,
// Dynamic property with a computed key
[`${featureBase}_1`]: "Bluetooth Connectivity",
[`${featureBase}_2`]: "Wireless Charging",
// Function to show details
getDetails() {
return `Device: ${this.deviceType} (${this.brand}), Serial: ${this.serialNumber}`;
},
// Function to list features
listFeatures() {
return Object.entries(this)
.filter(([key]) => key.startsWith(featureBase))
.map(([_, value]) => value)
.join(", ");
},
// Static utilities
static: {
isValidSerial(serial) {
return typeof serial === "number" && serial > 0;
},
compareDevices(deviceA, deviceB) {
return deviceA.releaseYear > deviceB.releaseYear
? `${deviceA.deviceType} is newer than ${deviceB.deviceType}`
: `${deviceB.deviceType} is newer than ${deviceA.deviceType}`;
}
}
};
// Demonstrate the object
console.log(smartDevice);
// Example Output:
// {
// deviceType: 'Smartphone',
// brand: 'TechCo',
// serialNumber: 12345,
// releaseYear: 2024,
// feature_1: 'Bluetooth Connectivity',
// feature_2: 'Wireless Charging',
// getDetails: [Function: getDetails],
// listFeatures: [Function: listFeatures],
// static: { isValidSerial: [Function: isValidSerial], compareDevices: [Function: compareDevices] }
// }
console.log(smartDevice.getDetails());
// Device: Smartphone (TechCo), Serial: 12345
console.log(smartDevice.listFeatures());
// Bluetooth Connectivity, Wireless Charging
// Use static functions
console.log(smartDevice.static.isValidSerial(12345)); // true
console.log(smartDevice.static.isValidSerial("abc123")); // false
const deviceA = { deviceType: "Tablet", releaseYear: 2022 };
const deviceB = { deviceType: "Smartwatch", releaseYear: 2023 };
console.log(smartDevice.static.compareDevices(deviceA, deviceB));
// Smartwatch is newer than Tablet
satya - 12/24/2024, 3:56:23 PM
Note: The above is ChatGPT gnerated....
- Hopefully all pieces are correct
- Sometimes it does gets it wrong
- So use caution
satya - 12/24/2024, 4:00:20 PM
The object literal
// An object literal: no function, or class used
// var smartDevice is just an object
const smartDevice = {
// properties from global
deviceType,
brand,
serialNumber,
// as a key value pair
releaseYear: 2024,
satya - 12/24/2024, 4:42:47 PM
Easy ones
- It is an object literal
- It uses {} to do so
- Uses global variables as "self named" attributes of the key value pairs of the object
- An explicitly defined key value pair: releaseYear, name and value
- None of these are dynamic, so to say...although you can say the global variables kind of does that as well...
- if an attribute name, or key name is dynamic, then it will "forted" with []
satya - 12/24/2024, 4:43:58 PM
The dynamic object attribs or keys
// Dynamic property with a static key
releaseYear: 2024,
// Dynamic property with a computed key
[`${featureBase}_1`]: "Bluetooth Connectivity",
[`${featureBase}_2`]: "Wireless Charging",
satya - 12/24/2024, 4:46:27 PM
Commentary
- They key names are in between []
- Each is an expression
- The output of the evaluation becomes the key at run time of this block of code
- In the example "featureBase" is a global variable. It is expanded and concatenated to become
- feature_1
- feature_2
- Because the value of featureBase is the literal "feature"
satya - 12/24/2024, 4:49:08 PM
The darn commas...
- Each element of the object ends with a comma except for the last one
- attributes, functions, and static functions are separated that way
satya - 12/24/2024, 4:52:40 PM
On Functions in an object literal
- They are allowed
- "function" qualified is not used
- separated by commas
- static functions can be scoped inside static or independently indicated
- Pay attention to the "this" to qualify the instance attribute of the object inside that method
satya - 12/24/2024, 4:53:12 PM
Few functions
// Function to list features
listFeatures() {
return Object.entries(this)
.filter(([key]) => key.startsWith(featureBase))
.map(([_, value]) => value)
.join(", ");
},
// Static utilities
static: {
isValidSerial(serial) {
return typeof serial === "number" && serial > 0;
},
compareDevices(deviceA, deviceB) {
return deviceA.releaseYear > deviceB.releaseYear
? `${deviceA.deviceType} is newer than ${deviceB.deviceType}`
: `${deviceB.deviceType} is newer than ${deviceA.deviceType}`;
}
}
satya - 12/24/2024, 4:54:53 PM
Understanding [] in function parameters...
listFeatures() {
return Object.entries(this)
.filter(([key]) => key.startsWith(featureBase))
.map(([_, value]) => value)
.join(", ");
},
satya - 12/31/2024, 12:17:58 PM
Object.entries
- It is a static method on base object
- It takes an object reference as an input like 'this'
- It then returns an array of key value pairs
- Each key value pair is again an array with 2 elements
- So it is like an array of arrays
- Both attributes and functions are returned
satya - 12/31/2024, 12:19:27 PM
Example
const myObject = {
//attributes
name: "Sathya",
age: 30,
//functions
greet() {console.log("Hello!");},
listEntries() {
return Object.entries(this);
}
};
console.log(myObject.listEntries());
// Output:
// [
// ["name", "Sathya"],
// ["age", 30],
// ["greet", [Function: greet]]
// ]
satya - 12/31/2024, 12:30:42 PM
Other methods on an object
//Like a hash map or table
.keys // just the atttribute names
.values // values
.assign(targetObj, ...sourceObjects)
//assign the source object attributes to the target object
//and return the target
.freeze
//no additions, removals, or updates
.seal
//updates are allowed
.is(obj1, obj2)
// compare their values like ===
satya - 12/31/2024, 12:31:16 PM
Quick look
- keys
- values
- assign
- freeze
- seal
- is
satya - 12/31/2024, 1:25:31 PM
On prototypes
- When you create a function, it is also a constructor to create an object
- The function or object has properties of its own and also a default property pointing to a SINGLE parent object called a prototype
- The prototype has its own properties and methods
- All instances will share the properties and methods
- By default the prototype indirectly points to the Object.prototype object
- The prototype object can be changed to whatever prototype object you like
- During read, attributes are looked up the chain
- Not so during writes, the same attribute when tried to be updated using object instance reference, will create a new shadow property
- If you were to explicitly set the prototypes property by navigating to its name, it will be updated for ALL instances
- The newer class syntax uses the prototypes behind the scenes
- So all methods on that class become protoype methods shared by all instances
- The instance attributes of the class become the "own" attributes that are specific to each instance
satya - 12/31/2024, 1:37:31 PM
Constructor functions are different
- They are invoked with new
- They don't return
- If they return, that object becomes the output of new
- If the return is a primitive, then they are ignored and the output of new is returned.
satya - 12/31/2024, 1:38:30 PM
Lets get back to the [] in function parameters
listFeatures() {
return Object.entries(this)
.filter(([key]) => key.startsWith(featureBase))
.map(([_, value]) => value)
.join(", ");
},
satya - 12/31/2024, 2:08:11 PM
Few notes on []
- The [] is a way to extract something from what is originally passed in
- it is a bit like "select" statement on that incoming entity
- The incoming entity can be a row (array) or an object
- [] allows you to pluck selectively values from arrays or objects
satya - 12/31/2024, 2:23:57 PM
The filter method and calling vs called
- In the code above, the filter method is a function on any array type
- It uses a call back
- The callback ought to return a boolean to indicate to choose the current element of the array
- The cb is passed, among other things, the element of the array as its first argument
- The returned filtered array by the filter is a NEW array, but contains the same elemental references
- JS allows the caller to pass more arguments than the call back has "specified"
- The additional args are ignored
- The signature is "defined" by the caller, the "called" can choose to implement more or less arguments
- The filter method DOES call the callback with more parameters than just the element, but the callback here is only interested in the first element.
satya - 12/31/2024, 2:33:08 PM
[key]....explanation
- The object.entries is an array of key value pairs (which itself is a 2 element array)
- The filter passes the first row of that array
- So input to the first argument is [key, value]
- By select [just-one], the first column of that row or array is assigned to the variable just-one, and that variable then used in the body of the arrow function to eval and return a boolean
satya - 1/1/2025, 4:41:13 PM
More on prototypes....
- when a cf is defined (not even called yet), a "prototype" object is created and associated with that cf
- This allows for adding common methods before creating any instance
- The prototype instance can be accessed with the Cf name and a dot
- Two independents cfs will have distinct instances of their respective prototype instances
satya - 1/1/2025, 4:45:49 PM
If I am able to define all methods on a cf, why define them on a prototype?
- If I am able to define all methods on a cf, and they are available in all objects, why do I need to do add methods to a prototype?
- Apparently JS "actually" copies the methods of the cf into each new object taking up memory. Literally. Not reference to the common method like in OO, but copied!
- Doing these methods instead on a prototype saves memory
- Only way to change behavior of all instances at "run time" is to add them to the prototype, for you cannot change the cf, as the object is already created.
satya - 1/1/2025, 4:46:40 PM
A pattern: A refactored cf, Consider the following cf
function Person(name, age) {
this.name = name;
this.age = age;
// If methods are here, this can get messy!
this.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
this.getAge = function () {
return this.age;
};
}
satya - 1/1/2025, 4:47:17 PM
Comments
- The methods are inside the cf.
- There is a way to get them outside....see below
satya - 1/1/2025, 4:47:41 PM
Using a prototype instead
function Person(name, age) {
this.name = name; // Instance-specific
this.age = age; // Instance-specific
}
// Shared methods
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
Person.prototype.getAge = function () {
return this.age;
};
satya - 1/1/2025, 4:48:51 PM
Note how the methods are extracted out
- May be....
- is it a better pattern? may be.
- At least good to know such option is open.
satya - 1/1/2025, 4:53:46 PM
The one-to-one aspect of a prototype to its cf
- Every cf typically or should have its own prototype instance
- Usually the do
- when a cf is defined, a prototype is defined for it
satya - 1/1/2025, 4:58:22 PM
When do you program the prototype?
- Like in OO programming, you typically don't inherit from a prototype
- Although you can, the primary purpose a prototype is used often is "along with its cf" in lockstep
- Couple of reasons
- The functions in the prototype actually "uses" data that is defined in the cf!! Go figure.
- So those functions needs to be designed with the capabilities of the cf!
- For implementing the more advanced OO patterns, there are multiple ways to engineer prototypes and hand stitch them to do this
- Note: However in the variants of the JS modern languages like TypeScript, classes does this for you for most common cases, engineering prototypes behind the scenes.
satya - 1/1/2025, 5:00:36 PM
What does this cf time approach may look like?
- That there are very many instances of objects for a given cf
- You want to be efficient and move all "methods" to the prototype
- while leaving the data in the cf
- Again, good to know, but defining classes and using TypeScript will make this knowledge a bit academic
satya - 1/1/2025, 5:00:52 PM
In other words a class in JS: cf + prototype :)
In other words a class in JS: cf + prototype :)
satya - 1/1/2025, 5:11:57 PM
Do object literals create cf functions or move them to the prototype?
- Object literals also define functions in their body just like cfs.
- Question is, as that may be a syntactic sugar, do these methods stay on the object, or cf, or moves to the prototype?
- Answer:
- In case of object literals, as no one is using a new, So, no cf is created!
- The created object uses the Object.prototype as its prototype
- So the question doesn't arise as neighther a cf nor a prototype is constructed
- Moreover every object created out of a literal "share" the SAME object.prototype instance!
satya - 1/1/2025, 5:23:58 PM
The strangeness of "this" of a prototype
- Methods of a Prototype often use data from the object instances
- So the question is, how do they 1) anticipate this data to act on and b) get access to this data
- The answer:
- The strangeness (and rawness) of sharing stuff with prototypes....
- It is really a low level mechanism
- First, the cf and the prototype are typically written at the same time hand in hand
- It is almost like writing a java class and do it twice once with data, and once with methods and then join them, so to say...
- Secondly, when a method is called on an instance, it bubbles up to the prototype definition for that method and the "this" parameter in that method is the pointed to the "instance of the object"
- In that sense it is not a "true" inheritance as one will expect, it is just writing the class in a 2 step process
- Of course all this is corrected in the newer versions and TypeScript
- So sneakily, the prototype methods are EXPECTED to work with the elements of the "this" variable, which is the instance.
satya - 1/1/2025, 5:31:20 PM
Pointers on variable scopes, and this
- In a function if variable is indicated, it is looked up in the local scope, then in the global scope walking up the chain
- If it is prefixed with a "this" it is looked up in the instance variables, and then in the prototypes instance variables
- In addition as said, when a function is invoked and it turns out to be a shared function on the prototype, applicable to all instances, the "this" variable will distinguish the instance for that function to act on.
- Further sometimes when a callback function is called, the passed in this variable may belong to a different context than the original instance to which the callback function belongs.
satya - 1/1/2025, 5:39:27 PM
Arrow functions and this
- This is implied in arrow functions
- Intending to make their calling inline and obvious
- Arrow functions lexically bind this to the scope where they are defined, instead of dynamically determining it based on how they are called.
- Means the code can rely on the immediate surroundings where they are defined
satya - 1/1/2025, 5:47:42 PM
Lets get complex a bit with "call" and inheritance, the old way
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name); // Initialize instance-specific properties
this.grade = grade;
}
// Inherit methods from Person
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// Add Student-specific methods
Student.prototype.study = function () {
console.log(`${this.name} is studying for grade ${this.grade}`);
};
const alice = new Student("Alice", "A");
alice.greet(); // Output: Hello, my name is Alice
alice.study(); // Output: Alice is studying for grade A
satya - 1/1/2025, 5:49:31 PM
Notes
- The student's constructor calls the parent Person constructor by passing its own this via a "call"
- This will ensure all parents attributes including its functions are inherited!
- However....
- At that point the Person and Student have their own distinct prototypes!
- That will be problem because those prototypes are not chained (or inherited)
- So the code above sets it up correctly
satya - 1/1/2025, 5:50:09 PM
ES6 brings this to the modern world
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name); // Calls the parent constructor
this.grade = grade;
}
study() {
console.log(`${this.name} is studying for grade ${this.grade}`);
}
}
const alice = new Student("Alice", "A");
alice.greet(); // Output: Hello, my name is Alice
alice.study(); // Output: Alice is studying for grade A
satya - 1/1/2025, 5:50:34 PM
The code is like MOST other oo languages....
The code is like MOST other oo languages....
satya - 1/1/2025, 6:01:20 PM
Nature of Object.create
- This function is specifically designed to setup the prototype hierarchy
- The input is the parent prototype
- The output is a new prototype object or instance that is chained to the parent
- Often used in setting up inheritance hierarchies
- The individual cfs don't share the same prototype directly, instead they have their own prototype object that has a pointer back to the same parent prototype instance
- This allows the prototype of each cf to have its distinct signature
satya - 1/1/2025, 7:59:03 PM
What happens if 2 cfs share the same instance of a prototype
- Obviously doing so, will allow one cf, if it choses, to alter the behavior of another cf instances
- If you don't do that, you can use them...
- However....
- Any debugging tools looking at the prototype cannot tell which of the two "cfs" a given prototype belongs to, for the idea is that they are one to one
- Other than that you can use them so with some care...
satya - 1/1/2025, 8:21:59 PM
Static functions in literals
- They are like any other methods in an object literal
- No distinction
- However you can provide a namespace with a "some-div:" and use that sub name space to indicate the use of those functions and use the to call
- See such a "static:" block in the object literal sample at the beginning of this doc
- These object literals with only static methods can act as name spaces. This is kind of encapsulation with out classes
satya - 1/1/2025, 8:33:31 PM
Really high level concepts covered in this article/exploration
- Object literals with properties, dynamic properties, methods, static or utility methods classification
- Basic example of an object literal
- Basic example of a class in ES6
- Function argument selectors using []
- Base functions on an object: keys, values, assign, freeze, seal, is, entries
- Function syntax for filter, map, join: The array manipulations
- What are Prototypes
- How are attributes and methods shared between cfs and prototypes
- How do you use prototypes
- Prototypes themselves as object instances
- Object.create to work with prototypes
- Object.call to implment inheritance
- How do prototype methods know what data to expect in the instances?
- One to one relationship between cfs and prototypes
- How ES6 translates classes to prototypes
- How cfs actually copy methods for each instance wasting memory and how prototypes if needed can solve them
- The "raw" approach to design using cfs and prototypes, the heart of JS
- The pathways of looking up scope for instance variables and local and global variables
satya - 1/1/2025, 8:35:30 PM
If I have to classify even more, the topics
- Object literals
- Arrow functions and Argument unpacking
- Prototypes
- Objects and classes