This page is for my friend PoRa in JiangNanGame, who is a excellent new generation PvZ Travel developer. I had promised to him that I would provide necessary helps for him to develop the new things in the game.
This page contains some of my suggestions for writing JavaScript. Although some advice may be boring when we face the modern powerful JavaScript engine, I still hope he can refer to it and see it as a habit to programming.
This page will be updated from time to time. Whenever I have a new idea, I will add it to this page.
Parts of this page have been extracted from: v8.dev/blog/
Try to avoid make difference between objects
The JavaScript engine uses Hidden Class technology to optimize the access of js object's properties. It will create a offset table called "Hidden Class" for objects having same order to initialize dynamic properties.
In order to make use of the hidden class, we should:
- Initialize all object properties in constructor functions with the default value of the correct data type, since changing the data type of property will force js engine to regenerate hidden class.
- Try to avoid adding some new property after initialize an object.
- Try to avoid changing the data type of some property after it is initialized.
- If you need to initialize the properties of the objects from same class in different place, try to always initialize the properties in the same order. That's because the different orders can lead to different hidden classes built.
Bad:
class Point {
x = null;
y = null;
}
const p = new Point();
p.x = 0.1;
p.y = 402.1;
Good:
class Point {
x = -0;
y = -0;
}
const p = new Point();
p.x = 0.1;
p.y = 402.1;
Well, by now you should have noticed that these requirements are easy to meet if you just write the idiomatic, readable code and don't do anything weird!
Prefer Small Integer if possible
The JavaScript engine divides number value into two types: Small Integer(Smi), Float64(including NaN
, Infinity
, -0
, etc)
The range of Smi in modern computer is from to . If an integer number is larger than this range, although it does not appear to be a decimal, it is Float64.
You had better use Smi as possible as you can.
Because:
- Your cpu executes integer operations much faster than floating-point operations.
- If the type of a number value is Float64, it will be stored as HeapNumber, which means it's much harder for js engine to manage the number value.
Avoid putting items with different types into Array one after another
The JavaScript engine divides array into multiple types. The relationship between different types forms a lattice.
Here’s a simplified visualization of that featuring only the most common types:
It’s only possible to transition downwards through the lattice. Once a double number is added to an array of integer (PACKED_SMI_ELEMENTS
), it is marked as an arrary of double (PACKED_DOUBLE_ELEMENTS
), even if you later overwrite the double number with a integer. Similarly, once a hole is created in an array, it’s marked as an sparse array (HOLEY_ELEMENTS
) forever, even when you fill it later.
Here is an example:
Good:
let a = [77, 88, 0.5, true];
Bad:
let a = [];
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts```
In the good example, the engine can know it should build an array that can contain anything (PACKED_ELEMENTS
) and compute the Hidden Class immediately.
In the bad example, a
is an array that only can contain integer number at first. However, when you put a double number(or NaN
, Infinity
) into it, the engine had to convert it as an array that can contain double number. Finally, when you put a boolean value into it, it will be converted as an array that can contain anything. It's obvious that this process will cost much.
Don't make your array sparse
That's because if an array is sparse, the JavaScript engine will permanently convert it to an array of type HOLEY_ELEMENTS
, which will make reading and writing its elements in the future more expensive.
Good:
let s = [];
// make sure that keys are incremental numbers from 0
s[0] = 0;
s[1] = 1;
s[2] = 2;
s[3] = 3;
s[4] = 4;
s[5] = 5;
// or
let s = [0, 1, 2, 3, 4, 5];
// or
let s = [];
s.push(0); /*...*/ s.push(5);
Bad:
let s = [];
s[0] = 0;
s[3] = 1;
s[5] = 2;
s[6] = 3;
s[9] = 4;
s[20] = 5;
const array = new Array(3); // you get a sparse array with 3 holes
array[0] = 'a';
array[1] = 'b';
array[2] = 'c';
// although you fill the holes, the type of array is still `HOLEY_ELEMENTS`
For user-defined function called frequently, don't make it polymorphic
When a function is called frequently with the parameters of same type, the engine might see its parameter's type is fixed and create a optimize assembly code for this function. This is what is called Inline Caching (IC) technology.
So it's bad to suddenly change the type of parameters while calling the function again, which will lead the engine to change or even give up all the optimizations it had made and regenerate the code for the function.
e.g:
function add(a, b) {
return a + b;
}
for (let i = 0; i < 1000; ++i) add(1, 2);
add('a', 'b'); // Bad
Here is a another example:
const each = (array, callback) => {
for (let index = 0; index < array.length; ++index) {
const item = array[index];
callback(item);
}
};
const doSomething = (item) => console.log(item);
each(['a', 'b', 'c'], doSomething);
// `each` is called with `PACKED_ELEMENTS`. V8 uses an inline cache
// (or “IC”) to remember that `each` is called with this particular
// elements kind. V8 is optimistic and assumes that `array.length`
// and `array[index]` accesses inside the `each` function are monomorphic
// (i.e this function only receive an array with `PACKED_ELEMENTS` elements)
// until proven otherwise
//
// For every future call to `each`, V8 checks if the elements kind
// is `PACKED_ELEMENTS`. If so, V8 can re-use the previously-generated
// code. If not, more work is needed.
each([1.1, 2.2, 3.3], doSomething);
// `each` is called with `PACKED_DOUBLE_ELEMENTS`. Because V8 has
// now seen different elements kinds passed to `each` in its IC, the
// `array.length` and `array[index]` accesses inside the `each`
// function get marked as polymorphic. V8 now needs an additional
// check every time `each` gets called: one for `PACKED_ELEMENTS`
// (like before), a new one for `PACKED_DOUBLE_ELEMENTS`.
// This incurs a performance hit.
each([1, 2, 3], doSomething);
// `each` is called with `PACKED_SMI_ELEMENTS`. This triggers another
// degree of polymorphism. There are now three different elements
// kinds in the IC for `each`. For every `each` call from now on, yet
// another elements kind check is needed to re-use the generated code
// for `PACKED_SMI_ELEMENTS`. This comes at a performance cost.
However, native methods (such as Array.prototype.forEach
) can deal with this kind of polymorphism much more efficiently, so consider using them instead of user-defined functions in performance-sensitive situations.
Write Declarative JavaScript instead of Obscure
Although JavaScript is a dynamic and weak-typed language, you should avoid writing obscure code, otherwise the js engine will generate more assembly code for your code.
Bad:
var func = function(obj) {
if (obj) { return obj.x; }
}
// or
var func = function(obj) {
// it's even slower than above!
if (obj != undefined) { return obj.x; }
}
Good:
var func = function(obj) {
if (obj !== undefined) { return obj.x; }
}
If you're not sure if an array subscript is out of the boundary, add a range check instead of trying to access it directly.
When you try to access a property(e.g. a subscript out of the range) which is not present on the array itself, the JavaScript engine has to perform expensive prototype chain lookups. Once a load has run into this situation, the engine remembers that “this load needs to deal with special cases”, and it will never be as fast again as it was before reading out-of-bounds.
Bad (Very very very slow):
const access1 = (ar, idx) => {
let t = ar[idx];
if(t === void 0) return;
return t;
}
const forEach1 = (ar, doSomething) => {
for (let i = 0, item; (item = ar[i]) != null; i++) {
doSomething(item);
}
}
let s = [];
s[-1] = 0; // f**k !!!
s[0] = 1;
s[1] = 2;
s[2] = 3;
Good:
const access2 = (ar, idx) => {
// get the length of array is very cheap in modern js
if(idx < 0 || idx >= ar.length) return;
return ar[idx];
}
const forEach2 = (ar, doSomething) => {
for (let index = 0; index < ar.length; ++index) {
const item = ar[index];
doSomething(item);
}
}
Prefer arrays over array-like objects
Some objects in JavaScript — especially in the DOM — look like arrays although they aren’t proper arrays. It’s possible to create array-like objects yourself:
const arrayLike = {};
arrayLike[0] = 'a';
arrayLike[1] = 'b';
arrayLike[2] = 'c';
arrayLike.length = 3;
This object has a length and supports indexed element access (just like an array!) but it lacks array methods such as forEach on its prototype. It’s still possible to call array generics on it, though:
Array.prototype.forEach.call(arrayLike, (value, index) => {
console.log(`${ index }: ${ value }`);
});
// This logs '0: a', then '1: b', and finally '2: c'.
This code calls the Array.prototype.forEach built-in on the array-like object, and it works as expected. However, this is much slower than calling forEach on a proper array, which is highly optimized in JavaScript engine.
In general, avoid array-like objects whenever possible and use proper arrays instead.
Never use eval()!
Because:
- Programs that use
eval()
are more vulnerable to attacks. - The code that goes through
eval()
is slower because it has to call the JavaScript interpreter, while much other code is optimized by modern JS engines. - The modern JavaScript engine converts your program into machine code. This means that any concept of variable naming is erased. Thus, any use of
eval()
will force the engine to do long, expensive variable name lookups to figure out where the variable exists in the machine code and set its value.