In September 2016, I shared this topic on Tencent’s IMWeb Conf. If the article was too long and messy, I could check the slides on GitHub

typeof null === 'object'

This is a well-known mistake. The problem actually stems from a bug in the first JavaScript implementation.

Note: The code referenced in the above article is not actually the original implementation, but Brendan Eich himself tweeted that it was a abstraction which could be read as a way to admit that it was a bug in the code.

typeof NaN === 'number'

Not sure if this counts as a design error, but it’s certainly counterintuitive.

Speaking of nans, there’s a lot more interesting about Idiosyncrasies of NaN that’s worth watching in this 13-minute talk.

NaN.isNaN().Number.isNaN()

NaN is the only value in JavaScript that is not equal to itself. While there is a good reason for this design (see the aforementioned talk, in THE IEE 754 specification there are so many binary sequences that can be treated as nans that any two NAns are likely to have different binary representations), it’s worth laughing at anyway…

The isNaN() function has a very confusing naming and behavior:

  1. It’s not just used to determine if a value isNaNBecause it also returns for all non-numeric valuestrue;
  2. But you can’t say that it’s used to determine whether a value is a number, because according to the previous paragraph,NaNIs of typenumber, should be considered a number.

Fortunately, ES2015 introduced number.isnan () to sort things out: it only returns true if the argument isNaN. However, this approach is still a little late…

Automatic Semicolon insertion (ASI) mechanism

Restricted Productions

According to Brendan Eich, when JavaScript was first designed, superiors demanded that the language’s syntax be Java-like. So, like Java, JavaScript statements need to be delimited by semicolons when they are parsed. But later, in order to reduce the cost of learning or improve the language’s fault tolerance, he added a correction mechanism of semicolon automatic insertion into grammar parsing.

This is certainly well-intentioned, and many other languages do the same (Swift, for example). The problem, however, is that the JavaScript syntax is not designed to be secure enough for ASI to handle a number of special cases, in some cases mistakenly adding semicolons (these are referred to as Restricted Productions in the standard documentation). The most typical is the return statement

// returns undefined
return
{
    status: true
};

// returns { status: true }
return {
    status: true
};Copy the code

This has resulted in the JavaScript community writing code without curly braces, which is unimaginable in other programming language communities.

Missing a semicolon

There is another problem that ASI will not correct:

var a = function(x) { console.log(x) }
(function() {
    // do something
})()Copy the code

This problem usually occurs when two files are compressed and then spliced together.

Semicolon – less styles

The problem of Restricted Productions is already a feature of the language and cannot be bypassed. In any case, we need to learn and master it.

The second type of problem mentioned earlier, where ASI fails, is really hard to avoid using the mandatory semicolon code style (now easier with ESLint’s no-unexpected-Multiline rule). It is better to practice the semicolon-less code style. Instead of writing semicolons at the end of the line, you should add semicolons to the same semicolon on each of the five operators + – [(/) at the beginning of the line. This has much lower memory cost and error probability than the mandatory semicolon style.

= =.= = =Object.is()

JavaScript is a weakly typed language with implicit type conversions. Thus, the behavior of == is very puzzling:

[] = =! [] // true 3 == '3' // trueCopy the code

So various JavaScript books recommend using === instead of == (except in cases like NULL checking)

But it turns out that === doesn’t always work, with at least two exceptions:

  1. As mentioned earlier,NaN === NaNReturns thefalse
  2. + 0 = = = 0Returns thetrue, however, these two are not equal values (1 / +0 === Infinity; 1 / -0 === -Infinity)

It wasn’t until ES2015 that we had a method to compare whether two values were exactly equal: object.is (), which handled both exceptions to === correctly.

about= =Here is a JavaScript Equality Table to look at

Falsy values

There are at least six false values (equivalent to false in conditional expressions) in JavaScript: 0, null, undefined, false, “, and NaN.

+,-Implicit type conversions associated with the operator

It can be roughly written as follows: + as a binary operator does its best to convert the values on both sides to strings, while – and + as a unary operator do its best to convert the values to numbers.

("foo" + + "bar") === "fooNaN" // true
'3' + 1 // '31'
'3' - 1 // 2
'222' - - '111' // 333Copy the code

null,undefinedAnd the holes of the array.

Having null and undefined in the same language may seem hard to understand at first glance, but here are some discussions:

  • Java has null but only for reference types. With untyped JS, the uninitialized value should not be reference-y or convert to 0.
  • Some discussion on GitHub
  • Null for Objects and undefined for primitives

But holes in the array are a lot harder to understand.

There are two ways to generate holes: var a = [1,, 2]; The second is to use the Array object constructor, new Array(3).

The array methods are very, very, very inconsistent in the treatment of holes. Some skip holes (forEach), some do not process holes but keep holes (map), some eliminate holes (filter), and some treat holes as undefined (join). This is one of the biggest bugs in JavaScript, and it’s hard to make sense of without looking at the documentation.

For details, please refer to these two articles:

  • Array iteration and holes in JavaScript
  • ECMAScript 6: holes in Arrays

Array-like objects

In JavaScript, there are a lot of objects that are like arrays but not arrays. Such objects often have length properties and can be traversed, but they lack some of the methods on the array prototype and are very inconvenient to use. For example, in order for arguments objects to use the Shift method, we often need to write a statement like this:

var args = Array.prototype.slice.apply(arguments)Copy the code

Very inconvenient.

In ES2015, arguments objects are no longer recommended and we can use rest parameters (function f(… Args) {}) instead, the object is an array.

However, in addition to the language standard, the DOM standard also defines a number of array-like objects, such as NodeList and HTMLCollection. For these objects, we can handle them with the spread operator in ES2015:

const nodeList = document.querySelectorAll('div')
const nodeArray = [...nodeList]

console.log(Object.prototype.toString.call(nodeList))   // [object NodeList]
console.log(Object.prototype.toString.call(nodeArray))   // [object Array]Copy the code

arguments

In sloppy mode, assigning arguments changes the corresponding parameter.

function a(x) {
    console.log(x === 1);
    arguments[0] = 2;
    console.log(x === 2);    // true
}

function b(x) {
    'use strict';
    console.log(x === 1);
    arguments[0] = 2;
    console.log(x === 2);    // false
}

a(1);
b(1);Copy the code

In the case of function-level scope and Variable promotion

Functional scope

I think you’ve all seen examples from butterfly books:

// The closure in loop problem for (var i = 0; i ! = = 10; ++i) { setTimeout(function() { console.log(i) }, 0) }Copy the code

Functional scope itself is fine, but it can be very counterintuitive in a lot of code if you have to use only functional scope, as in the example of the loop above. It is much easier for programmers to determine the scope of a variable based on the position of the curly braces than to find the outer function.

Previously, the only way to solve this problem was to use closure + IIFE to create a new scope, and the code was pretty ugly (the code blocks that follow the with and catch statements are block-level scopes, but they are not generic).

Thanks to the introduction of let/const in ES2015, we can finally use true block-level scopes.

Variable ascension

When executing code, the JavaScript engine processes all variable declarations in the scope, allocates space for variables (called binding in the standard), and then executes the code.

This should not be a problem, but the var declaration will be initialized to undefined (CreateMutableBinding in ES5) as well as allocated space. This will elevate the var declaration to the beginning of the function scope. This is called “collieries”.

Let/const introduced in ES2015 implements temporal dead zone. Although variables declared with let and const will also be allocated space when entering scope, they will not be initialized. ReferenceError is raised if a reference to a variable occurs before the initialization statement.

// without TDZ
console.log(a)  // undefined
var a = 1

// with TDZ
console.log(b)  // ReferenceError
let b = 2Copy the code

At the standard level, this is done by splitting the CreateMutableBing internal methods into CreateMutableBinding and InitializeBinding, Only VarDeclaredNames executes the InitializeBinding method.

let / const

However, the introduction of lets and const also introduces a pit. The main reason is that the naming of these two keywords is not accurate and reasonable.

The fact that const keywords define an immutable binding (similar to final keywords in Java) rather than true constants (constant) is also counterintuitive to many people.

Allen Wirfs-Brock, the lead author of the ES2015 specification, said in a post on ESDiscuss that if he could do it all over again, he would prefer to use let var/let or mut/let instead. Unfortunately, this can only be a beautiful fantasy.

for... in

for… The problem with in is that it iterates through properties on the prototype chain, which you already know needs to be checked by obj.hasOwnProperty(key).

In ES2015+, use for (const key of object.keys (obj)) or for (const [key, value] of object.entries ()) to get around this problem.

Incidentally Object. The keys (), Object, getOwnPropertyNames (), Reflect. OwnKeys () the difference between: We are the most commonly used is generally the Object. The keys () method, the Object. The getOwnPropertyNames () will the enumerable: The false property name is also added, and reflect.ownkeys () adds a Symbol key to this property.

with

The main problem is that it relies on runtime semantics, which affects optimization.

In addition, it will reduce program readability, error-prone, easy to leak global variables.

function f(foo, length) {
  with (foo) {
    console.log(length)
  }
}
f([1, 2, 3], 222)   // 3Copy the code

eval

The problem with Eval is not that you can execute code dynamically, and this ability is not in any way a weakness of the language.

Scope

The first pitfall is that the code snippet passed to Eval as a parameter has access to the closure in which the current statement resides.

Code that executes dynamically with New Function does not have this problem, because the Function generated by new Function is guaranteed to execute in the outermost scope. The arguments object can be retrieved from the new Function.

function test1() { var a = 11 eval('(a = 22)') console.log(a) // 22 } function test2() { var a = 11 new Function('return  (a = 22)')() console.log(a) // 11 }Copy the code

Direct Call vs. Indirect Call

The second pitfall is the difference between calling eval directly and calling it indirectly.

In fact, the concept of “direct invocation” is confusing enough.

First, eval is a member function on a global object;

However, a call such as window.eval() is not a direct call because the base of the call is a global object rather than an “environment record”.

Then there is the matter of history.

  • In the ES1 era,evalThere is no distinction between direct and indirect invocation;
  • Then in ES2, the concept of direct call was added. According to theDmitry Soshnikov laterDistinguishing between these two calls may be for security reasons. Only legal at this pointevalThe way to use it isDirect callIf theevalCalled indirectly or assigned to some other variable, JavaScript engineYou can chooseRaise a Runtime Error (ECMA-262 2nd Edition, p. 63).
  • But when browser vendors tried to implement this feature, they found that it made older sites incompatible.
  • Considering that this was, after all, an optional feature, they eventually opted not to report errors and instead let all calls be made indirectlyevalAll execute in global scope.

    This ensures compatibility with older sites and a degree of security.
  • By the ES5 era, standard-setters wanted to be consistent and normalized with the current conventions, so they removed the optional implementation from the previous standard and mandated indirect invocation insteadevalThe behavior of

The biggest difference between direct and indirect calls is their scope:

function test() {
  var x = 2, y = 4
  console.log(eval("x + y"))    // Direct call, uses local scope, result is 6
  var geval = eval;
  console.log(geval("x + y"))   // Indirect call, uses global scope, throws ReferenceError because `x` is undefined
}Copy the code

The greatest use (and probably the only practical use) of an indirect call to eval is to get global objects anywhere (Function(‘return this’)() can do this too, however) :

Var global = ("indirect", eval)("this"); var global = ("indirect", eval)("this");Copy the code

In the future, this last bit won’t help if Jordan Harband’s System.global proposal makes it into the standard…

In non-strict mode, assigning to an undeclared variable causes a new global variable to be created

Value Properties of the Global Object

NaN, Infinity, undefined are not used as primitive values (null is primitive value), but as property names defined on global objects.

Prior to ES5, these attributes can be overwritten without any additional information. After ES5, they are changed to a non-64x and non-writable system.

However, because these attribute names are not JavaScript reserved words, they can be used as variable names. Even if the properties on a global variable cannot be changed, we can still override the names in our scope.

// logs "foo string" (function(){ var undefined = 'foo'; console.log(undefined, typeof undefined); }) ();Copy the code

Stateful RegExps

In JavaScript, functions on re objects are stateful:

const re = /foo/g
console.log(re.test('foo bar')) // true
console.log(re.test('foo bar')) // falseCopy the code

This makes these methods difficult to debug and thread-safe.

Brendan Eich says that these methods come from Perl 4 in the ’90s, when so much wasn’t thought of

weird syntax of import

The current syntax is import x from ‘y’, but changing it to from y import x makes it more natural and convenient to trigger IDE/editor auto-completion.

Brendan Eich also expressed regret in a post on ESDiscuss.

In addition, while many people believe that the ES2015 module system was borrowed from Python, in fact, according to Dave Herman, the designer of the ES2015 module system, the idea for the module system was mainly borrowed from Racket, It has nothing to do with Python at all (except that the resulting syntax happens to be somewhat similar).

Array constructor inconsistency

This is an API design failure.

// <https://github.com/DavidBruant/ECMAScript-regrets/issues/21>
Array(1, 2, 3); // [1, 2, 3]
Array(2, 3); // [2, 3]
Array(3); // [,,] WATCopy the code

Primitive type wrappers

Primitive type wrappers (Boolean/Number/String…) Absolutely notorious for all the reasonable and irrational comparison rules and type conversions that can drive people crazy, but I won’t go into detail here (too lazy to write).

Brendan Eich tried to solve this problem once and for all with a radical strongly typed solution in JS2/ES4, but then ES4 failed and the proposal fell by the side.

Date Object

The JavaScript Date object is a direct copy of the Java Date class, so these problems are actually inherited from Java (many methods in Java have been deprecated, but JavaScript has evolved over the years. No substitute for Date has been added.

Date.getMonth()

The return value of date.getMonth () is 0.

const d = new Date('2016-07-14')
d.getDate()     // 14
d.getYear()     // 116 (2016 - 1900)
d.getMonth()    // 6Copy the code

Date comparison

$ node
> d1 = new Date('2016-01-01')
Fri Jan 01 2016 08:00:00 GMT+0800 (CST)
> d2 = new Date('2016-01-01')
Fri Jan 01 2016 08:00:00 GMT+0800 (CST)
> d1 <= d2
true
> d1 >= d2
true
> d1 == d2
false
> d1 === d2
falseCopy the code

The reason is that in the abstract relation comparison algorithm, the left and right values will be ToNumber first in certain cases, while the abstract equality comparison will not be transformed, so this situation is caused.

See this ATA article for details

prototype

Prototype has two slots.

The first is that it doesn’t make sense.

There are only two hard things in Computer Science: cache invalidation and naming things

— Phil Karlton

The various ineffable names in JavaScript have become a bit of a joke…

Prototype, as an object attribute, is not the concept of “prototype” at all when we talk about prototype inheritance mechanisms. fallbackOfObjectsCreatedWithNew would be a better name.

Before the introduction of object.getProtoTypeof () in ES5, there was no conventional method to obtain the true prototype of an Object.

However, many browsers implement a non-standard __proto__ (IE excepted), and in ES2015, this extended attribute was standardized.

Object destructuring syntax

The syntax for aliasing variables when deconstructing assignments is a bit convoluted:

/ / < https://twitter.com/Louis_Remi/status/748816910683283456 > / / deconstruction out new variable here is y, it is equivalent to z.x / / 'as' colon can read, convenient memory let {x: y } = zCopy the code

While this isn’t a design mistake (many other languages do it, after all), it’s not intuitive.

Other references

Esdiscuss.org/topic/10-bi… Esdiscuss.org/topic/exclu… Wtfjs.com/ bonsaiden. Making. IO/JavaScript -… www.nczonline.net/blog/2012/0…