Starting JavaScript: ECMAScript, Browser vs Node.js, Core Syntax and var vs let vs const
Key takeaways
JavaScript tutorial for beginners: ECMAScript and runtimes, core syntax, and var vs let vs const—with examples you can run in the browser or Node.js.
Introduction
JavaScript became the one programming language every web browser runs natively (WebAssembly now runs alongside it, but still depends on JavaScript to reach the page). Today, with Node.js, you run the same language on servers, CLIs, and tools. This article gives you history and the ECMAScript standard, runtimes, core syntax, and var / let / const in one place so you can continue to variables and types.
History of JavaScript and ECMAScript
A short history
- 1995: Netscape introduced a language to add behavior to web pages: Mocha → LiveScript → JavaScript. The “Java” in the name was marketing; the language is not Java.
- 1997 onward: To align divergent browser implementations, ECMA published ECMAScript, a formal standard. Versions labeled ES (ES5, ES2015, etc.) refer to that standard.
- 2015 (ES2015 / ES6): A major update that brought
let/const, arrow functions, classes, and modules (import/export)—the backbone of modern JavaScript. - The spec is updated every year (ES2016, ES2017, …). Modern JavaScript usually means roughly ES2015 and later.
ECMAScript vs JavaScript
| Term | Meaning |
|---|---|
| ECMAScript | The standard document that defines syntax, types, and built-in objects |
| JavaScript | A language that implements ECMAScript (e.g. V8 in Chrome/Node, SpiderMonkey in Firefox) |
For developers, “which ECMAScript version is supported?” is the compatibility baseline—for example, “to use ES2020 BigInt, check your build target and browser versions.”
There are two ways around an old runtime, and they solve different problems. A transpiler (Babel, TypeScript, esbuild) rewrites new syntax such as ?. or class fields into older syntax. A polyfill adds missing built-ins such as Array.prototype.at by defining them in JavaScript. Some features can only be done one way: BigInt cannot be polyfilled with the same semantics at all, because 10n is new syntax and the arithmetic operators cannot be overloaded. Today all evergreen browsers and current Node.js versions support ES2020 and well beyond, so the question mostly matters for old embedded browsers, WebViews and legacy enterprise environments.
Runtimes: browser vs Node.js
The syntax is the same, but built-in APIs differ by environment.
Browser
- Role: Renders HTML/CSS, manipulates the DOM, and reacts to events (clicks, input).
- Typical APIs:
document,window,fetch,localStorage—web-specific APIs. - Execution: Load via
<script>ortype="module"in HTML.
<script>
console.log(document.title);
</script>
Where the <script> tag sits matters more than beginners expect. A classic script in <head> runs as soon as the parser reaches it, before the rest of the page exists, so document.getElementById("button") returns null and the next line fails with Uncaught TypeError: Cannot read properties of null (reading 'addEventListener'). The fixes are to put the script at the end of <body>, add the defer attribute, or use type="module", which is deferred by default. Module scripts also run in strict mode and have their own scope, so a top-level let in a module does not become a global the way it would in a classic script.
Node.js
- Role: Servers, build scripts, CLI tools. Suited for OS-adjacent work: files, processes, HTTP servers.
- Typical APIs:
fs,path,http,process. There is nowindow/documentlike in the browser. - Execution:
node file.jsfrom the terminal, or npm scripts inpackage.json.
// Node.js (e.g. server.js)
const http = require("http");
// Or use import in projects with "type": "module"
Node.js supports two module systems, and mixing them produces two of the most searched Node errors. Using import in a file Node treats as CommonJS gives SyntaxError: Cannot use import statement outside a module; using require in an ES module gives ReferenceError: require is not defined in ES module scope. Which system applies is decided per file: .mjs is always ESM, .cjs is always CommonJS, and a plain .js follows the "type" field of the nearest package.json (CommonJS when it is missing). For a new project, setting "type": "module" and using import everywhere matches what browsers and bundlers use.
At a glance
| Browser | Node.js | |
|---|---|---|
| Main focus | UI and page interaction | Servers, tools, automation |
| DOM | Yes | Generally no |
| Modules | import/export + bundlers is common | require (CJS) or import (ESM, per package.json) |
Suggested learning path: Syntax is the same everywhere—console logs and conditionals work in DevTools or Node. If you care about web UI, pair JavaScript with HTML in the browser soon; for backend, add Node in parallel.
Core syntax: variables, types, operators
Variables (overview)
A variable is a name for a value. Keywords are var / let / const; differences are summarized under var vs let vs const.
let count = 0;
const siteName = "pkglog";
Types (overview)
JavaScript is dynamically typed. You can reassign a variable from a number to a string (in real code, avoid that when possible).
Common primitive types:
string,number,bigint,boolean,undefined,symbolnullexplicitly means “no value.” Object types: Arrays, functions, plain objects{ }are handled by reference.
typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof { a: 1 }; // "object"
typeof (() => {}); // "function"
typeof null; // "object" (a historical bug kept for compatibility)
typeof [1, 2]; // "object" (use Array.isArray to detect arrays)
The last two lines are the classic typeof traps. typeof null returning "object" dates back to the first implementation and can never be fixed without breaking existing sites, so a check like typeof value === "object" must also test value !== null before reading a property. Arrays are objects as well, which is why Array.isArray exists.
There is also only one number type: every number is a 64-bit IEEE 754 floating-point value, integers included. That gives the famous 0.1 + 0.2 === 0.30000000000000004, and it means integers are exact only up to Number.MAX_SAFE_INTEGER (2^53 − 1). Database IDs or timestamps in nanoseconds beyond that silently lose precision when parsed from JSON, which is what bigint (123n) and string IDs are for.
Operators (commonly used)
- Arithmetic:
+,-,*,/,%,**(exponentiation) - Comparison:
===,!==(prefer strict equality),<,>,<=,>= - Logical:
&&,||,! - Nullish coalescing / optional chaining (ES2020+):
??,?.
// == coerces types and is hard to predict. Prefer ===.
0 == false; // true (avoid)
0 === false; // false
const name = null;
const display = name ?? "guest"; // "guest"
?? and || look interchangeable but differ on “falsy” values. count || 10 replaces 0, "" and false with the default, which is a bug when 0 is a legitimate count; count ?? 10 only replaces null and undefined. The one place == is commonly still used on purpose is value == null, which is true for both null and undefined and nothing else; everywhere else, === avoids the coercion rules ("" == 0 and "0" == 0 are both true, while "" == "0" is false).
For more on variables, types, and coercion, see JavaScript variables and data types.
var vs let vs const
Quick comparison
| Keyword | Scope | Redeclaration | Reassignment | Notes |
|---|---|---|---|---|
var | Function | Allowed (same scope) | Allowed | Unusual hoisting behavior |
let | Block | Not in same block | Allowed | Safe in loops and if |
const | Block | Not allowed | No rebinding | Object contents can still change |
Prefer let and const
// ✅ Prefer: default to const; use let only when reassignment is needed
const apiBase = "https://api.example.com";
let retryCount = 0;
retryCount += 1;
// ❌ Avoid var in new code
var oldStyle = 1;
// const prevents rebinding, not mutation
const config = { retries: 3 };
config.retries = 5; // OK: the object itself can change
// config = {}; // TypeError: Assignment to constant variable.
const frozen = Object.freeze({ retries: 3 }); // shallow: nested objects stay mutable
const is about the variable, not the value. It guarantees that config always points at the same object, which is what makes it a good default: a reader knows the name will not be reassigned later in the function. If you need the object’s contents to be read-only too, Object.freeze does that, but only one level deep.
Typical var pitfalls (for understanding)
var is function-scoped, not block-scoped, and hoisting can surprise you.
if (true) {
var x = 1;
}
console.log(x); // 1 — visible outside the block (with let: ReferenceError)
console.log(hoist); // undefined (declaration hoisted, assignment not)
var hoist = 2;
The bug that made let necessary is the loop closure. With var there is one i for the whole function, so every callback sees its final value:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0, 1, 2
}
let in a for header creates a fresh binding for each iteration, so each callback captures its own j. Before ES2015 the workaround was wrapping the body in an immediately invoked function, which is why older code is full of (function (i) { ... })(i). When I read legacy code with odd IIFEs inside loops, this is almost always the reason.
One more browser-only surprise: a top-level var in a classic script becomes a property of window. var name = 42; typeof name gives "string" in a browser, because it is really window.name, which converts every value to a string. Top-level let and const do not create window properties, so they avoid this.
Practical rule: In new code use const first, then let if needed; keep var for reading legacy code.
First code example
A minimal example you can run in the browser console or Node.
function greet(name) {
return `Hello, ${name}!`;
}
const user = "developer";
console.log(greet(user));
In HTML, place this before </script> or load app.js with script src="app.js".
Frequently Asked Questions (FAQ)
Q. Are let and const hoisted like var?
A. Their declarations are hoisted to the top of the block too, but unlike var they are not initialized to undefined. From the start of the block until the declaration line runs, the variable is in the temporal dead zone, and reading it throws a ReferenceError instead of quietly returning undefined as the var hoist example does. That louder failure is one of the practical reasons to default to const and let: a use-before-declaration bug surfaces immediately instead of producing an undefined value somewhere downstream.
Related Articles
- JavaScript Values and Types: Primitives vs References, typeof Quirks, == vs ===
- JavaScript Functions: Hoisting, Arrow Functions and this, Closures
- JavaScript DOM Manipulation
- HTML and CSS Basics: How the Browser Builds the DOM
- Starting with TypeScript: What the Compiler Checks