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.
- 01Spot Annex B codeRecognize old features such as
010,<!--,substr,__proto__andgetYearwhen you read old code. - 02Know where each one worksSay which old features are a SyntaxError in strict code or in modules, and which still run everywhere.
- 03Replace 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.
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.
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
010and<!--comments. - Old functions and methods, such as
escape,substrandgetYear. - Old rules for normal-looking code, such as a function declared inside an
ifblock.
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.
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
010orsubstr - In real life: New maps print only the new name
- In JavaScript: New code writes
0o10andslice
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.
var price = 3;
<!-- price = 4;
console.log(price);
--> this line is a comment tooLine 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.
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.
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.
// 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.
<!-- 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".
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.
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.
"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.
"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.
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.
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.
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.
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."
| The call | What the two numbers mean | What 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.
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.
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 ".)
In a page, build elements with the DOM instead. Setting textContent always treats the text as text, never as HTML.
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.
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.
__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.
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__.
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.
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.
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.
/(\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.
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.
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.
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.
A replay of the example code, not a view inside a JavaScript engine. The example runs for real in sloppy mode.
script
if (true) { function greet() { return "hello"; }}console.log(typeof greet);console.log(greet());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.
"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.
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.
"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.
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.
"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.
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.
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.
A replay of the example code, not a view inside a JavaScript engine. The example runs for real in sloppy mode.
script
throw "oops";} catch (e) { var e = "fixed"; console.log(e);}console.log(e);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.
"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.
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.
"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.
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.
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.
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.
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.
"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."
| The old code | What happens in sloppy code | What 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 --> comments | They are comments. | They are comments in strict scripts, but SyntaxErrors in modules. |
A function declared in an if block | Its 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. |
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()
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.
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.
console.log(010);console.log(0o10);same88Both programs print the same thing. The modern one is what to write in new code.
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."
| Old code | What to write instead | Why 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 textContent | The 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.getOwnPropertyDescriptor | The new methods are clearer and can set every part of a property. |
RegExp.$1 and RegExp.lastMatch | match[1], match[0] or named groups | The 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 block | let greet; and greet = function () {} in the block | The code then means the same thing in sloppy and strict mode. |
tea: function brew() {} | function brew() {} with no label | A 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
010and labelled functions.substr,escape,__proto__,getYearandRegExp.$1still run. - "
<!--is a SyntaxError in strict code." No. A strict classic script still treats it as a comment. Only modules reject it. - "
08is an octal number." No. 8 is not an octal digit, so sloppy code reads08as the normal number 8. - "
substr(1, 2)andslice(1, 2)give the same result." No. The 2 is a length forsubstrand an end forslice, so"tea"giveseaande. - "
trimLeftis a different method fromtrimStart." 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.$1belongs 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."
| The feature | What is true | What to watch for |
|---|---|---|
| Annex B features | Still 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.
Line 1 starts with a 0, and line 2 doesn't. What does line 3 print?
var a = 010;
var b = 10;
console.log(a + b);"use strict";
var a = 0o10;
var b = 10;
console.log(a + b);010 is octal, so a is 8, and b is 10. Line 3 prints 18. The version shown writes 0o10, which says clearly that it is octal and is allowed in strict code. It prints 18 too.
What does line 1 print? Then write the same call with slice.
console.log("cupcake".substr(-4, 2));console.log("cupcake".slice(-4, -2));Position -4 is position 3, and substr takes 2 characters from there: ca. The version shown uses slice(-4, -2), which starts at the same place and stops before position 5, so it prints ca too.
The if condition is false. What does line 6 print? Then rewrite the program so it means the same thing in strict mode.
if (false) {
function brew() {
return "tea";
}
}
console.log(typeof brew);"use strict";
let brew;
if (false) {
brew = function () {
return "tea";
};
}
console.log(typeof brew);Sloppy mode makes an outer brew set to undefined, so line 6 doesn't throw. The block never runs, so the function is never copied out, and line 6 prints undefined. The version shown declares brew with let outside the block and is strict. It prints undefined too, and means the same thing in both modes.
What does line 6 print? Then rewrite the program so it prints 4.
try {
throw 3;
} catch (price) {
var price = 4;
}
console.log(price);var price;
try {
throw 3;
} catch (error) {
price = 4;
}
console.log(price);The original prints undefined: the 4 goes into the catch's price, and the outer price keeps undefined. The version shown declares price before the try and names the caught value error. Now the 4 goes into the outer price, and it prints 4.
What does line 4 print? Then rewrite lines 2 and 3 so that both objects get menu as their prototype, without writing __proto__.
var menu = { kind: "menu" };
var a = { __proto__: menu };
var b = { ["__proto__"]: menu };
console.log(a.kind, b.kind);var menu = { kind: "menu" };
var a = Object.create(menu);
var b = Object.create(menu);
console.log(a.kind, b.kind);a has menu as its prototype, so a.kind is menu. b only has an own property named __proto__, so b.kind is undefined. Line 4 prints menu undefined. The version shown uses Object.create(menu) for both, which always sets the prototype, so it prints menu menu.
Rewrite the program without any Annex B feature, so that it works with "use strict" on line 1. What does your program print?
var day = new Date(2024, 0, 15);
var code = "teacup".substr(0, 3);
console.log(day.getYear() + 1900, code, escape("tea & cake"));"use strict";
const day = new Date(2024, 0, 15);
const code = "teacup".slice(0, 3);
console.log(day.getFullYear(), code, encodeURIComponent("tea & cake"));slice(0, 3) takes the same 3 characters as substr(0, 3). getFullYear gives 2024 directly. encodeURIComponent encodes the space and & the same way escape does. So the rewritten program prints 2024 tea tea%20%26%20cake, like the original, and runs in strict code.
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.
Question 1 of 7What does adding a legacy octal number and a modern one print?
Read the code, then predictconsole.log(012 + 0o12);Choose an answer to see the explanation.
Question 2 of 7What does this classic script with <!-- in the middle of a line print?
Read the code, then predictvar count = 2 <!-- 5; console.log(count);Choose an answer to see the explanation.
Question 3 of 7Which of these is a SyntaxError in strict code?
Choose an answer to see the explanation.
Question 4 of 7What does checking brew before and after its block print in sloppy mode?
Read the code, then predictconsole.log(typeof brew); if (true) { function brew() {} } console.log(typeof brew);Choose an answer to see the explanation.
Question 5 of 7What is true about trimLeft and trimStart?
Choose an answer to see the explanation.
Question 6 of 7Which pair should replace escape and unescape in new code?
Choose an answer to see the explanation.
Question 7 of 7What does getYear give for a date in 2024?
Read the code, then predictconsole.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. 010and"\101"are legacy octal. Strict code rejects them, so write0o10and"\x41".- Use
encodeURIComponentoverescape,sliceoversubstr, andtrimStartovertrimLeft. - A plain
__proto__:key sets the prototype. UseObject.getPrototypeOfandObject.createin new code. RegExp.$1belongs 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'se.getYearcounts from 1900. UsegetFullYear.
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.