javascript

    Metaprogramming

    02 Dec, 2023public

    Class in JS

    05 Dec, 2023public

    Promises

    13 Nov, 2023public

    Js notes

    11 Jan, 2024public

    Iteration in javascript

    15 Nov, 2023public
    economics

    Incentive

    21 Aug, 2023public
    demo

    Demo

    13 Nov, 2023public
    personal

    Teaching myself CS

    05 Dec, 2023public

    People

    12 Dec, 2023public

    Links

    25 Mar, 2024public
    computer graphics

    Computer graphics from scratch

    12 Dec, 2023public
    notag

    Flexbox practice

    14 Aug, 2024public
    Lying in wait, set to pounce on the blank page,are letters up to no good,clutches of clauses so subordinatethey'll never let her get away.
    - The Joy Of Writing, Wislawa Szymborska

    Js notes

    Notes written while going through deep javascript book
    .11 Jan, 2024

    Environment under the hood

    Notes while going through chapter environment under the hood.

    Consider the following simple piece of code:

    function f(x) {
      return x * 2;
    }
    
    function g(y) {
      const tmp = y + 1;
      return f(tmp);
    }
    assert.equal(g(3), 8);
    

    As soon as we start executing the code we enter into the top-level scope. What does entering into a scope mean? It means an execution context is pushed to a stack in the memory. What is an execution context ? It's just a pointer to the environment? What's an environment? You can imagine environment as just a key-value data structure where keys are variable names and values are "values of those variables". So basically when we say "entering into a scope" we mean "a pointer pointing to a variable key-value store in the memory is pushed to the top of the stack"

    Let's break down what happens when we start executing the code above:

    1. We enter the top-level scope i.e an execution context is pushed on top of stack.

    2. The context points to an environment in the memory which contains variable name-value pair. Right now, our environment contains variables f and g, both of them being functions.

    Loading svg

    When we reach the last line (assert.equal(g(3), 8)), we call g.

    This is what g function looks like:

    function g(y) {
      const tmp = y + 1;
      return f(tmp);
    }
    

    Calling a function starts a new scope which means one more execution context is pushed onto the stack. This execution context would point to another environment with it's own variables. This second environment contains variables y and tmp.

    Loading svg

    The main thing to note here is that we can still access f inside g ( when we return f(tmp) ) i.e somehow "environment 1" has access to the variable f.

    How's it possible?

    To access f, there needs to be a pointer in "environment 1" which points to "environment 0". This pointer is called the outer. The outer field of an environment points to the environment corresponding to surrounding scope.

    Loading svg

    When we access f inside of g, the search for the value of f first begins in the "environment 1", and then proceeds to "environment 0". Basically search for a variable begins in the environment pointed to by the execution context at top of stack and then proceeds to environments pointed to by outer fields.

    Just one question remains now: How does environment 1 know to point to environment 0 as soon as the execution of g begins?

    To achieve this, each function has an internal property called [[Scope]] which points to it's birth environment i.e the environment where the function was first defined.

    Loading svg

    So every time a function is called, following things happen:

    1. Execution context is pushed on to the stack
    2. This execution context points to the corresponding environment in the memory where the environment contains variable name-value pairs.
    3. The outer field of this environment points at the environment pointed to by [[Scope]] internal poperty of this function.

    With that in mind, let's try and build a similar diagram for another piece of code to really solidify the concepts

    function f(x) {
      function square() {
        const result = x * x;
        return result;
      }
      return square();
    }
    assert.equal(f(6), 36);
    

    Loading svg

    Copying

    The usual method of copying objects by spreading in javascript has several edge cases

    • The copied object doesn't have the same prototype
    class Myclass{}
    const original = new Myclass()
    
    console.log(original instanceof Myclass)
    
    let copy = {...original}
    
    console.log(copy instanceof Myclass)
    
    

    unless we specify it explicitly

    copy = {...original, __proto__: Myclass.prototype}
    console.log(copy instanceof Myclass)
    
    
    • The copied object doesn't contain inherited properties of the original i.e only own properties are copied
    const proto = {'a': 2}
    
    const original = {'b':2, __proto__: proto}
    
    const copy = {...original}
    
    console.log(original.a)
    console.log(copy.a)
    
    
    • property attributes aren't copied
    const original = Object.defineProperty({}, "a", {
      value: 1,
      writable: false,
      enumerable: true,
      configurable: false
    })
    
    const copy = {...original}
    
    
    console.log(Object.getOwnPropertyDescriptors(copy))
    

    Improved shallow copying

    function intactShallowCopy(original) {
      return Object.defineProperties({}, Object.getOwnPropertyDescriptors(original))
    }
    
    
    const original = Object.defineProperty({}, "a", {
      value: 1,
      writable: false,
      enumerable: true,
      configurable: false
    })
    original.b = {'c': 2}
    
    const copy = intactShallowCopy(original)
    
    console.log(Object.getOwnPropertyDescriptors(copy))
    
    

    Deep copying

    function deepCopy(original) {
      if (Array.isArray(original)) {
        const copy = [];
        for (const [index, value] of original.entries()) {
          copy[index] = deepCopy(value);
        }
        return copy;
      } else if (typeof original === 'object' && original !== null) {
        const copy = {};
        for (const [key, value] of Object.entries(original)) {
          copy[key] = deepCopy(value);
        }
        return copy;
      } else {
        // Primitive value: atomic, no need to copy
        return original;
      }
    }
    

    Objects under the hood

    In javascript, objects are composed of two things:

    • Internal slots - storage locations not accessible by javascript
    • properties - think of them as key-value pairs but they are more than that

    Read more on internal slots here

    Properties

    Properties are what's interesting. Consider the following object

    const obj = {a: 1}
    

    obj has a property whose key is a and value is 1 but under the hood, a property has some attributes too, which can be accessed using Object.getOwnPropertyDescriptor

    console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
    

    The attributes are:

    • value - The value of the property.

    • writable (boolean) - whether the value of the property can be changed

    • configurable (boolean) - whether the property can be configured i.e whether we can change the values of attributes themselves. If false, we can't change the any attribute other than value, can only change writable from true to false

    • enumerable (boolean) - Whether some properties participate in operations like Object.keys(), Object.entries() etc.

    Let's try fiddling with these attributes to see these different behaviors

    Object.defineProperty(obj, "a", {
      writable: false,
      configurable: true,
      enumerable: true
    })
    obj.a = 2 
    //obj.a can't be changed if writable is false
    console.log(obj.a) 
    
    //"a" won't be in the resulting array if enumerable is false
    console.log(Object.keys(obj))
    
    //The line below will throw an error if configurable is false
    Object.defineProperty(obj, 'a', {
      enumerable: true
    })
    
    console.log(Object.keys(obj))
    
    

    Now consider the following object:

    const obj = {
      x: 1,
      get a()  {
        return this.x
      },
    
      set a(value) {
         this.x = value 
      }
    }
    
    
    console.log(obj.a)
    obj.a = 2
    
    console.log(obj.a)
    console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
    
    

    In the obj above, a is an accessor property. The normal properties are called data properties. Accessor properties have two different attributes - get and set (both are functions) instead of value and writable.

    Assignment vs definition

    In previous section we say two ways of creating or changing properties on an object:

    const obj = {}
    //creation using assignment
    obj.a = 1
    
    //creation using definition
    Object.defineProperty(obj, 'b', {
      value: 2,
    })
    

    The important thing to keep in mind is that main purpose of assignment is making changes while the main purpose of definition is to create own properties.

    To elaborate what this means consider the following code snippet

    const proto = {
      data: 1,
      get x() {
        return this.data
      },
      set x(value) {
        this.data = value 
      }
    }
    
    const obj = {
      __proto__ : proto
    }
    
    
    //assignment
    obj.x = 2
    
    console.log(Object.getOwnPropertyDescriptor(obj["__proto__"], 'x'))
    //since we only have "assigned" x, we have just invoked the inherited setter for x.
    console.log(Object.getOwnPropertyDescriptor(obj, 'x'))
    
    //definition
    
    Object.defineProperty(obj, 'x', {
      value: 2
    })
    
    console.log(Object.getOwnPropertyDescriptor(obj["__proto__"], 'x'))
    //since we used "defined" x, we have created an own property on obj 
    console.log(Object.getOwnPropertyDescriptor(obj, 'x')) 
    console.log(obj.data)
    

    Now although assignment respects inherited setters it doesn't change prototype's properties

    obj.data = 3
    
    //this assignment creates an own property on obj instead of changing the data property of prototype
    console.log(Object.getOwnPropertyDescriptors(obj))
    
    Table of contents
    Environment under the hood
    Copying
    Improved shallow copying
    Deep copying
    Objects under the hood
    Properties
    Assignment vs definition
    function f(x) {
      return x * 2;
    }
    
    function g(y) {
      const tmp = y + 1;
      return f(tmp);
    }
    assert.equal(g(3), 8);
    1function g(y) {
    2  const tmp = y + 1;
    3  return f(tmp);
    4}
    1function f(x) {
    2  function square() {
    3    const result = x * x;
    4    return result;
    5  }
    6  return square();
    7}
    8assert.equal(f(6), 36);
    1function deepCopy(original) {
    2  if (Array.isArray(original)) {
    3    const copy = [];
    4    for (const [index, value] of original.entries()) {
    5      copy[index] = deepCopy(value);
    6    }
    7    return copy;
    8  } else if (typeof original === 'object' && original !== null) {
    9    const copy = {};
    10    for (const [key, value] of Object.entries(original)) {
    11      copy[key] = deepCopy(value);
    12    }
    13    return copy;
    14  } else {
    15    // Primitive value: atomic, no need to copy
    16    return original;
    17  }
    18}
    1const obj = {}
    2//creation using assignment
    3obj.a = 1
    4
    5//creation using definition
    6Object.defineProperty(obj, 'b', {
    7  value: 2,
    8})
    Abhimanyu
    stackstackheapheapffggenvironment 0environment 0function value (f)function value (f)function value (g)function value (g)
    ffggyytmptmp3344outerouterenvironment 0environment 0environment 1environment 1
    outer field of environment 1 points to environment 0
    ffgg[[scope]][[scope]]environment 0environment 0points to environment 0points to environment 0[[Scope]][[Scope]]points to environment 0points to environment 0
    f and g were defined during execution of top level scope, which means their [[scope]] property will point to environment corresponding to top scope which is environment 0.
    ffggfunction  valuefunction  valuefunction  valuefunction  valueyytmptmp4433how can this  environment access f?how can this  environment access f?environment 0environment 0environment 1environment 1
    ff[[scope]][[scope]]top-level scopetop-level scopescope inside fscope inside fenvironment 0environment 0squaresquare[[scope]][[scope]]environment 1environment 1xx66outerouterscope inside squarescope inside squareresultresult3636outerouterenvironment 2environment 2