Object literal enhancements, classes, samples

  1. When a variable is initialized with an object definition inline
  2. no functions are used
  3. No classes are used
  4. Just initializing a variable with an object in line
  5. How do you define properties
  6. Methods
  7. Dynamic properties

// 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
  1. Hopefully all pieces are correct
  2. Sometimes it does gets it wrong
  3. So use caution

// 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,
  1. It is an object literal
  2. It uses {} to do so
  3. Uses global variables as "self named" attributes of the key value pairs of the object
  4. An explicitly defined key value pair: releaseYear, name and value
  5. None of these are dynamic, so to say...although you can say the global variables kind of does that as well...
  6. if an attribute name, or key name is dynamic, then it will "forted" with []

// Dynamic property with a static key
    releaseYear: 2024,

    // Dynamic property with a computed key
    [`${featureBase}_1`]: "Bluetooth Connectivity",
    [`${featureBase}_2`]: "Wireless Charging",
  1. They key names are in between []
  2. Each is an expression
  3. The output of the evaluation becomes the key at run time of this block of code
  4. In the example "featureBase" is a global variable. It is expanded and concatenated to become
  5. feature_1
  6. feature_2
  7. Because the value of featureBase is the literal "feature"
  1. Each element of the object ends with a comma except for the last one
  2. attributes, functions, and static functions are separated that way
  1. They are allowed
  2. "function" qualified is not used
  3. separated by commas
  4. static functions can be scoped inside static or independently indicated
  5. Pay attention to the "this" to qualify the instance attribute of the object inside that method

// 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}`;
        }
    }

listFeatures() {
        return Object.entries(this)
            .filter(([key]) => key.startsWith(featureBase))
            .map(([_, value]) => value)
            .join(", ");
    },
  1. It is a static method on base object
  2. It takes an object reference as an input like 'this'
  3. It then returns an array of key value pairs
  4. Each key value pair is again an array with 2 elements
  5. So it is like an array of arrays
  6. Both attributes and functions are returned

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]]
// ]

//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 ===
  1. keys
  2. values
  3. assign
  4. freeze
  5. seal
  6. is
  1. When you create a function, it is also a constructor to create an object
  2. The function or object has properties of its own and also a default property pointing to a SINGLE parent object called a prototype
  3. The prototype has its own properties and methods
  4. All instances will share the properties and methods
  5. By default the prototype indirectly points to the Object.prototype object
  6. The prototype object can be changed to whatever prototype object you like
  7. During read, attributes are looked up the chain
  8. Not so during writes, the same attribute when tried to be updated using object instance reference, will create a new shadow property
  9. If you were to explicitly set the prototypes property by navigating to its name, it will be updated for ALL instances
  10. The newer class syntax uses the prototypes behind the scenes
  11. So all methods on that class become protoype methods shared by all instances
  12. The instance attributes of the class become the "own" attributes that are specific to each instance
  1. They are invoked with new
  2. They don't return
  3. If they return, that object becomes the output of new
  4. If the return is a primitive, then they are ignored and the output of new is returned.

listFeatures() {
        return Object.entries(this)
            .filter(([key]) => key.startsWith(featureBase))
            .map(([_, value]) => value)
            .join(", ");
    },
  1. The [] is a way to extract something from what is originally passed in
  2. it is a bit like "select" statement on that incoming entity
  3. The incoming entity can be a row (array) or an object
  4. [] allows you to pluck selectively values from arrays or objects
  1. In the code above, the filter method is a function on any array type
  2. It uses a call back
  3. The callback ought to return a boolean to indicate to choose the current element of the array
  4. The cb is passed, among other things, the element of the array as its first argument
  5. The returned filtered array by the filter is a NEW array, but contains the same elemental references
  6. JS allows the caller to pass more arguments than the call back has "specified"
  7. The additional args are ignored
  8. The signature is "defined" by the caller, the "called" can choose to implement more or less arguments
  9. 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.
  1. The object.entries is an array of key value pairs (which itself is a 2 element array)
  2. The filter passes the first row of that array
  3. So input to the first argument is [key, value]
  4. 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
  1. when a cf is defined (not even called yet), a "prototype" object is created and associated with that cf
  2. This allows for adding common methods before creating any instance
  3. The prototype instance can be accessed with the Cf name and a dot
  4. Two independents cfs will have distinct instances of their respective prototype instances
  1. 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?
  2. 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!
  3. Doing these methods instead on a prototype saves memory
  4. 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.

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;
    };
}
  1. The methods are inside the cf.
  2. There is a way to get them outside....see below

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;
};
  1. May be....
  2. is it a better pattern? may be.
  3. At least good to know such option is open.
  1. Every cf typically or should have its own prototype instance
  2. Usually the do
  3. when a cf is defined, a prototype is defined for it
  1. Like in OO programming, you typically don't inherit from a prototype
  2. Although you can, the primary purpose a prototype is used often is "along with its cf" in lockstep
  3. Couple of reasons
  4. The functions in the prototype actually "uses" data that is defined in the cf!! Go figure.
  5. So those functions needs to be designed with the capabilities of the cf!
  6. For implementing the more advanced OO patterns, there are multiple ways to engineer prototypes and hand stitch them to do this
  7. 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.
  1. That there are very many instances of objects for a given cf
  2. You want to be efficient and move all "methods" to the prototype
  3. while leaving the data in the cf
  4. Again, good to know, but defining classes and using TypeScript will make this knowledge a bit academic

In other words a class in JS: cf + prototype :)

  1. Object literals also define functions in their body just like cfs.
  2. Question is, as that may be a syntactic sugar, do these methods stay on the object, or cf, or moves to the prototype?
  3. Answer:
  4. In case of object literals, as no one is using a new, So, no cf is created!
  5. The created object uses the Object.prototype as its prototype
  6. So the question doesn't arise as neighther a cf nor a prototype is constructed
  7. Moreover every object created out of a literal "share" the SAME object.prototype instance!
  1. Methods of a Prototype often use data from the object instances
  2. So the question is, how do they 1) anticipate this data to act on and b) get access to this data
  3. The answer:
  4. The strangeness (and rawness) of sharing stuff with prototypes....
  5. It is really a low level mechanism
  6. First, the cf and the prototype are typically written at the same time hand in hand
  7. 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...
  8. 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"
  9. In that sense it is not a "true" inheritance as one will expect, it is just writing the class in a 2 step process
  10. Of course all this is corrected in the newer versions and TypeScript
  11. So sneakily, the prototype methods are EXPECTED to work with the elements of the "this" variable, which is the instance.
  1. In a function if variable is indicated, it is looked up in the local scope, then in the global scope walking up the chain
  2. If it is prefixed with a "this" it is looked up in the instance variables, and then in the prototypes instance variables
  3. 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.
  4. 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.
  1. This is implied in arrow functions
  2. Intending to make their calling inline and obvious
  3. Arrow functions lexically bind this to the scope where they are defined, instead of dynamically determining it based on how they are called.
  4. Means the code can rely on the immediate surroundings where they are defined

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
  1. The student's constructor calls the parent Person constructor by passing its own this via a "call"
  2. This will ensure all parents attributes including its functions are inherited!
  3. However....
  4. At that point the Person and Student have their own distinct prototypes!
  5. That will be problem because those prototypes are not chained (or inherited)
  6. So the code above sets it up correctly

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

The code is like MOST other oo languages....

  1. This function is specifically designed to setup the prototype hierarchy
  2. The input is the parent prototype
  3. The output is a new prototype object or instance that is chained to the parent
  4. Often used in setting up inheritance hierarchies
  5. 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
  6. This allows the prototype of each cf to have its distinct signature
  1. Obviously doing so, will allow one cf, if it choses, to alter the behavior of another cf instances
  2. If you don't do that, you can use them...
  3. However....
  4. 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
  5. Other than that you can use them so with some care...
  1. They are like any other methods in an object literal
  2. No distinction
  3. 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
  4. See such a "static:" block in the object literal sample at the beginning of this doc
  5. These object literals with only static methods can act as name spaces. This is kind of encapsulation with out classes
  1. Object literals with properties, dynamic properties, methods, static or utility methods classification
  2. Basic example of an object literal
  3. Basic example of a class in ES6
  4. Function argument selectors using []
  5. Base functions on an object: keys, values, assign, freeze, seal, is, entries
  6. Function syntax for filter, map, join: The array manipulations
  7. What are Prototypes
  8. How are attributes and methods shared between cfs and prototypes
  9. How do you use prototypes
  10. Prototypes themselves as object instances
  11. Object.create to work with prototypes
  12. Object.call to implment inheritance
  13. How do prototype methods know what data to expect in the instances?
  14. One to one relationship between cfs and prototypes
  15. How ES6 translates classes to prototypes
  16. How cfs actually copy methods for each instance wasting memory and how prototypes if needed can solve them
  17. The "raw" approach to design using cfs and prototypes, the heart of JS
  18. The pathways of looking up scope for instance variables and local and global variables
  1. Object literals
  2. Arrow functions and Argument unpacking
  3. Prototypes
  4. Objects and classes