JS in details (part 1, Fundamentals)
This article opens the set of articles about JS. It covers the most interesting topics about JS, contains an explanation to the most common questions and can be used as a preparation to the interviews. Most paragraphs are cited from the book "You don't know JS".
What is Javascript?
JS is an implementation of the ECMAScript standard (version ES2019 as of this writing), which is guided by the TC39 committee and hosted by ECMA. It runs in browsers and other JS environments such as Node.js. ES - is a language specification. JS's syntax and behavior are defined in the ES specification.
JS is a multi-paradigm language, meaning the syntax and capabilities allow a developer to mix and match (and bend and reshape!) concepts from various major paradigms, such as procedural, object-oriented (OO/classes), and functional (FP).
JS is a compiled language, meaning the tools (including the JS engine) process and verify a program (reporting any errors!) before it executes.
Not JS functions
In fact, a wide range of JS-looking APIs, like alert(..), fetch(..),
getCurrentLocation(..), and getUserMedia(..), are all web APIs that look like JS.
In Node.js, we can access hundreds of API methods from various built-in modules,
like fs.write(..).
Another common example is console.log(..) and all the other console.* methods!
These are not specified in JS, but because of their universal utility are defined
by pretty much every JS environment, according to a roughly agreed consensus.
So alert(..) and console.log(..) are not defined by JS. But they look like JS.
They are functions and object methods and they obey JS syntax rules. The behaviors
behind them are controlled by the environment running the JS engine, but on the surface
they definitely have to abide by JS to be able to play in the JS playground.
Most of the cross-browser differences people complain about with "JS is so inconsistent!" claims are actually due to differences in how those environment behaviors work, not in how the JS itself works.
Don't trust what behavior you see in a developer console as representing exact to-the-letter JS semantics; for that, read the specification. Instead, think of the console as a "JS-friendly" environment. That's useful in its own right.
The developer console is not trying to pretend to be a JS compiler that handles your entered code exactly the same way the JS engine handles a .js file. It's trying to make it easy for you to quickly enter a few lines of code and see the results immediately. These are entirely different use cases, and as such, it's unreasonable to expect one tool to handle both equally.
- Whether a var or function declaration in the top-level "global scope" of the console actually creates a real global variable (and mirrored window property, and vice versa!).
- What happens with multiple let and const declarations in the top-level "global scope."
- Whether "use strict"; on one line-entry (pressing <enter> after) enables strict mode for the rest of that console session, the way it would on the first line of a .js file, as well as whether you can use "use strict"; beyond the "first line" and still get strict mode turned on for that session.
- How non-strict mode this default-binding works for function calls, and whether the "global object" used will contain expected global variables.
Backwards compatibility & transpiling
One of the most foundational principles that guides JavaScript is preservation of backwards compatibility. Backwards compatibility means that once something is accepted as valid JS, there will not be a future change to the language that causes that code to become invalid JS. Code written in 1995—however primitive or limited it may have been!—should still work today. As TC39 members often proclaim, "we don't break the web!"
HTML and CSS, by contrast, are forwards-compatible but not backwards-compatible. If you dug up some HTML or CSS written back in 1995, it's entirely possible it would not work (or work the same) today.
Since JS is not forwards-compatible, it means that there is always the potential for a gap between code that you can write that's valid JS, and the oldest engine that your site or application needs to support. If you run a program that uses an ES2019 feature in an engine from 2016, you're very likely to see the program break and crash.
For new and incompatible syntax, the solution is transpiling. Transpiling is a contrived and community-invented term to describe using a tool to convert the source code of a program from one form to another (but still as textual source code). Typically, forwards-compatibility problems related to syntax are solved by using a transpiler (the most common one being Babel to convert from that newer JS syntax version to an equivalent older syntax.
If the forwards-compatibility issue is not related to new syntax, but rather to a missing API method that was only recently added, the most common solution is to provide a definition for that missing API method that stands in and acts as if the older environment had already had it natively defined. This pattern is called a polyfill (aka "shim").
Parsing and execution
JS is a parsed language. The parsed JS is converted to an optimized (binary) form, and that "code" is subsequently executed; the engine does not commonly switch back into line-by-line execution mode after it has finished all the hard work of parsing—most languages/engines wouldn't, because that would be highly inefficient.
Consider the entire flow of a JS source program:
- After a program leaves a developer's editor, it gets transpiled by Babel, then packed by Webpack (and perhaps half a dozen other build processes), then it gets delivered in that very different form to a JS engine.
- The JS engine parses the code to an AST (Abstract syntax tree).
- Then the engine converts that AST to a kind-of byte code, a binary intermediate representation (IR), which is then refined/converted even further by the optimizing JIT compiler.
- Finally, the JS VM executes the program
Strict mode
Back in 2009 with the release of ES5, JS added strict mode as an opt-in mechanism for encouraging better JS programs.
Why strict mode? Strict mode shouldn't be thought of as a restriction on what you can't do, but rather as a guide to the best way to do things so that the JS engine has the best chance of optimizing and efficiently running the code. Most JS code is worked on by teams of developers, so the strict-ness of strict mode (along with tooling like linters!) often helps collaboration on code by avoiding some of the more problematic mistakes that slip by in non-strict mode.
Most strict mode controls are in the form of early errors, meaning errors that aren't strictly syntax errors but are still thrown at compile time (before the code is run). For example, strict mode disallows naming two function parameters the same, and results in an early error. Some other strict mode controls are only observable at runtime, such as how this defaults to undefined instead of the global object.
Strict mode is like a linter reminding you how JS should be written to have the highest quality and best chance at performance.
LET & VAR
Both let and var declares a variable.
The let keyword has some differences to var, with the most obvious being that let allows a more limited access to the variable than var. This is called "block scoping" as opposed to regular or function scoping.
Block-scoping is very useful for limiting how widespread variable declarations are in our programs, which helps prevent accidental overlap of their names.
var adult = true;
if (adult) {
var name = "Kyle";
let age = 39;
console.log("Shhh, this is a secret!");
}
console.log(name); // Kyle
console.log(age); // Error!
CONST
Const declared variables are not "unchangeable", they just cannot be re-assigned.
It's bad advise to use const with object values, because those values can still be changed even though the variable can't be re-assigned. This leads to potential confusion down the line, so I think it's wise to avoid situations like:
const actors = ["Morgan Freeman", "Jennifer Aniston"];
actors[2] = "Tom Cruise"; // OK :(
actors = []; // Error!
If you stick to using const only with primitive values, you avoid any confusion of re-assignment (not allowed) vs. mutation (allowed)! That's the safest and best way to use const.
Types
A variable in JavaScript can contain any data. A variable can at one moment be a string and at another be a number. Programming languages that allow such things are called “dynamically typed”, meaning that there are data types, but variables are not bound to any of them.
There are 8 basic data types in JavaScript.
- number for numbers of any kind: integer or floating-point, integers are limited by ±253.
- bigint is for integer numbers of arbitrary length.
- string for strings. A string may have one or more characters, there’s no separate single-character type.
- boolean for true/false.
- null for unknown values – a standalone type that has a single value null.
- undefined for unassigned values – a standalone type that has a single value undefined.
- object for more complex data structures.
- symbol for unique identifiers.
typeof undefined // "undefined"
typeof 0 // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof "foo" // "string"
typeof Symbol("id") // "symbol"
typeof Math // "object"
typeof null // "object"
typeof alert // "function"
Postfix & Prefix Increment
let a = 1, b = 1;
let c = ++a; // c=2, a=2
let d = b++; // b=2, d=1
Function decalration and function expression
In JS functions are values. They can be assigned, copied or declared in any place of the code. If the function is declared as a separate statement in the main code flow, that’s called a function declaration. If the function is created as a part of an expression, it’s called a function expression. Function Declarations are processed before the code block is executed. They are visible everywhere in the block. Function Expressions are created when the execution flow reaches them.
Function Declaration: a function, declared as a separate statement, in the main code flow.
function awesomeFunction(coolThings) {
// ..
return amazingStuff;
}
vs. Function Expression: a function, created inside an expression or inside another syntax construct. Here, the function is created at the right side of the “assignment expression” =:
var awesomeFunction = function(coolThings) {
// ..
return amazingStuff;
};
The more subtle difference is when a function is created by the JavaScript engine.
A Function Expression is created when the execution reaches it and is usable only from that moment.
A Function Declaration can be called earlier than it is defined.
For example, a global Function Declaration is visible in the whole script, no matter where it is. That’s due to internal algorithms. When JavaScript prepares to run the script, it first looks for global Function Declarations in it and creates the functions. We can think of it as an “initialization stage”. And after all Function Declarations are processed, the code is executed. So it has access to these functions.
Comparison
== is a coercive comparisons. Coercion means a value of one type being converted to its respective representation in another type (to whatever extent possible).
42 == "42"; // true
1 == true; // true
These relational operators typically use numeric comparisons, except in the case where both values being compared are already strings; in this case, they use alphabetical (dictionary-like) comparison of the strings:
var x = "10";
var y = "9";
x < y; // true, watch out!
==='s equality comparison is often described is, "checking both the value and the type". But the === operator does have some nuance to it, a fact many JS developers gloss over, to their detriment. The === operator is designed to lie in two cases of special values: NaN and -0. Consider:
NaN === NaN; // false
0 === -0; // true
it's best to avoid using === for them. For NaN comparisons, use the
Number.isNaN(..) utility, which does not lie.
For -0 comparison, use the Object.is(..) utility, which also does not lie.
Object.is(..) can also be used for non-lying NaN checks.
Object.is(value1, value2); method determines whether
two values are the same value.
Object comparison
In JS, all object values are held by reference, are assigned and passed by reference-copy, and to our current discussion, are compared by reference (identity) equality.
var x = [ 1, 2, 3 ];
// assignment is by reference-copy, so
// y references the *same* array as x,
// not another copy of it.
var y = x;
y === x; // true
y === [ 1, 2, 3 ]; // false
x === [ 1, 2, 3 ]; // false
In this snippet, y === x is true because both variables hold a reference to the same initial array. But the === [1,2,3] comparisons both fail because y and x, respectively, are being compared to new different arrays [1,2,3]. The array structure and contents don't matter in this comparison, only the reference identity.