JavaScript代碼優化 --- 長期更新

自2015年6月17日(ECMAScript 2015(ES6)正式版發佈至今已快三年,衆多新特性使得JavaScript不再是一門“簡陋”的語言,Node端在6.4.0之後的版本對ES6的原生支持已達到95%(參考:node各版本ES6支持程度表),而前端藉助babel的編譯也使得我們幾乎可以隨心所欲的在代碼中使用各種ES6及之後版本的新特性而不用考慮不同瀏覽器(版本)的支持情況,我相信各位前端從業者or愛好者肯定也已經將一些新特性應用到了自己的項目中用於提升開發效率等。本文依據自己的體驗與借鑑他人成果(會附原地址)記錄了一些在JavaScript代碼層面的優化,我會儘可能寫得詳盡,文章長期更新,內容之間可能會較爲混亂敬請見諒!

1. 尾調用優化:

來源: 阮一峯ES6入門-函數擴展-7
定義: 尾調用指函數的最後一步調用另一個函數
前情概要: V8引擎對JavaScript函數調用會形成一個“調用記錄”,又稱”調用幀“,用於保存調用位置和內部變量信息等,其中一個典型就是“閉包”。如果在函數A的內部調用函數B,那麼在A的調用幀上方還會形成一個B的調用幀,等到B運行結束將結果返回A,B的調用幀纔會消失,如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推···所有的調用幀就形成了一個”調用棧“。 while(!kandong) {readAgain()}
調用棧示例:

function A() {
	const x = 1;
	const y = 2;
	return B(x + y);
}
A();

//上述函數調用棧等同於
function A() {
	return B(3);
}

//上述函數調用棧等同於
B(3);

如果函數A最後是const z = B(x + y);return z,那麼A就要保存內部變量x和y的值與B的調用位置等信息,但如代碼中這樣寫,調用B之後函數A的工作就結束了,同時A的調用幀就可以不再保存只留下B的,因此最終的調用幀只剩下B(3)。
**意義:**減少調用棧的長度,節省內存。
**優化示例:**部分人在前端筆試面試或其他渠道應該會遇到用JavaScript實現一個Fibonacci數列,實現方式很多,代碼大部分人都能寫出來,網上常見的一種實現方式爲:

function Fibonacci(n) {
	if (n <= 1) return 1;
	return Fibonacci(n - 1) + Fibonacci(n - 2);
}

這種方式簡明扼要,代碼比較容易看懂,不過因爲每次遞歸都會向內存中保存新的調用幀,所以當n比較大時瀏覽器就會由於堆棧溢出引發崩潰,這種時候就需要使用到”尾調用優化了“,即將return的內容重構爲單個函數。
大佬寫法:

'use strict';
function newFibonacci(n, sum1 = 1, sum2 = 1) {
	if (n <= 1) return sum2;
	return newFibonacci(n - 1, sum2, sum1 + sum2)
}

這樣當n比較大時主要影響計算力,內存方面就不會存在問題了。

注:使用尾調用優化需要開啓嚴格模式,ES6類和模塊的內部時默認開啓嚴格模式的。

2.空對象作鍵值對時創建優化

來源: Hash maps without side effects
詳解: 有時我們習慣創建一個“{}”來保存鍵值對信息,比如數組去重啊之類的,雖然ES6出了Map專門用來做鍵值對存儲,但畢竟{}習慣了用着順手,不過用{}當鍵值對它的_proto_屬性就不發揮作用而佔用內存,不是很合理,因此可以使用const map = Object.create(null)來創建類{}效果,這樣創建的對象是默認沒有任何屬性的,特別純,用法也沒有差異。
效果:
這裏寫圖片描述
使用場景: 不用到prototype便可以考慮用此法替代,也不用強求,可讀性也是挺重要的,遇到key不爲String的情況可以使用ES6的Map結構。

3.減少作用域向上查找次數

來源: 基本操作,來源已不詳。
概念: 當在某下層作用域多次用到一個上層對象或其某些屬性時,在當前作用域創建一個變量暫存,可減少查找耗時。
例子: 看過jQuery源碼的同學都知道jQuery剛開始就是(function(window,undefine){…})(window,undefined),這個undefined有另類作用此處不詳說,大概就用來保證undifined一定是undefined,想了解的同學可自行查閱jQuery源碼分析相關內容,另一個window的作用就是進行作用域降級,將全局作用域window編程局部作用域,這樣當內部需要添加全局變量或使用到window時就能至少少訪問一層,加快代碼運行效率。
使用場景: 這個優化方式比較常用的,類似操作的還有css style、const tempState = Object.assign({},this.state),之類的,少一次訪問,多一次速度優化,平時注意點大部分地方都可以用到,屬於犧牲內存優化速度的範疇。

4.使用位運算替代一些計算方式

來源: 運算符——阮一峯
概念: 數值和字符串在內存中都是以0和1的形式存儲,每個0或1稱爲一個‘位’,位運算符包括與(&)、或(|)、非(~)、亦或(^)、左移(<<)、右移(>>)、帶符號右移(>>>),使用位運算符進行計算操作稱爲位運算。
優劣: 位運算是很底層的計算,速度很快,但不容易理解。
常用場景: 由於是二進制操作,因此在某些涉及2的計算就可以考慮是否能使用位運算符,常用場景有:

  1. 判斷奇偶性:n & 1 ? 'odd ' : 'even'
  2. 2的n次方:1 << n
  3. 向下取整:~~decimals
  4. 交換兩個整數的值:a ^= b;b ^= a;a ^= b;
  5. x乘以2的n次方:x << n
  6. x除以2的n次方: x >> n
    注意事項: 可讀性不強,畢竟前端了解這個的不多,然後就是位運算符的計算優先級不高,所以別忘了加小括號。

5. cloneNode()拷貝節點取代反覆新建

來源: 寫項目的時候想到優化,然後去搜了搜。Node.cloneNode - Web API接口 | MDN
概念: 當項目中涉及某dom元素反覆創建使用刪除(如果不刪除可以考慮只更改css),那麼每次都createElement消耗是較大的,而且代碼量也多,這時可以考慮先初始化一個節點,之後調用該節點的cloneNode()進行節點拷貝,速度上會有較大的提升(莫不是亙古不變的複製比新建塊?)。
實測效果: 阿西吧,萬萬沒想到,創建節點比拷貝節點快難道cloneNode()的實現是createElement加屬性拷貝嗎·····各位,勇敢的追逐create吧。。。。。

後期附:根據這個問題我查閱了一些相關書籍(如高性能JavaScript),使用cloneNode和createElement在不同瀏覽器上速度有所差異,而且也和所要拷貝(創建)的節點複雜度有關,所以沒有絕對的誰快誰慢,大家可以根據自己開發場景進行選擇。

6. 判斷條件多時使用Array/Object/Map/WeakMap代替if else和switch

來源: 基本操作and高性能JavaScript
概念: 如果有很長的條件控制,用if else和switch就要經過多次判斷,最壞的結果要走過所有條件而且if else語句會不夠直觀switch代碼會特長,這時候就可以考慮來一份Array/Object/Map/WeakMap套餐。
套餐優劣: 四者速度相當,Array可以代碼最少,但是由於用下標獲取,在代碼可讀性、擴展性、魯棒性上都有一定的劣勢;Object、Map和WeakMap都是鍵值對形式存儲,優點兼具直觀明瞭,Object使用最簡單但不能key的類型有限制,Map夠專業但使用麻煩,前面這三者都會造成對象引用導致內部變量無法被回收(V8的引用計數回收機制),WeakMap弱引用避免內存泄漏但key只能是對象,用起來相對麻煩一些。
代碼示例:

/* 
** 需求:我做了一個toC的服務盈利型app,已收集大量用戶身份數據,現在準備
** 根據不同身份的用戶進行不同的服務定價,假設基本價格爲10元,人羣價格劃分
** 如下:小蘿莉2元、古風系女子4元、可愛的coser6元、常規女性8元、常規男性
** 10元、苦逼程序員12元、邪惡資本家99元、死肥宅不賣(用不上)。
** 提供性別(1男0女)、身份信息(string),假設女性用戶多(條件排序)。
*/
// if else寫法
let price = 10;
if (sex === 0) {
	if (identity === '常規女性') {
		price = 8;
	} else if (identity === '小蘿莉') {
		price = 2;
	} else if (identity === '古風系女子') {
		price = 4;
	} else {
		price = 6;
	}
} else {
	if (identity === '常規男性') {
		price = 10;
	} else if (identity === '苦逼程序員') {
		price = 12;
	} else if (identity === '邪惡資本家') {
		price = 99;
	} else {
		price = NaN;
	}
}

// switch寫法
let price = 10;
switch (identity) {
	case '常規女性':
		price = 8;
		break;
	case '常規男性':
		price = 10;
		break;
	case '苦逼程序員':
		price = 12;
		break;
	case '死肥宅':
		price = NaN;
		break;
	case '邪惡資本家':
		price = 99;
		break;
	case '小蘿莉':
		price = 2;
		break;
	case '古風系女子':
		price = 4;
		break;
	case '可愛的coser':
		price = 6;
		break;
	default:
		break;
}

// Array寫法
const arr = [2, 4, 6, 8, 10, 12, 99, NaN];
let price = arr[i];

// Object寫法
const object = {
	'小蘿莉': 2,
	'古風系女子': 4,
	'可愛的coser': 6,
	'常規女性': 8,
	'常規男性': 10,
	'苦逼程序員': 12,
	'邪惡資本家': 99,
	'死肥宅': NaN,
}
let price = object[identity];

// Map寫法
const map = new Map([['小蘿莉', 2], ['古風系女子', 4],
	['可愛的coser', 6], ['常規女性', 8], ['常規男性', 10],
	['苦逼程序員', 12], ['邪惡資本家', 99], ['死肥宅', NaN]]);
let price = map.get(identity);

最終用誰可以自己根據實際情況抉擇。

7.熟練操作Array的新方法

來源: Array - JavaScript | MDN
使用:

/* 主要針對簡單類型的方法:*/
const array = [1, 2, 3, 5, 4, 5];
let n = 5;
// 需求:n是否在array中
if (array.indexOf(n) !== -1) {/* n在array中 */} //之前很常用
if (array.includes(n)) {/* n在array中 */} // 更爲語義化,且可傳入第二個參數限定範圍(ES 2016成爲標準)
// 需求:n在array中出現的最後一個位置
let lastIndex = array.lastIndexOf(n);


/* 主要針對複雜類型的方法 */
const persons = [{name: '小明', age: 17}, {name: '小紅', age: 22}, {name: '小剛', age: 20}];
// 需求:找到第一個成年的人
let firstAdult = persons.find(person => person.age >= 18);  // {name: '小紅', age: 22}
// 需求:第一個成年人所在的數組索引位置
let firstAdultIndex = persons.findIndex(person => person.age >= 18); // 1
// 需求:獲得所有成年人
let adults = persons.filter(person => person.age >= 18); // [{name: '小紅', age: 22}, {name: '小剛', age: 20}]
// 需求:是否有成年人
let hasAdult = persons.some(person => person.age >= 18); // true
// 需求:是否都成年
let isAllAdult = persons.every(person => person.age >= 18); // false
// 需求:總年齡
let sumAge = persons.map(person => person.age).reduce((sum, age)=> sum + age); // 59
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章