Level 21: YDKJS, Part 1
You don’t know JS
Haven’t had time to post because of rolling off of my old job and starting a new one! Excitement! Here are some notes from my reading of YDKJS. I strongly recommend it to anyone interested in learning front-end development and want to get to really know the internals of the language.
Up and Going:
- JavaScript is an amalgamation of different languages, which makes it so accessible to new users
- Often, can ship production code without really fully understanding JS behind the scenes, so rarely do people actually master it
- Expressions: literal value, variable expression, arithmetic expression, assignment, and call expression (function calls)
- JS makes use of dynamic typing, which allows for variables to hold any type
- Scope: Each function gets its own scope, variable name has to be unique within its scope
-
Code in one scope can utilize scope variables of its own and the scope outside of its own
- Object types (7): number, null, undefined, string, boolean, object, (symbol is new)
- Only values have types, not their variables
- Bracket notation useful if property/key in object is named the same as another variable in scope, obj[b] vs. obj[“b”]
- Object -> subtype is function or array
- FALSY: “”, 0, -0, NaN, null, undefined, false
- == with coercion, === without
- If either side of comparison can be true/false, or if either value is 0, “”, [], use ===. Otherwise, can use ==
- Non-primitive equality checks are only referential and not by value
- Function scopes:
- Var keyword to declare variable in current function scope, or global scope if outside any function
- Hoisting: declarations are “moved” to the top of the enclosing scope at compilation time. Generally okay for functions, not for variables
- When variable value is not available, you get a ReferenceError
- Variables declared in outer scope can be accessed by any lower/inner scopes
- Immediately Invoked Function Expressions (IIFE) can be used to return values
- Closures used for module pattern, defining public API and hiding internals
- ‘This’ does not refer to the function itself
- To handle backwards compatibility of new features in JS, we use transpilers (Babel) or polyfills or shims
Scope & Closures:
- Compilers do: tokenizing -> parsing (into Abstract Syntax Tree) -> Code Generation
- For RHS lookups, will get reference errors. For LHS lookups not in strict mode, will create a global scope variable
Lexical Scope
- defined as the scope defined at sexing time - where variables and blocks of scope are authored when the lever processes the code, WHERE FUNCTION IS DECLARED
- Scope lookups stop once they find the first match
- Cheating lexical scope leads to poorer performance, but can be done through
eval()/setTimeOut()/setTimeInterval
as if you programmatically wrote code orwith
, which creates its own separate lexical scope. “Var” inside with will be in containing scope, can lead to weird side effects to the global scope - With and eval will stop the javascript engine from optimizing because it can have no idea about the contents of the code so it cannot optimize.
- In strict mode, eval has its own lexical scope and will not affect the scope around it Function vs. Block Scope
- Function-based scope
- Keeps values enclosed and
hidden
from the outside world - Helps collision avoidance
- Use IIFE for functional expressions
- Anonymous function expressions can be hard to debug as there’s no label, and leaves documentation lacking
- Catch creates its own block scope but otherwise, js doesn’t have a built-in concept of block scoping
- Adding explicit blocks
{}
can allow for garbage collecting, note thatlet
doesn’t get hoisted.const
also creates a block-scoped variable - Var is function scope, let and const are block scopes
Hoisting
- Declaration is done in the compilation phase, assignment during execution phase
- Function declarations are hoisted, but not expressions
- Functions are hoisted first before normal variables
- Any functions within blocks normally get hoisted to enclosing scope
Scope Closure
- closure is when a function can remember and access its lexical scope even when function is executing outside of its lexical scope
- callback functions are often examples of closures, or whenever you treat functions as a first-class citizen
- in for loops with closures, may need to use IIFEs and ensure that there is a new scope per iteration of the loop. Or you could use let and it will be declared for each iteration
- Can use module-like pattern and expose an object from a function that has references to other functions, sort of like a public api of sorts
- outer function must be invoked at least once, and must return back at least one inner function to access closure and private scopes
- ES6 syntax for import/export of modules must be in their own files now
- Dynamic scope is runtime, and cares where it’s called from, lexical scope is where the function was declared
- Arrow functions take the immediate lexical enclosing scope, which is different from the usual. No need to do “var self = this;” to bind functions to its own lexical scope
- This.count++, and then have a “bind(this) on the end of the function, to bind to enclosing lexical scope
This and Object Prototypes:
this
is not related to lexical scopethis
matters where the function is called, and not where declared. Run time mechanism- Binding made when function is invoked and what it references is based on the call-site of where the fun is called
this all makes sense now!
- Call-site, where the function is called
- Focus on the call-stack to find the moment right before the invocation of the function
- Call-site will follow rules:
- Default binding - plain undecorated function reference; with “use strict” in contents of function not its call-site, will throw TypeError undefined
- Implicit binding - when an object has function as attribute, and you call from there - obj.foo(), would have obj as call-site
- Can get implicitly lost because functions are passed by reference in parameters, and will not be tied to any object reference even if it has
- Explicit binding - using call and apply, forcing the method to use the object passed in as the “this” binding
- Hard binding - create a function that calls the function you want -> function() { foo.call(obj);}, called bind in ES6
- API call contexts does something similar as well, passing an extra parameter to be the obj you wish to bind
new
binding - no actual constructor functions, but construction calls of functions. New object is created and set as thethis
binding, will return object automatically
- New > explicit > implicit > default
- Can explicitly bind null to do currying or to go to default binding rules -> leads to manipulating global state in cases
- Use Object.create(null), which skips Object.prototype delegation for safety instead of null
- Indirection -> when you do assignment, the return value is reference to underlying function, which would then be global
- (p.foo = o.foo)(); (not o.foo or p.foo, but rather foo());
- Soft-binding will be overridden with default unless there is an alternate binding that is above default. Allows for flexibility compared to the hard binding
- Arrow functions bind to the lexical scope, so it overrides rules of
this
(old way was var self = this). In a codebase, choose one or the other but not both
Objects
- Primary types in JS: string, number, boolean, null, undefined, object
- Sub-objects = String, Number, Boolean, Object, Function, Array, Date, RegExp, Error
- JS will coerce primitives to their related object when required for length, etc.
- “.attribute” is property access, [“attribute”] is key access. Property Access requires an Identifier compatible property name
- Can add properties to an array, as it is an object
- Object.assign() takes target object and multiple sources to create an object via reference, but only immediately available attributes
- Can define properties and metadata, as well
Object.defineProperty( myObject, "a", { value: 2, writable: false, // not writable! configurable: true, enumerable: true } );
- Object preventExtensions will stop getting new properties added to it
- Object.seal() will lock any new attributes and even config changes
- Object.freeze will seal and leads to immutability for the object
- Object.get() does hv a recursive branch from theory
- Supports GET, PUT, DELETE
- Built-in delete operator syntax
Mixing “Class” Objects
- Child inheritance implies copies
- Explicit mixins - specifying exactly which method to call, and the object to bind it to -> Vehicle.drive.call(this); for shadowed variables
- Will still get references that don’t duplicate, like for arrays, functions
- Parasitic inheritance is possible as well, but mainly hijacking the parent and using closures to keep the reference of the original shadowed method
- Implicit mixins - declaring another object and grafting on a function call that will make a call to another object’s function.
var Another = { cool: function() { // implicit mixin of `Something` to `Another` Something.cool.call( this ); } };
I’ll post the follow-up to these notes soon! Thanks, as always, for following.