編寫高性能的JavaScript代碼
這篇文章告訴你一些簡單的技巧來優化JavaScript編譯器工作,從而讓你的JavaScript代碼運行的更快。尤其是在你游戲中發現幀率下降或是當垃圾回收器有大量的工作要完成的時候。
單一同態:
當你定義了一個兩個參數的函數,編譯器會接受你的定義,如果函數參數的類型、個數或者返回值的類型改變編譯器的工作會變得艱難。通常情況下,單一同態的數據結構和個數相同的參數會讓你的程序會更好的工作。
function example(a, b) {
// 期望a,b都為數值類型
console.log(++a * ++b);
};
example(); // 不佳
example(1); // 仍然不佳
example("1", 2); // 尤其不佳
example(1, 2); // 很好
展開:
編譯器會在編譯的時候求出變量的值并且展開它(最佳實踐),因此在程序執行前可以盡量多的表達信息。常量和變量一樣可以被展開,只要它們沒有用任何的與運行期有關的運算。
const a = 42; // 很容易展開
const b = 1337 * 2; // 可以求值
const c = a + b; // 也可以求值
const d = Math.random() * c; // 只能展開'c'
const e = "Hello " + "Medium"; // 其他類型的值也可以
// 展開前
a;
b;
c;
d;
e;
// 展開后
// 會在編譯的時候做好這些!
42
2674
2716
Math.random() * 2716
"Hello Medium"
函數內聯:
JIT編譯器會找出你的代碼中哪些部分是經常執行的。在編譯的時候通過將函數分成小塊來將代碼塊內聯并且熱追蹤函數之后代碼會執行的更快。
// 以下這些會內聯
// [?] 單一的返回語句
// [?] 返回總是一樣的
// [?] 返回時單一同態的
// [?] 參數是單一同態的
// [?] 函數體是一個單一的語句
// [?] 不是包裹在另一個函數體內
// ...
function isNumeric(n) {
return (
n >= 48 && n <= 57
);
};
let cc = "8".charCodeAt(0);
// 內聯前
if (isNumeric(cc)) {
}
// 內聯之后
if (cc >= 48 && cc <= 57) {
}
Declarations:
避免在頻繁調用的函數里聲明函數/閉包或對象。對象(也包括函數,對象)會被壓到堆里,垃圾回收器會影響這個堆,那里有很多 wat和wut 需要確定下一步(像釋放與否)
相反,聲明一個變量會很快,因為它們是被壓到棧里。比如,一個函數會有自己的棧,與函數相關的變量都會壓到這個棧里,無論何時這個函數退出,棧也隨著釋放。
// 欠佳
function a() {
// 決不再函數里面申明函數
// 會在每次調用函數分配資源
let doSomething = function() {
return (1);
};
return (doSomething());
};
let doSomething = function() {
return (1);
};
// 很好
// 在函數外申明 'doSomething'
// 因此可以只調用它,而不是
// 在每次調用'b'去申明和調用
function b() {
return (doSomething());
};
參數:
函數調用的代價是昂貴的(如果編譯器不能內聯它們)。嘗試去使用盡可能少的參數并且不在函數體內修改參數。
function mul(a, b) {
return (arguments[0]*arguments[1]); // 欠佳, 很慢
return (a*b); // 很好
};
function test(a, b) {
a = 5; // 欠佳, 不修改參數標識
let tmp = a; // 很好
tmp *= 2; // 可以修改偽造的 'a'
};
數據類型:
嘗試盡可能多的取用數值和布爾類型,它們在比較操作中比其他基本類型要快很多。比如,聲明一個字符串類型會偷偷的造成一大堆垃圾數據,因為字符串是一個復雜的有很多預設屬性的對象。
同時,避免操作負數和多位小數的雙精度浮點數。
const ROBOT = 0;
const HUMAN = 1;
const SPIDER = 2;
let E_TYPE = {
Robot: ROBOT,
Human: HUMAN,
Spider: SPIDER
};
// 欠佳
// 避免在大任務中緩存字符串(最好在一般中場景也不)
if (entity.type === "Robot") {
}
// 很好
// 編譯器會算出數值表達式
// 沒有深度計算會更快
if (entity.type === E_TYPE.Robot) {
}
// 完美
// 右側的二元表達式也是可以被展開的
if (entity.type === ROBOT) {
}
嚴格和抽象運算符:
嘗試使用三等號操作如“===”(嚴格的)而不是 “==” (寬松的, 抽象的)。執行嚴格的運算符保證編譯器去預設一個明確的值,而不用以多種情況去比較語句。(比如 n>0=^true),這樣會導致更好的性能方案。
嚴格條件:
JavaScript提供了很棒的語法糖來允許你比如“if (a) then bla”這樣的代碼。這種情況下,編譯器必須去以多種類型去比較“a”來確定是否為真,因為它不知道是那種類型的結果。當然,你應該使用這些很棒的語法糖,但是在快速執行的復雜的有多個返回語句的函數(如 null或者Object)中,你應該考慮以下建議。
有毒:
以下列表里的語言特性,會較少或阻止代碼優化過程。
-
eval
-
with
-
try/catch
對象:
對象實例通常會嘗試共享一個隱藏的類,謹慎的給一個實例化的對象添加一個成員變量,因為這樣會創建一個新的隱藏類并且對編譯器來說這會復雜很多(對你也會一樣)
// 隱藏類'hc_0'
class Vector {
constructor(x, y) {
// 編譯器找到并且期望的成員聲明在這
this.x = x;
this.y = y;
}
};
// 兩個vector對象共享隱藏類'hc_0'
let vec1 = new Vector(0, 0);
let vec2 = new Vector(2, 2);
// 欠佳,vec2這樣會創建新的隱藏類'hc_1'
vec2.z = 0;
// 很好,編譯器知道這個成員
vec2.x = 1;
循環:
緩存你的數組長度屬性,并且使用數組作為一個單一同態的類型。通常避免使用“for..in”或者在數組上循環,因為這會很慢。
循環語句中continue 和 break 語句會比if語句體更快。保持你的循環干凈,把所有的東西打包成一個子函數,這樣編輯器感覺會更舒服。同時,使用預增操作符(_++ i 代替 i ++_)會有一點點的性能提升。
let badarray = [1, true, 0]; // 欠佳, 不要混合類型
let array = [1, 0, 1]; // 好的用法
// 不好的選擇
for (let key in array) {
};
// 更好的
// 但是總是緩存數組大小
let i = 0;
for (; i < array.length; ++i) {
key = array[i];
};
// 很好
let i = 0;
let key = null;
let length = array.length;
for (; i < length; ++i) {
key = array[i];
};
來自:http://www.zcfy.cc/article/writing-efficient-javascript-felix-maier-medium-2279.html