1.前言
從大學到現在,接觸前端已經有幾年了,感想方面,就是對於程式設計師而言,想要提高自己的技術水平和編寫易於閱讀和維護的程式碼,我覺得不能每天都是平庸的寫程式碼,更要去推敲,去摸索和優化程式碼,總結當中的技巧,積極聽取別人的建議,這樣自己的技術水平會提高的更快。那麼今天,我在這裡就分享一下關於javascript方面的寫作的實用技巧和建議,這些技巧和建議是我平常在開發專案上會用到的,希望能讓大家學到知識,更希望能起到一個交流意見的作用,也就是說大家有什麼好的技巧或者建議,歡迎分享,或者覺得我的想法存在什麼問題,歡迎指出!
2.更短的陣列去重寫法
[...new Set([2,"12",2,12,1,2,1,6,12,13,6])]
//[2, "12", 12, 1, 6, 13]
//es6的新特性
複製程式碼
3.物件深淺拷貝
關於物件的深淺拷貝,我個人見解就是有一下幾點:
1.深拷貝和淺拷貝只針對像Object, Array這樣的引用型別資料。
2.淺拷貝是對物件引用地址進行拷貝,並沒有開闢新的棧,也就是拷貝後的結果是兩個物件指向同一個引用地址,修改其中一個物件的屬性,則另一個物件的屬性也會改變。
3.深拷貝則是開啟一個新的棧,兩個物件對應兩個不同的引用地址,修改一個物件的屬性,不會改變另一個物件的屬性。
淺拷貝
var myInfo={name:'守候',sex:'男'};
複製程式碼
var newInfo=myInfo;
複製程式碼
newInfo.sex='女';
複製程式碼
console.log(myInfo) //{name: "守候", sex: "女"}
複製程式碼
假-深拷貝
假-深拷貝這個是自己隨性命名的,大家看看就好,別當真!
var myInfo={name:'守候',sex:'男'};複製程式碼
var newInfo=Object.assign({},myInfo)複製程式碼
newInfo.sex='女';複製程式碼
console.log(myInfo) //{name: "守候", sex: "男"}
console.log(newInfo) //{name: "守候", sex: "女"}
複製程式碼
真-深拷貝
真-深拷貝這個是自己隨性命名的,大家看看就好,別當真!
看著深淺拷貝,區別寫法很簡單,但是那個上面的深拷貝寫法是有問題的。看下面案例
var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=Object.assign([],arr)
//截斷陣列
newArr.length=1
console.log(newArr)//[{a:1,b:2}]
console.log(arr)//[{a:1,b:2},{a:3,b:4}]
//操作newArr,這裡看著對arr沒影響,實際上已經挖了一個坑,下面就跳進去
newArr[0].a=123
//修改newArr[0]這個物件,也是影響了arr[0]這個物件
console.log(arr[0])//{a: 123, b: 2}複製程式碼
為什麼會這樣呢,因為Object.assign並不是深拷貝,是披著深拷貝外衣的淺拷貝。最多也是Object.assign會課拷貝第一層的值,對於第一層的值都是深拷貝,而到第二層的時候就是 複製引用。類似的情況還有,slice方法和concat方法等。
要解決這個問題,就得自己封裝方法!如下
//利用遞迴來實現深拷貝,如果物件屬性的值是引用型別(Array,Object),那麼對該屬性進行深拷貝,直到遍歷到屬性的值是基本型別為止。
function deepClone(obj){
if(!obj&& typeof obj!== 'object'){
return;
}
var newObj= obj.constructor === Array ? [] : {};
for(var key in obj){
if(obj[key]){
if(obj[key] && typeof obj[key] === 'object'){
newObj[key] = obj[key].constructor === Array ? [] : {};
//遞迴
newObj[key] = deepClone(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=deepClone(arr)
console.log(arr[0])//{a:1,b:2}
newArr[0].a=123
console.log(arr[0])//{a:1,b:2}複製程式碼
還有一個方法就是簡單粗暴法,我現在在用的一個方法!原理很簡單,就是先把物件轉成字串,再把字串轉成物件!也能實現同樣效果
var newArr2=JSON.parse(JSON.stringify(arr));
console.log(arr[0])//{a:1,b:2}
newArr2[0].a=123
console.log(arr[0])//{a:1,b:2}複製程式碼
上面所說的淺拷貝,真假深拷貝(自己隨性命名的),這幾種情況,在開發上都有可能要用到,至於要使用哪一種方式,視情況而定!
4.使用事件委託
一個簡單的需求,比如想給ul下面的li加上點選事件,點選哪個li,就顯示那個li的innerHTML。這個貌似很簡單!程式碼如下!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="ul-test">
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
</body>
<script type="text/javascript">
var oUl=document.getElementById("ul-test");
var oLi=oUl.getElementsByTagName("li");
for(var i=0,len=oLi.length;i<len;i++){
oLi[i].addEventListener("click",function(){
alert(this.innerHTML)
})
}
</script>
</html>複製程式碼
很簡單,這樣就實現了,實際上這裡有坑,也待優化!
1.for迴圈,迴圈的是li,10個li就迴圈10次,繫結10次事件,100個就迴圈了100次,繫結100次事件!
2.如果li不是本來就在頁面上的,是未來元素,是頁面載入了,再通過js動態載入進來了,上面的寫法是無效的,點選li是沒有反應的!
所以就者需要用事件委託(即使不考慮上面的第二種情況,也是建議使用事件委託)!程式碼如下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="ul-test">
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
</body>
<script type="text/javascript">
var oUl=document.getElementById("ul-test");
oUl.addEventListener("click",function(ev){
var ev=ev||window.event;
var target=ev.target||ev.srcElement;
//如果點選的最底層是li元素
if(target.tagName.toLowerCase()==='li'){
alert(target.innerHTML)
}
})
</script>
</html>
複製程式碼
這樣寫,即使是動態新增進來的li點選也有反應,還有一個就是ul只有一個,事件繫結在ul上,無論li多少個,都是新增一次事件!但是也是可能會有問題,如果li下面還有子元素,那麼點選的時候,target可能不是li,而是滑鼠點選那個位置的最底層元素!如下圖,如果滑鼠點選白色區域,那個target就是body元素,滑鼠點選綠色區域target就是div元素,滑鼠點選藍色區域target就是ul,點選橙色就是li。
5.使用物件作為函式引數
大家試想下這樣一個函式--函式接受幾個引數,但是這幾個引數都不是必填的,函式該怎麼處理?是不是下面這樣
function personInfo(name,phone,card){
...
}
//以上函式,可以任意傳引數。比如我想傳card等於1472586326。這下是不是這樣寫
personInfo('','','1472586326')複製程式碼
有沒有覺得上面寫法奇怪,不太優雅?下面這裡看著舒服一點!
function personInfo(opt){
...
}
personInfo({card:'1472586326'})複製程式碼
再想一下,如果一個函式,引數很多,怎麼處理?
function test(arg1,arg2,arg3,arg4,arg5,arg6,arg7){
...
}複製程式碼
密集恐懼症復發沒有復發?下面這樣看著會舒服一點!
function personInfo(opt){
...
}複製程式碼
最後再想一下,如果需求改了,操作函式也要改!函式也要增加一個引數。
//原來函式
function personInfo(name,phone,card){
...
}
//修改後
function personInfo(name,age,phone,card){
...
}複製程式碼
這樣就是引數修改一次,函式的引數就要修改一次!如果是用物件,就不會出現這樣問題!
//修改前後都是這樣,變得是函式的操作內容和呼叫時候的傳參!
function personInfo(opt){
...
}複製程式碼
看了上面的幾個栗子,總結來說,就是當函式的引數不固定的時候,引數多(三個或者三個以上)的時候,建議用一個物件記錄引數,這樣會比較方便,也為以後如果引數要改留了條後路!
6.使用push和apply合併陣列
合併陣列這個已經是老生常談的話題了,方法也是多種多樣!
concat
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1=arr1.concat(arr2)
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]複製程式碼
concat會一個全新的陣列,表示arr1和arr2兩個陣列的組合,並讓arr1和arr2不變。簡單吧?
但如果arr1和arr2的長度都很長,那就產生了一個很長很長的陣列,記憶體又被佔用了那麼多。但是陣列長度沒限制!
for
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
for(var i=0,len=arr2.length;i<len;i++){
arr1.push(arr2[i])
}
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]複製程式碼
這裡是往arr1迴圈新增arr2的元素,但是有一個情況,arr1的長度遠小於arr2的長度,是不是迴圈arr1效能更好,迴圈次數更少。處理這個很簡單,但是萬一不知道arr1和arr2到底哪個長度更少呢?而且,for迴圈不夠優雅!(當然,這個可以用迭代方法來替代)
reduce
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1 = arr2.reduce( function(coll,item){
coll.push( item );
return coll;
}, arr1 );
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]複製程式碼
逼格高了一點,而且用ES6的箭頭函式還可以減少一些程式碼量,但它仍然需要一個函式,每個元素都需要呼叫一次。
push.apply
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1.push.apply(arr1,arr2);
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
複製程式碼
逼格看著高,程式碼少,也不會產生新的陣列,也不難理解,就是呼叫arr1.push
這個函式例項的apply
方法,同時把arr2
當作引數傳入,這樣arr1.push
這個方法就會遍歷arr2
陣列的所有元素,達到合併的效果。相當於arr1.push.apply(arr1,[6,7,8,9,10]);
,最後相當於arr1.push(6,7,8,9,10)
。遺憾的就是,這個方法對陣列長度有限制,網上說法是不同瀏覽器,不同的長度限制,一般不超過10萬!
之前是建議用push.apply,但是現在保留意見,就是大家覺得哪個方式用哪個方式!這個沒有一定的對錯!
7.toFixed保留整數
在開發上,經常會遇到最多保留多少位小數或者類似的問題,針對這個,使用toFixed可以很簡單的解決問題,但是如果資料是要和後臺互動的,而且後臺儲存的資料一般是儲存數字型別,而使用toFixed後生成的是一個字串,這下,就需要把toFixed生成的是一個字串轉成數字型別,轉發很多。今天我說一個最簡單--+。程式碼如下
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=+a
console.log(a)//123.37複製程式碼
PS:a=a|0和~~a也可以實現,但是生成的是一個整數,如下
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=a|0
console.log(a)//123
//---------------------------------分割線
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=~~a
console.log(a)//123
複製程式碼
8.其它型別資料轉布林資料
下面的轉換,大家一看就明白了,不多說。
console.log(!!'123')
//true
!!12
//true
!!-1
//true
!![]
//true
!!''
//false
!!null
//false複製程式碼
9.快取變數
for迴圈快取length
var arr=[1,2,3,4,5,6]
for(var i=0,i<arr.length;i++){
...
}
//------------------------分割線
var arr=[1,2,3,4,5,6]
for(var i=0,len=arr.length;i<len;i++){
...
}複製程式碼
第一段就是每一次迴圈的時候,都要查詢一次arr.length。第二段程式碼就是快取了arr.length,每次對比len就好,理論上是第二段程式碼的寫法比較好,效能比較高!但是隨著瀏覽器的發展,這個細節的效能上的影響貌似遠遠小於預期,現在還是建議快取!我寫了下面的測試用例(谷歌瀏覽器測試)!
var arr100=[], arr10000=[];
for(var i=0;i<100;i++){
arr100.push(i)
}
for(var i=0;i<10000;i++){
arr10000.push(i)
}
//快取情況
function testCache(arr){
console.time();
for(var i=0,len=arr.length;i<len;i++){
}
console.timeEnd()
}
//不快取情況
function testNoCache(arr){
console.time();
for(var i=0,len=arr.length;i<len;i++){
}
console.timeEnd()
}
testCache(arr100)//default: 0.007ms
testCache(arr10000)//default: 0.035ms
testNoCache(arr100)//default: 0.012ms
testNoCache(arr10000)//default: 0.109ms
//這只是一個最簡單的陣列,如果遍歷的是一個nodeList(元素列表),效果可能會更明顯。
複製程式碼
元素事件
這裡我用jquery來講解,比較容易理解,原生js也是這個道理!如下程式碼
$('.div1').click(function(){
...
})
//--------------------------分割線
var $div1=$('.div1');
$div1.click(function(){
...
})複製程式碼
上面的程式碼,改變的也是快取了$('.div1'),但是這裡就建議是第二種寫法了,因為第一種點選一次就要查詢一次.div1,Dom的操作還是能減少就減少!
10.使用innerHTML新增元素
比如有一個需求,往ul
裡面新增10個li
,兩種方法,如下程式碼
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="ul-test">
</ul>
</body>
<script type="text/javascript">
var oUl=document.getElementById("ul-test");
//createElement方式
console.time();
for(var i=0;i<10;i++){
var oLi=document.createElement('li');
oLi.innerHTML=i;
oUl.appendChild(oLi);
}
console.timeEnd();
//innerHTML方式
console.time();
var _html='';
for(var i=0;i<10;i++){
_html+='<li>'+i+'</li>'
}
oUl.innerHTML=_html;
console.timeEnd();
</script>
</html>
複製程式碼
大家把程式碼用瀏覽器開啟,發現基本是第二種方式更快,第8點也說了,DOM操作能少就少!第一種要操作10次DOM,第二種只需要操作1次DOM。還有一個就是,這個只是很簡單的li,如果是下面的列表呢?用第一種方式,得createElement多少次,innerHTML多少次,appendChild多少次?程式碼多,各個節點的邏輯和巢狀關係也亂!用第二種方式就是一個拼接字串的操作,比第一種方式好多了,如果用es6的模板字串,就更簡單了!
11.將引數轉成陣列
函式裡的arguments,雖然擁有length屬性,但是arguments不是一個陣列,是一個類陣列,沒有push,slice等方法。有些時候,需要把arguments轉成陣列,轉的方法也不止一個,推薦的是是下面的寫法!
var _arguments=Array.prototype.slice.apply(arguments)
複製程式碼
12.函式節流
這裡拿一個栗子說,比如mousemove,onscroll,onresize這些事件觸發的時候,可能已經觸發了60次事件,這樣很消耗效能,而且實際上,我們並不需要這麼頻繁的觸發,只要大約100毫秒觸發一次就好!那麼這樣就需要函式節流了!
普通寫法
var count = 0;
function beginCount() {
count++;
console.log(count);
}
document.onmousemove = function () {
beginCount();
};複製程式碼
效果
節流寫法
var count = 0;
function beginCount() {
count++;
console.log(count);
}
function delayFn(method, thisArg) {
clearTimeout(method.props);
method.props = setTimeout(function () {
method.call(thisArg)
},100)
}
document.onmousemove = function () {
delayFn(beginCount)
};
複製程式碼
效果
這種方式,其實是有問題的,在不斷觸發停下來等待100ms才開始執行,中間操作得太快直接無視。於是在網上找到下面這種方案!
第二種節流寫法
function delayFn2 (fn, delay, mustDelay){
var timer = null;
var t_start;
return function(){
var context = this, args = arguments, t_cur = +new Date();
//先清理上一次的呼叫觸發(上一次呼叫觸發事件不執行)
clearTimeout(timer);
//如果不存觸發時間,那麼當前的時間就是觸發時間
if(!t_start){
t_start = t_cur;
}
//如果當前時間-觸發時間大於最大的間隔時間(mustDelay),觸發一次函式執行函式
if(t_cur - t_start >= mustDelay){
fn.apply(context, args);
t_start = t_cur;
}
//否則延遲執行
else {
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
}
};
}
var count=0;
function fn1(){
count++;
console.log(count)
}
//100ms內連續觸發的呼叫,後一個呼叫會把前一個呼叫的等待處理掉,但每隔200ms至少執行一次
document.onmousemove=delayFn2(fn1,100,200)複製程式碼
我現在函式節流用得很少,這兩個寫法是比較基礎的,希望大家能共享下自己的比較好的方法!
13.其他寫作建議
關於其它的一些寫法技巧和建議,都是比較老生常談的,比如命名規範,函式單一性原則等。這一部分內容我自己總結和別人寫的基本一致!我就不展開說了(感覺展開說也基本是複製貼上別人的文章,這事我不幹),所以我推薦大家去看這篇文章(如何優雅的編寫 JavaScript 程式碼)。有些知識我也是從這裡獲得的!
14.小結
好了,關於我自己總結的一些實用技巧和建議,就到這裡了!關於javascript的技巧和建議,這點大家還是要多看網上的資源,也要自己多總結,畢竟我自己總結的只是我自己發現的,只是冰山一角。但還是希望這篇文章能幫到大家,讓大家學習到知識。當然,更希望的是能起到一個交流意見的作用。如果大家有什麼建議,技巧。也歡迎分享。覺得我哪裡寫錯了,寫得不夠好,也歡迎指出!讓大家一起互相幫助,互相學習!
-------------------------華麗的分割線--------------------
想了解更多,關注關注我的微信公眾號:守候書閣