cf.completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Annex B: web legacy features

Learn the old features in Annex B of the JavaScript spec, such as HTML-like comments, 010, substr and __proto__, and what to use instead.

By the end, you can
  • 01
    Spot Annex B codeRecognize old features such as 010, <!--, substr, __proto__ and getYear when you read old code.
  • 02
    Know where each one worksSay which old features are a SyntaxError in strict code or in modules, and which still run everywhere.
  • 03
    Replace each oneRewrite each old feature with its modern replacement and check that it gives the same result.

Annex B: old features the web still needs

The JavaScript spec is the rulebook that every JavaScript engine follows. Near its end are a few extra parts called annexes. Annex B is called "Additional ECMAScript Features for Web Browsers". It lists old features that web browsers must keep, so that old websites still work.

Many of these features came from early browsers, before there was a shared rulebook. Websites started to use them, so no browser could drop them. The spec writes them down so every browser does them the same way. It also says plainly that new code should not use them.

The old spelling and the new spelling give the same resultPop out in the code editor (opens in a new tab)JavaScript
console.log(010);
console.log(0o10);
console.log("tea".substr(1, 2));
console.log("tea".slice(1, 3));

Line 1 prints 010. A number that starts with 0 and another digit is an old way to write a number in base 8, called octal. Octal 10 means one 8 and no 1s, so line 1 prints 8. Line 2 writes the same number the modern way, with 0o in front, and also prints 8.

Line 3 calls substr, an old string method. substr(1, 2) starts at position 1 and takes 2 characters, so it prints ea. Line 4 calls slice(1, 3), the modern method. It starts at position 1 and stops before position 3, so it prints ea too.

Two rows. In the first, the old 010 and the new 0o10 both point at the value 8. In the second, the old tea substr 1, 2 and the new tea slice 1, 3 both point at the value ea.old spellingsame resultnew spelling01080o10"tea".substr(1, 2)"ea""tea".slice(1, 3)
Lines 1 and 2 both print 8. Lines 3 and 4 both print ea. The old spelling on the left still works in old scripts. The new spelling on the right is what to write today.

The features in this lesson fall into three groups:

  • Old ways to write code, such as 010 and <!-- comments.
  • Old functions and methods, such as escape, substr and getYear.
  • Old rules for normal-looking code, such as a function declared inside an if block.

Some of them only work in sloppy mode, the mode for code without "use strict". The sloppy mode lesson and the strict mode lesson explain the two modes. Every example here is a classic script, a normal <script> file, with no "use strict" unless its line 1 says so.

Real-life analogyOld street names on new signs

Picture a city that renamed many of its streets. Thousands of old letters and maps still use the old names. So the city keeps a second list, and every street sign shows the old name too. Old letters still arrive. But anyone drawing a new map uses only the new names.

In real life: The city's official list of street names
In JavaScript: The main part of the JavaScript spec
In real life: A second list of old names the signs must still show
In JavaScript: Annex B
In real life: Old letters and maps that use the old names
In JavaScript: Old websites that use 010 or substr
In real life: New maps print only the new name
In JavaScript: New code writes 0o10 and slice
The main idea

Annex B is the part of the JavaScript spec that lists old features browsers must keep for old websites. Each one has a modern replacement. Some of them are a SyntaxError in strict code or in modules. The rest still run everywhere, but new code should use the replacement.

HTML-like comments in classic scripts

In a classic script, <!-- starts a comment, just like //. So does --> at the start of a line. These are called HTML-like comments, because they look like comments in HTML.

Two kinds of HTML-like comment in a classic scriptPop out in the code editor (opens in a new tab)JavaScript
var price = 3;
<!-- price = 4;
console.log(price);
--> this line is a comment too

Line 1 makes a variable price with the value 3. Line 2 starts with <!--, so the whole line is a comment and price = 4 never runs.

Line 3 prints 3. Line 4 starts with -->, so it is a comment too, even though the words after it are not code.

Why does JavaScript allow this? The very first browsers that had JavaScript met pages written for even older browsers, which didn't know the <script> tag and showed its code as text. Authors hid the code inside an HTML comment, <!-- on the first line and --> on the last. JavaScript engines had to skip those two lines, and they still do.

<!-- doesn't have to start the line. Anywhere on a line, it starts a comment that runs to the end of that line.

<!-- in the middle of a linePop out in the code editor (opens in a new tab)JavaScript
var price = 3;
var count = price <!-- 2;
console.log(count);

Line 1 makes price with the value 3. On line 2, everything from <!-- on is a comment, so the line is just var count = price. Line 3 prints 3, not a comparison with 2.

--> is stricter. It only starts a comment when nothing but spaces or other comments come before it on its line.

--> after code on the same line is a SyntaxErrorPop out in the code editor (opens in a new tab)JavaScript
var price = 3; --> not a comment here
console.log(price);

Line 1 has code before -->, so --> is not a comment here. The engine reads it as -- and >, which can't follow 3;. The whole script is a SyntaxError, so line 2 never prints.

Strict mode doesn't change any of this: a classic script with "use strict" still allows HTML-like comments. Only a module rejects them. A module is a file loaded with <script type="module"> or with import.

The same comment in a module fileJavaScript
// app.js, loaded with <script type="module">
var price = 3;
<!-- price = 4;
console.log(price);

Line 1 is a normal comment: this file is loaded as a module. Line 2 makes price. Line 3 starts with <!--, and modules don't have HTML-like comments, so the whole module is a SyntaxError. Line 4 never runs. That is why this box has no Run button.

Two windows show the same two lines, var price = 3 and a line starting with an HTML-like comment. In the classic script window, the second line is a comment and the console prints 3. In the module window, the same line is a SyntaxError.classicvar price = 3;<!-- price = 4;a commentprints 3modulevar price = 3;<!-- price = 4;not allowed hereSyntaxError
In a classic script, <!-- starts a comment, so price = 4 never runs and price stays 3. A module doesn't allow HTML-like comments, so the same lines are a SyntaxError and nothing runs.

Use // or /* … */ for every comment. They work in classic scripts and modules alike.

Legacy octal numbers and octal escapes

Octal means base 8, which uses only the digits 0 to 7. Old JavaScript read any whole number that starts with 0, like 010, as octal. Today this is called a legacy octal number. Legacy means "kept only for old code".

Numbers that start with 0Pop out in the code editor (opens in a new tab)JavaScript
console.log(010);
console.log(0o10);
console.log(08);
console.log(019);

Line 1 prints 8, because 010 is octal 10, which is 8. Line 2 writes the same number with 0o, the modern octal prefix, and prints 8 too.

Line 3 is the surprise. 8 is not an octal digit, so 08 can't be octal. JavaScript reads it as the normal number 8 instead. Line 4 does the same: 019 has a 9, so it is the normal number 19. You can't tell which base a number uses without checking every digit.

Strings had the same idea. A backslash and up to three octal digits, such as \101, is a legacy octal escape: it means the character with that number.

Three ways to write the letter APop out in the code editor (opens in a new tab)JavaScript
console.log("\101");
console.log("\x41");
console.log("\u0041");

Line 1 uses the octal escape \101. Octal 101 is 64 + 1, which is 65, the number for the letter A, so it prints A.

Line 2 uses \x41, a hexadecimal escape: base 16 number 41, which is also 65. Line 3 uses \u0041, a Unicode escape with four hexadecimal digits. Both print A.

Strict mode bans both old forms. In strict code, 010, 08 and "\101" are each a SyntaxError.

010 in a strict script is a SyntaxErrorPop out in the code editor (opens in a new tab)JavaScript
"use strict";
console.log(010);

Line 1 makes the whole script strict. Line 2 has a legacy octal number, so the engine throws a SyntaxError while reading the script, and line 2 never prints.

"\101" in a strict script is a SyntaxErrorPop out in the code editor (opens in a new tab)JavaScript
"use strict";
console.log("\101");

Line 1 makes the script strict. Line 2 has the octal escape \101 in a string, so the script is a SyntaxError too.

A script can't catch its own SyntaxError. To see the error as a value, build the code from text with new Function, which reads the text while your code runs.

Catching the SyntaxError from strict codePop out in the code editor (opens in a new tab)JavaScript
try {
  new Function('"use strict"; return 010;');
} catch (error) {
  console.log(error.name);
}
console.log(new Function('"use strict"; return 0o10;')());

Line 2 builds a function from strict text that returns 010. Reading that text throws a SyntaxError. Lines 3 and 4 catch it and print its name, SyntaxError.

Line 6 builds the same function with 0o10. Strict code allows the modern prefix, so the call returns 8 and line 6 prints 8.

Template literals, the strings in backticks, never allowed octal escapes. \101 inside backticks is a SyntaxError even in sloppy code. So write numbers with 0o, and characters with \x or \u. They work in every kind of code.

escape() and unescape(): the old text encoders

Some characters can't go straight into a web address, such as a space or &. Before they go in, they are encoded: each one is replaced by % and a number. escape is the old global function that does this, and unescape turns the result back. Today, encodeURIComponent and decodeURIComponent do the job.

The old encoder and the modern onePop out in the code editor (opens in a new tab)JavaScript
var word = "tea & café";
console.log(escape(word));
console.log(encodeURIComponent(word));
console.log(unescape(escape(word)));
console.log(decodeURIComponent(encodeURIComponent(word)));

Line 1 makes a string with a space, an & and the letter é. Line 2 prints tea%20%26%20caf%E9. The space became %20, the & became %26, and é became %E9, its character number written in hexadecimal.

Line 3 prints tea%20%26%20caf%C3%A9. Only é is different. encodeURIComponent first turns the letter into UTF-8, the standard way to store text as bytes, and é is two bytes in UTF-8: %C3%A9. Servers and URLs expect UTF-8.

Lines 4 and 5 turn each result back. Each pair gets the original text, so both print tea & café.

The two pairs don't mix. Text made by escape can't always be read by decodeURIComponent.

decodeURIComponent can't read what escape madePop out in the code editor (opens in a new tab)JavaScript
try {
  decodeURIComponent(escape("café"));
} catch (error) {
  console.log(error.name);
}

Line 2 encodes café with escape, which gives caf%E9, and passes that to decodeURIComponent. %E9 alone is not valid UTF-8, so it throws a URIError. Lines 3 and 4 catch it and print its name, URIError.

escape also has its own form for characters with big numbers. escape("☕") gives %u2615, a form that URLs don't use at all. So in new code, use encodeURIComponent for one part of an address, and decodeURIComponent to read it back.

Old string methods: substr, trimLeft and HTML builders

Annex B also keeps a few string methods. They are normal methods, so they work in strict code and modules too. Each one has a modern replacement.

The first is substr(start, length). It takes a start position and a length, the number of characters to take. Most other methods, such as slice, take a start and an end instead.

Three ways to cut cu out of teacupPop out in the code editor (opens in a new tab)JavaScript
var word = "teacup";
console.log(word.substr(-3, 2));
console.log(word.slice(-3, -1));
console.log(word.substring(3, 5));

Line 1 makes the string teacup. Its positions are 0 to 5, so c is at position 3.

Line 2 calls substr(-3, 2). A negative start counts from the end, so -3 is position 3. It takes 2 characters and prints cu.

Line 3 calls slice(-3, -1). Both numbers count from the end: start at position 3, stop before position 5. It prints cu. Line 4 calls substring(3, 5), which also takes a start and an end, and prints cu.

The table below compares the three. Read each row as: "this call reads its two numbers like this, and gives this result."

substr, slice and substring on the same string
The callWhat the two numbers meanWhat it gives
"teacup".substr(-3, 2)A start and a length. A negative start counts from the end."cu"
"teacup".slice(-3, -1)A start and an end. Negative numbers count from the end."cu"
"teacup".substring(3, 5)A start and an end. Negative numbers become 0."cu"

The next pair is trimLeft and trimRight. They remove spaces from one end of a string. Their standard names are trimStart and trimEnd, to match padStart and padEnd.

trimLeft is trimStart under another namePop out in the code editor (opens in a new tab)JavaScript
console.log(String.prototype.trimLeft === String.prototype.trimStart);
console.log(String.prototype.trimRight === String.prototype.trimEnd);
console.log(String.prototype.trimLeft.name);
console.log("[" + "  tea  ".trimLeft() + "]");

Line 1 compares the two methods with === and prints true: they are the very same function. Line 2 does the same for trimRight and trimEnd, and prints true.

Line 3 prints the function's name, trimStart. The old name is only a second property that points at it. Line 4 removes the spaces at the start only, so it prints [tea ].

The last group builds HTML. bold, link, anchor, italics, fontcolor and a few more each wrap the string in an HTML tag.

String methods that build HTML tagsPop out in the code editor (opens in a new tab)JavaScript
var item = "Tea";
console.log(item.bold());
console.log(item.link("/menu"));
console.log("<i>".bold());

Line 1 makes the string Tea. Line 2 prints <b>Tea</b>. Line 3 prints <a href="/menu">Tea</a>, a link to /menu.

Line 4 shows the danger. The string <i> is pasted in as it is, so the result is <b><i></b>, which now holds a real <i> tag. These methods never make the text safe. If the text comes from a user, they could slip their own HTML into your page. (Only a " in the address of link or anchor is changed, to &quot;.)

In a page, build elements with the DOM instead. Setting textContent always treats the text as text, never as HTML.

The same link built with the DOMJavaScript
var link = document.createElement("a");
link.href = "/menu";
link.textContent = "Tea";
console.log(link.outerHTML);

Line 1 makes a new <a> element. Line 2 sets its address. Line 3 sets its text. Line 4 prints its HTML, <a href="/menu">Tea</a>. This box has no Run button, because it needs a web page with a document.

__proto__ and the old getter helpers

Every object has a prototype: another object it falls back on when it lacks a property. The prototype chain lesson explains it. Before there was a standard way to reach the prototype, browsers added __proto__, with two underscores on each side. Annex B keeps it in two forms.

The first form is a __proto__ key inside an object literal, the { … } that makes an object.

A __proto__ key sets the prototypePop out in the code editor (opens in a new tab)JavaScript
var menu = { kind: "menu" };
var cart = { __proto__: menu, price: 3 };
console.log(cart.kind);
console.log(Object.getPrototypeOf(cart) === menu);
console.log(Object.hasOwn(cart, "__proto__"));
console.log(cart.__proto__ === menu);

Line 1 makes a menu object with a kind property. Line 2 makes cart with the key __proto__: menu and a price. That key doesn't make a property. It sets the prototype of cart to menu.

Line 3 reads cart.kind. cart has no kind of its own, so JavaScript checks its prototype and prints menu. Line 4 asks for the prototype the modern way, with Object.getPrototypeOf, and prints true.

Line 5 asks whether cart has its own property called __proto__. It prints false: the key only set the link. Line 6 reads cart.__proto__, the second form, and prints true.

A cart labeled cart holds price: 3. An arrow labeled __proto__ points from it to a second cart labeled menu, which holds kind: menu.price: 3cart__proto__kind: "menu"menuown property: pricefound through the link: kind
Line 2's __proto__: menu doesn't add a property to cart. It links cart to menu. So line 3 finds kind on menu, and line 5 prints false.

Only a plain __proto__: key works this way, and an object literal may have it only once. Two __proto__: keys in one literal are a SyntaxError. Exercise 5 shows a way to write the key that doesn't set the prototype.

The second form, obj.__proto__, is not stored on obj. It is a getter and a setter on Object.prototype. A getter is a function that runs when you read a property, and a setter runs when you write it.

obj.__proto__ is a getter on Object.prototypePop out in the code editor (opens in a new tab)JavaScript
var desc = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
console.log(typeof desc.get, typeof desc.set);
var bare = Object.create(null);
console.log(bare.__proto__);
console.log(Object.getPrototypeOf(bare));

Line 1 gets the description of the __proto__ property on Object.prototype. Line 2 prints function function: it has a getter and a setter.

Line 3 makes bare with Object.create(null), an object with no prototype at all. So it can't reach the getter on Object.prototype, and line 4 prints undefined. Line 5 uses Object.getPrototypeOf, which works on every object, and prints the true answer, null.

Browsers also added four helpers for getters and setters: __defineGetter__, __defineSetter__, __lookupGetter__ and __lookupSetter__.

Adding a getter the old wayPop out in the code editor (opens in a new tab)JavaScript
var cart = { price: 3 };
cart.__defineGetter__("total", function () {
  return this.price + 1;
});
console.log(cart.total);
console.log(typeof cart.__lookupGetter__("total"));

Line 1 makes cart with a price of 3. Lines 2 to 4 add a getter called total, which returns the price plus 1.

Line 5 reads cart.total, which runs the getter and prints 4. Line 6 looks the getter up and prints function.

The same getter with Object.definePropertyPop out in the code editor (opens in a new tab)JavaScript
var cart = { price: 3 };
Object.defineProperty(cart, "total", {
  get: function () {
    return this.price + 1;
  },
  enumerable: true,
  configurable: true,
});
console.log(cart.total);
console.log(typeof Object.getOwnPropertyDescriptor(cart, "total").get);

Line 1 makes the same cart. Lines 2 to 8 call Object.defineProperty, which adds a property with exact settings. Lines 3 to 5 give it the same getter.

Lines 6 and 7 make it show up in loops and allow it to be redefined or deleted later, which is what __defineGetter__ does too. Line 9 prints 4. Line 10 reads the getter from the property's description and prints function.

In a class or an object literal, the shortest way is get total() { … }. The getters and setters lesson covers it.

RegExp.$1, lastMatch and compile()

When a regular expression, or regex, finds a match, browsers also save the parts it found on the RegExp function itself. RegExp.$1 to RegExp.$9 hold the text of each group in brackets, and RegExp.lastMatch holds the whole match. These are called the legacy RegExp statics.

Unlike the rest of this lesson, these are not in the text of Annex B. A separate standards proposal describes them, and browsers support them.

The same groups read two waysPop out in the code editor (opens in a new tab)JavaScript
var match = /(\w+)@(\w+)/.exec("asha@shop");
console.log(RegExp.$1, RegExp.$2);
console.log(RegExp.lastMatch);
console.log(match[1], match[2]);
console.log(match[0]);

Line 1 runs a regex with two groups on asha@shop. The first group finds asha, the second finds shop, and exec returns the result as match.

Line 2 reads the groups from RegExp.$1 and RegExp.$2 and prints asha shop. Line 3 prints the whole match, asha@shop. Lines 4 and 5 read the same things from match, the modern way, and print the same text.

The problem: RegExp.$1 belongs to the whole page, not to one regex. Every successful match changes it.

Another regex overwrites RegExp.$1Pop out in the code editor (opens in a new tab)JavaScript
/(\w+)@(\w+)/.exec("asha@shop");
"tea".replace(/(e)/, "E");
console.log(RegExp.$1);

Line 1 matches asha@shop, so for a moment RegExp.$1 is asha. Line 2 runs a different regex inside replace, and its group finds e. Line 3 prints e, not asha. Any code that runs between your match and your read, even inside a library, can do this.

Read the groups from the match result instead. Named groups, written (?<name>…), make the code easier to read.

Named groups on the match resultPop out in the code editor (opens in a new tab)JavaScript
var match = /(?<name>\w+)@(?<shop>\w+)/.exec("asha@shop");
console.log(match.groups.name, match.groups.shop);

Line 1 names the first group name and the second shop. Line 2 reads them from match.groups and prints asha shop. No other regex can change match.

Annex B does list one old regex method: compile. It changes an existing regex in place, to a new pattern and new flags.

compile changes a regex in placePop out in the code editor (opens in a new tab)JavaScript
var pattern = /tea/;
pattern.compile("cake", "g");
console.log(pattern.source, pattern.flags);
var modern = new RegExp("cake", "g");
console.log(modern.source, modern.flags);

Line 1 makes a regex for tea. Line 2 calls compile, which turns the same regex into one that looks for cake with the g flag. Line 3 prints cake g.

Lines 4 and 5 make a new regex with new RegExp instead and print cake g too. A new regex is clearer, because any other code holding the old one isn't surprised by a change.

Functions declared in blocks and after labels

A block is code inside { }, such as the body of an if. For years the spec didn't say what a function declared inside a block should do, but browsers allowed it anyway, each in its own way. Today, a function declared in a block belongs to that block. For sloppy code, Annex B adds one more rule so that old pages still work.

A function declared in an if block, in sloppy modePop out in the code editor (opens in a new tab)JavaScript
console.log(typeof greet);
if (true) {
  function greet() {
    return "hello";
  }
}
console.log(typeof greet);
console.log(greet());

Line 1 checks greet before the block. It doesn't throw an error. It prints undefined, because sloppy mode already made a var-like name greet for the whole script, set to undefined.

Line 2's condition is true, so the block runs. Lines 3 to 5 declare greet inside the block. When line 3 is reached, Annex B copies the block's function into the outer greet.

Line 7 is after the block, so it finds the copied function and prints function. Line 8 calls it and prints hello.

Step through a function that leaves its block
Step 0 of 5Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The example runs for real in sloppy mode.

Running in
  1. script
Next: line 1
if (true) {  function greet() {    return "hello";  }}console.log(typeof greet);console.log(greet());
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step and watch the two greet names. The block's greet holds the function from the moment the block starts. The outer greet only gets it at line 3. This replays the example code; it isn't a view inside the JavaScript engine.

Strict code has no copy rule. There, the function stays inside its block.

The same code in strict modePop out in the code editor (opens in a new tab)JavaScript
"use strict";
if (true) {
  function greet() {
    return "hello";
  }
}
console.log(typeof greet);

Line 1 makes the script strict. Lines 2 to 6 are the same block as before. Line 7 is after the block, and strict mode keeps greet inside it, so line 7 prints undefined.

Two halves. On the left, sloppy mode: a box labeled if block holds the function greet, and an arrow copies it down into a box labeled script, so typeof greet gives function. On the right, strict mode: the if block holds greet, the script box has no greet, so typeof greet gives undefined.sloppy modegreetif blockcopied at line 3greetscript"function"strict modegreetif blockstays in the blockno greetscript"undefined"
In sloppy mode, reaching the declaration on line 3 copies greet out of the block, so the check after the block finds a function. In strict mode, greet stays inside the block, so the same check gives "undefined".

So the same lines mean two different things, depending on the mode. To get one meaning in both, declare the name outside the block and give it a function inside.

One meaning in both modesPop out in the code editor (opens in a new tab)JavaScript
"use strict";
let greet;
if (true) {
  greet = function () {
    return "hello";
  };
}
console.log(typeof greet);

Line 1 makes the script strict. Line 2 declares greet with let, outside the block. Lines 4 to 6 store a function in it inside the block. Line 8 prints function. Remove line 1 and it still prints function.

Annex B also allows a function after a label. A label is a name and a colon in front of a statement, like tea:. It is meant for loops, so that break tea can leave the right loop.

A label in front of a functionPop out in the code editor (opens in a new tab)JavaScript
tea: function brew() {
  return "brewed";
}
console.log(brew());

Lines 1 to 3 declare brew with the label tea: in front. In sloppy code the label does nothing, so line 4 calls brew and prints brewed.

A labelled function in strict code is a SyntaxErrorPop out in the code editor (opens in a new tab)JavaScript
"use strict";
tea: function brew() {
  return "brewed";
}

Line 1 makes the script strict. Line 2 puts a label in front of a function declaration, which strict code doesn't allow, so the script is a SyntaxError.

The last case is an if with a function and no braces.

A function as the whole body of an ifPop out in the code editor (opens in a new tab)JavaScript
if (true) function brew() { return "brewed"; }
console.log(brew());

Line 1 declares brew as the whole body of an if, with no braces. Sloppy code reads it as if it had braces around it, so the copy rule applies, and line 2 prints brewed. In strict code, line 1 is a SyntaxError. Always write the braces.

var in catch blocks and for-in starting values

catch (e) makes a name e that lives only in the catch block. The main rules say a var with the same name inside that block is a SyntaxError. Annex B allows it, because old code did it.

var e inside catch (e)Pop out in the code editor (opens in a new tab)JavaScript
try {
  throw "oops";
} catch (e) {
  var e = "fixed";
  console.log(e);
}
console.log(e);

Lines 1 and 2 throw the string oops. Line 3 catches it in a new name e that lives only in the catch block.

Line 4 has two parts. var e makes a second e for the whole script, which starts as undefined. The = "fixed" part writes to the nearest e, which is the catch's e. Line 5 prints fixed.

Line 7 is outside the catch block, so it reads the script's e. Nothing ever wrote to it, so it prints undefined.

Step through var e inside catch (e)
Step 0 of 6Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The example runs for real in sloppy mode.

Running in
  1. script
Next: line 1
  throw "oops";} catch (e) {  var e = "fixed";  console.log(e);}console.log(e);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step and watch the two e names side by side. Line 4 changes only the catch's e. This replays the example code; it isn't a view inside the JavaScript engine.

Unlike most rules in this lesson, this one isn't only for sloppy code. Strict code allows it too.

The same catch in strict modePop out in the code editor (opens in a new tab)JavaScript
"use strict";
try {
  throw "oops";
} catch (e) {
  var e = "fixed";
  console.log(e);
}
console.log(e);

Line 1 makes the script strict. Lines 2 to 8 are the same code as before, one line lower. Line 6 prints fixed, and line 8 prints undefined.

The rule only covers a plain name in catch (e). With destructuring, such as catch ({ e }), a var e inside is still a SyntaxError. Either way, one name for two variables is hard to read, so give the var a different name.

The second rule is about for-in loops, which visit each property name of an object. In sloppy code, a var in the loop head may have a starting value.

A starting value in a for-in headPop out in the code editor (opens in a new tab)JavaScript
var empty = {};
for (var key = "none" in empty) {
  console.log("never runs");
}
console.log(key);

Line 1 makes an empty object. Line 2 sets key to "none" once, before the loop starts. Then the loop visits each property of empty, but there are none, so line 3 never runs.

Line 5 prints none, the starting value. If the object had properties, key would hold the last name the loop visited instead.

A starting value in strict code is a SyntaxErrorPop out in the code editor (opens in a new tab)JavaScript
"use strict";
var empty = {};
for (var key = "none" in empty) {}

Line 1 makes the script strict. Line 3 has a starting value in the loop head, so the script is a SyntaxError. With let, const or destructuring in the head, a starting value is a SyntaxError even in sloppy code.

The starting value on its own linePop out in the code editor (opens in a new tab)JavaScript
var empty = {};
var key = "none";
for (key in empty) {}
console.log(key);

Line 1 makes the empty object. Line 2 gives key its starting value on its own line. Line 3 is a normal for-in loop that reuses key. Line 4 prints none, and this version works in strict code too.

Old Date methods: getYear, setYear and toGMTString

The first JavaScript Date was modeled on the Date class of the Java language, which counted years from 1900. getYear and setYear still do.

getYear counts from 1900Pop out in the code editor (opens in a new tab)JavaScript
var day = new Date(2024, 0, 15);
console.log(day.getYear());
console.log(day.getFullYear());
day.setYear(99);
console.log(day.getFullYear());

Line 1 makes a date for 15 January 2024. Months count from 0, so 0 is January. Line 2 prints 124: the year minus 1900. Line 3 prints the real year, 2024.

Line 4 calls setYear(99). A number from 0 to 99 is read as a year after 1900, so the date moves to 1999, and line 5 prints 1999. A bigger number, such as setYear(2030), is used as it is.

Code like "19" + day.getYear() shows the year 2000 as 19100. Use getFullYear and setFullYear, which always use the real year.

toGMTString is the last one. GMT is the old name for the time zone now called UTC.

toGMTString is toUTCString under another namePop out in the code editor (opens in a new tab)JavaScript
var day = new Date(Date.UTC(2024, 0, 15));
console.log(Date.prototype.toGMTString === Date.prototype.toUTCString);
console.log(day.toGMTString());
console.log(day.toUTCString());

Line 1 makes a date at midnight UTC on 15 January 2024. Line 2 prints true: the two names point at the very same function.

Lines 3 and 4 both print Mon, 15 Jan 2024 00:00:00 GMT. Use the standard name, toUTCString.

Replacing Annex B features in old code

You will meet these features in old scripts and old libraries. They become a problem when you add "use strict", turn a script into a module, or just want the next reader to understand the code. Here is a small function written the old way.

An old function with three Annex B featuresPop out in the code editor (opens in a new tab)JavaScript
function orderLine(item, price, day) {
  var code = item.substr(0, 3).toUpperCase();
  var year = day.getYear() + 1900;
  return code + "-" + year + " " + escape(item) + " " + price;
}
console.log(orderLine("tea cake", 3, new Date(2024, 0, 15)));

Lines 1 to 5 make orderLine. Line 2 takes the first 3 characters of the item with substr and makes them capital letters. Line 3 gets the year with getYear and adds 1900 back.

Line 4 joins the code, the year, the item encoded with escape, and the price. Line 6 prints TEA-2024 tea%20cake 3.

The same function with modern replacements, in strict modePop out in the code editor (opens in a new tab)JavaScript
"use strict";
function orderLine(item, price, day) {
  const code = item.slice(0, 3).toUpperCase();
  const year = day.getFullYear();
  return code + "-" + year + " " + encodeURIComponent(item) + " " + price;
}
console.log(orderLine("tea cake", 3, new Date(2024, 0, 15)));

Line 1 makes the script strict, to show this version is allowed there. Line 3 uses slice(0, 3), which takes the same 3 characters. Line 4 uses getFullYear, so there is no 1900 to add.

Line 5 uses encodeURIComponent. Line 7 prints TEA-2024 tea%20cake 3, the same as before. With an item such as café, the two versions would differ, and the new one gives the UTF-8 form that servers expect.

Before you rewrite, it helps to know which features break when code becomes strict. Read each row of the table as: "this old code does this in sloppy code, and this in strict code."

Where each kind of Annex B feature stops working
The old codeWhat happens in sloppy codeWhat happens in strict code
010, "\101", labelled functions, for (var x = 1 in obj)They run.They are SyntaxErrors, so nothing in the script runs.
<!-- and --> commentsThey are comments.They are comments in strict scripts, but SyntaxErrors in modules.
A function declared in an if blockIts name is also visible after the block.Its name stays inside the block.
substr, escape, __proto__, getYear, RegExp.$1, catch (e) { var e }They run.They run the same way.
Where does each old feature stop working?
  • 010
  • "\101"
  • tea: function brew() {}
  • for (var key = "none" in cart) {}
  • <!-- an old comment
  • "teacup".substr(1, 2)
  • escape("tea")
  • { __proto__: menu }
  • catch (e) { var e = 1; }
  • day.getYear()
Try it yourself
0 of 10 correct

Put each piece of old code in its group: a SyntaxError in strict code, a SyntaxError only in modules, or works in strict code and modules too.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The playground runs an old feature and its modern replacement side by side. Pick a feature, then add "use strict" on top and see which old programs stop working. Guess first, then look at the result.

Playground: run an old feature next to its replacement
The old programPop out in the code editor (opens in a new tab)JavaScript
console.log(010);
The modern programPop out in the code editor (opens in a new tab)JavaScript
console.log(0o10);
What each program printssame
old8
modern8
Try it yourself
Same result.

Both programs print the same thing. The modern one is what to write in new code.

Both runs are real. This page runs each program as the body of new Function, which is sloppy code unless it starts with "use strict".

Try 010 with "use strict": the old program is a SyntaxError, and the modern one still prints 8. Try substr(): both programs print the same in either mode. Try "function in a block": in strict mode the old program prints undefined, and the modern one still prints function.

The table below lists every replacement in this lesson. Read each row as: "old code like this can be written like that, and here is why the new version is better."

Modern replacements for Annex B features
Old codeWhat to write insteadWhy it is better
<!-- and --> comments// or /* … */They are SyntaxErrors in modules. Normal comments work everywhere.
010 and "\101"0o10 and "\x41" or "\u0041"The old forms are SyntaxErrors in strict code. The new forms say clearly which base they use.
escape() and unescape()encodeURIComponent() and decodeURIComponent()The new pair uses UTF-8, which servers expect, for letters such as é.
"teacup".substr(-3, 2)"teacup".slice(-3, -1)slice takes a start and an end, like most other methods.
trimLeft() and trimRight()trimStart() and trimEnd()They are the very same functions. The new names match padStart and padEnd.
"Tea".bold() and "Tea".link(url)document.createElement and textContentThe old methods paste text into HTML without escaping it.
obj.__proto__Object.getPrototypeOf(obj) and Object.setPrototypeOf(obj, proto)They work on every object, even one made with Object.create(null).
__defineGetter__ and __lookupGetter__Object.defineProperty and Object.getOwnPropertyDescriptorThe new methods are clearer and can set every part of a property.
RegExp.$1 and RegExp.lastMatchmatch[1], match[0] or named groupsThe old values are shared by the whole page, so any other regex can change them.
pattern.compile("cake", "g")new RegExp("cake", "g")Making a new regex is clearer than changing an old one in place.
function greet() {} inside an if blocklet greet; and greet = function () {} in the blockThe code then means the same thing in sloppy and strict mode.
tea: function brew() {}function brew() {} with no labelA label on a function does nothing useful, and strict code rejects it.
catch (e) { var e = 1; }A different name, such as var result = 1;One name for two variables is hard to read.
for (var key = "none" in cart)var key = "none"; then for (key in cart)The starting value is set on its own line, which strict code allows.
getYear() and setYear()getFullYear() and setFullYear()The full year is the real year, not the year minus 1900.
toGMTString()toUTCString()They are the very same function. toUTCString is the standard name.

A linter can find many of these for you. ESLint, a popular one, has rules such as no-octal, no-octal-escape and no-proto.

Common mix-ups

Most mix-ups about Annex B come from thinking strict mode removes all of it, or that the old and new names do different things.

  • "Annex B features are gone from JavaScript." No. Browsers must keep them for old websites. They are only for old code, but they still run.
  • "Strict mode turns off all of Annex B." No. It bans old syntax such as 010 and labelled functions. substr, escape, __proto__, getYear and RegExp.$1 still run.
  • "<!-- is a SyntaxError in strict code." No. A strict classic script still treats it as a comment. Only modules reject it.
  • "08 is an octal number." No. 8 is not an octal digit, so sloppy code reads 08 as the normal number 8.
  • "substr(1, 2) and slice(1, 2) give the same result." No. The 2 is a length for substr and an end for slice, so "tea" gives ea and e.
  • "trimLeft is a different method from trimStart." No. They are the very same function under two names.
  • "{ __proto__: menu } makes a property called __proto__." No. A plain __proto__: key sets the prototype instead.
  • "RegExp.$1 belongs to the regex you just ran." No. It belongs to the whole page, and any later match changes it.

The table below puts three of these side by side. Read each row as: "this feature does this, and here is what to watch for."

What is true about three Annex B features
The featureWhat is trueWhat to watch for
Annex B featuresStill work in every browser today.They are only for old code. New code uses the modern replacement.
catch (e) { var e = 1; }Is allowed in sloppy and strict code.The 1 goes into the catch's e, not the outer e.
<!--Starts a comment anywhere on a line in a classic script.It is a SyntaxError in a module.

Practice exercises

Every program below is a sloppy classic script unless its line 1 says "use strict". For each one, find the old feature first and ask what it does in sloppy code. Guess first, then run it.

Exercise 1 · Warm-upPredict a sum with a legacy octal number

Line 1 starts with a 0, and line 2 doesn't. What does line 3 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
var a = 010;
var b = 10;
console.log(a + b);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict substr with a negative start

    What does line 1 print? Then write the same call with slice.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    console.log("cupcake".substr(-4, 2));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict a function in a block that never runs

      The if condition is false. What does line 6 print? Then rewrite the program so it means the same thing in strict mode.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      if (false) {
        function brew() {
          return "tea";
        }
      }
      console.log(typeof brew);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind where the 4 goes

        What does line 6 print? Then rewrite the program so it prints 4.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        try {
          throw 3;
        } catch (price) {
          var price = 4;
        }
        console.log(price);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePredict two ways to write the __proto__ key

          What does line 4 print? Then rewrite lines 2 and 3 so that both objects get menu as their prototype, without writing __proto__.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          var menu = { kind: "menu" };
          var a = { __proto__: menu };
          var b = { ["__proto__"]: menu };
          console.log(a.kind, b.kind);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeRewrite three old features for strict mode

            Rewrite the program without any Annex B feature, so that it works with "use strict" on line 1. What does your program print?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            var day = new Date(2024, 0, 15);
            var code = "teacup".substr(0, 3);
            console.log(day.getYear() + 1900, code, escape("tea & cake"));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, first ask whether the code is sloppy, strict or a module. Then ask what the old feature does there. Each answer explains why it's right or wrong.

              The Annex B quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does adding a legacy octal number and a modern one print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(012 + 0o12);

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this classic script with <!-- in the middle of a line print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                var count = 2 <!-- 5;
                console.log(count);

                Choose an answer to see the explanation.

              3. Question 3 of 7Which of these is a SyntaxError in strict code?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does checking brew before and after its block print in sloppy mode?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(typeof brew);
                if (true) {
                  function brew() {}
                }
                console.log(typeof brew);

                Choose an answer to see the explanation.

              5. Question 5 of 7What is true about trimLeft and trimStart?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which pair should replace escape and unescape in new code?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does getYear give for a date in 2024?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(new Date(2024, 5, 1).getYear());

                Choose an answer to see the explanation.

              Key takeaways

              Annex B lets old websites keep working. Knowing its features lets you read old code, and replace each one safely.

              • Annex B is the part of the spec that lists old features browsers must keep. The spec says new code should not use them.
              • In a classic script, <!-- and a line-starting --> are comments. Modules reject them.
              • 010 and "\101" are legacy octal. Strict code rejects them, so write 0o10 and "\x41".
              • Use encodeURIComponent over escape, slice over substr, and trimStart over trimLeft.
              • A plain __proto__: key sets the prototype. Use Object.getPrototypeOf and Object.create in new code.
              • RegExp.$1 belongs to the whole page. Read groups from the match result instead.
              • In sloppy code, a function declared in a block is copied out of it. In strict code, it stays inside.
              • catch (e) { var e = 1; } works in strict code too, but the 1 goes into the catch's e.
              • getYear counts from 1900. Use getFullYear.

              Remember the one-liner.
              Annex B keeps old features alive for old websites; read them, but write the modern replacement.

              Coming next: Deprecated & obsolete features. It covers more old features you will find in old code, and what replaced them.

              CompleteFrontend Clear concepts. Working examples.