寫在前面:
這篇博文偏應用,改天再寫篇概念性的。該篇幅内容包括:減少DOM操作,時間委托,冒泡事件,Promise相關, 微任務宏任務,作用域,變量提升,閉包,變量類型,深淺拷貝,原型和作用域鍊後續争取把js重點都記錄上,深入淺出。
DOM操作
- 使用文檔碎片減少DOM操作
- 冒泡事件:stopPropagation();阻止向上冒泡
- 事件委托 :減少DOM請求次數
使用文檔碎片減少DOM操作
假設在一個ul中,如果持續添加一百次插入 li 的DOM操作,那這個過程無疑是非常消耗記憶體的,這時不妨使用文檔碎片
fragment
,将插入的100次操作存儲在這,結束後在一并添加到ul中,這樣DOM操作從原本的100次變為一次。
<ul id="list">
</ul>
<script src="index.js"></script>
const list = document.getElementById('list');
// 建立文檔碎片,内容放在記憶體裡,最後将結果一次插入list,減少DOM操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 5; i++) {
const item = document.createElement('li');
item.innerHTML = `事件${i}`;
// 每次插入都是一次DOM操作,消耗性能,是以先往文檔碎片放入li,循環完後在插入list
// list.appendChild(item);
fragment.appendChild(item);
}
list.appendChild(fragment);
阻止冒泡事件
依舊舉個栗子:當點選一個 li , 他沒有綁定事件處理函數,那麼他會往父級元素(一直往上找直到body)看有沒有事件處理函數,有的話同樣觸發。
這樣可以簡便的為子元素綁定處理事件,但弊端也很明顯:如果父元素有處理函數,即使你給子元素綁定了處理事件,那麼在觸發子元素的處理事件同時,還會向上冒泡在觸發父元素的處理函數!!這就需要到
stopPropagation();
阻止冒泡行為。
<div id="one">
<p id="p1">冒泡</p>
</div>
<hr>
<div id="two">
<p id="p4">冒泡</p>
<p id="p5">阻止</p>
</div>
const p3 = document.getElementById('p3');
const one = document.getElementById('one');
const body = document.body;
function bindEvent (elem, type, fn) {
elem.addEventListener(type, fn);
}
bindEvent(one, 'click', function () {
console.log("one的click");
})
bindEvent(body, 'click', function () {
console.log("body的click");
})
bindEvent(p3, 'click', function () {
console.log("p5的click");
})**

在沒有綁定阻止冒泡時,給了body,第一個父元素和第三個子元素綁定處理事件,點選第三個會觸發第三個和body的,點選第一個會觸發父元素和body的。如果隻想觸發第三個事件綁定,可将p3的觸發函數修改為:傳入event,利用event下的
stopPropagation();
方法。
bindEvent(p3, 'click', function (event) {
event.stopPropagation();
console.log("阻止冒泡");
})
事件委托
事件指的是:不在要發生的事件(直接dom)上設定監聽函數,而是在其父元素上設定監聽函數,通過事件冒泡,監聽子元素,來做出不同的響應。
例如在ul中有這麼五個小 li ,要給他們綁定點選事件,可擷取他們标簽名後,利用slice方法對li的類數組轉化為數組,在對其中的 li 添加注冊事件。
<ul id="list">
<li>事件1</li>
<li>事件2</li>
<li>事件3</li>
<li>事件4</li>
<li>事件5</li>
<button id="btn">點選添加委托事件</button>
</ul>
const lis = document.getElementsByTagName('li');
listAray = Array.prototype.slice.call(lis);
listAray.forEach(li => {
addEvent(li, 'click', () => {
alert(li.innerHTML);
})
});
要是有500個li呢,那豈不是周遊綁定500次,這産生的事件監聽器非常消耗記憶體。這時可以找到其父元素,設定事件監聽器,利用
target
給 li 綁定事件監聽。
// 綁定事件處理函數
function addEvent (elem, type, fn) {
elem.addEventListener(type, fn);
}
addEvent(list, 'click', (e) => {
const target = e.target;
if (target.nodeName === 'LI') {
alert(target.innerHTML);
}
})
btn.addEventListener('click', () => {
const li = document.createElement('li');
li.innerHTML = '新增綁定事件';
list.insertBefore(li, btn);
})
這樣,即使不循環周遊 ul 下的 li ,也能通過對 ul 的綁定列印出子元素 li 的資訊。
異步
- 使用promise加載一張圖檔
- then,catch改變promise的狀态
- async , await , 和Promise
- 微任務和宏任務
使用promise加載一張圖檔
考驗對promise的基本使用情況。思路:執行個體化一個promise對象,對圖檔的加載分别成功與否用
resolve, reject
處理。
function loadImage (src) {
// 建立并執行個體化一個promise對象
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img');
// 如果圖檔加載成功,就傳入圖檔進resolve
img.onload = function () {
resolve(img);
}
img.onerror = function () {
const err = new Error(`圖檔加載失敗, url:${{ src }}`)
reject(err);
}
img.src = src;
});
return promise;
}
const url = 'https://cn.bing.com/th?id=OHR.WalhallaOverlook_ZH-CN1059655401_1920x1080.jpg&rf=LaDigue_1920x1080.jpg'
loadImage(url).then(img => {
console.log('img', img);
}).catch(e => {
console.log('err', e);
})
使用then,catch改變promise的狀态
先說結論:
then 和 catch 正常傳回的時候,Promise狀态是 fulfilled ,抛出錯誤的時候,Promise 狀态是 rejected。
fulfilled 狀态的Promise執行then 的回調,而rejected則執行catch的回調。
怎麼了解?看下面例子:
const p1 = Promise.resolve();
console.log('p1', p1);
const p1Then = p1.then(() => {
console.log('p1Then', p1Then);
})
const p1Error = p1.then(() => {
throw new Error('p1 then error')
})
console.log('p1Error', p1Error);
p1Then.then(() => {
console.log('fulfilled狀态走then回調');
}).catch(() => {
console.log('fulfilled狀态走catch回調');
})
p1Error.then(() => {
console.log('p1Error走then回調');
}).catch(() => {
console.log('p1Error走catch回調');
})
首先定義一個 resolve 傳回值的promise對象,列印顯示狀态為 fulfilled ,儲存目前資訊在列印走catch還是then;然後在手動跑出個錯誤,列印目前狀态,在判斷走catch還是then。結果顯示:
再回過頭看先前的結論,發現也不難了解。reject類型的示範也差不多,結論見上。
async , await , 和Promise
這裡也先說這三者的結論:
執行async 函數傳回的都是Promise對象;promise.then的情況對應await ;promise.catch異常情況對應try…catch。
// async結論 :async傳回的都是Promise對象
async function test1 () {
return 1;
}
const result1 = test1();
console.log('result1', result1);
// await 結論:promise.then成功情況對應await
async function test2 () {
const p2 = Promise.resolve(2);
p2.then(data => {
console.log('promise情況:', data);
})
const data = await p2;
console.log('await情況:', data);
}
test2();
// promise.catch異常情況對應try...catch
async function test3 () {
const p3 = Promise.reject(6);
try {
const data3 = await p3;
console.log('data3', data3);
} catch (e) {
console.log('try-catch捕捉的promise異常:', e);
}
}
test3();
例子比較通俗易懂,配合結果應該能更清晰的了解結論。
微任務和宏任務
宏任務有:setTimeout , setInterval , Dom事件,AJAX請求
微任務有:Promise ,async / await
微任務會比宏任務先執行,具體可搜該類型的答應題目進行深入了解。
作用域,變量提升,和閉包
關于作用域,有全局和局部(函數作用域,塊級作用域ES6才有),局部能通路全局,全局不能通路局部。在查找變量的時候,變量的父子級關系就是一條作用域鍊。找同名變量優先局部,注意this引用
變量提升,這得多看幾道作用域輸出變量的題(下面這張我是去牛客網刷js專項訓練截來的)
閉包和匿名函數不要搞混!那麼什麼是閉包,紅寶書說:閉包是有權通路另一個函數作用域的變量函數。也就是說,局部之間并非是不能互相通路的,局部也可以通過閉包通路局部的變量。下面舉倆個小例子:
// 函數作為傳回值,在定義的地方向上找變量,而不是再調用的地方
function test1 () {
const a = 1;
return function () {
console.log('a', a); //輸出:a 1
}
}
const a1 = test1();
const a = 2;
a1();
// 函數作為參數
function test2 (fn) {
const b = 6;
fn();
}
const b = 7;
function fn () {
console.log('b', b); //輸出:b 7
}
test2(fn);
為什麼要使用閉包: 因為正常情況下,函數執行完存儲在記憶體的變量會被銷毀,再次調用該函數時重新計算。假設這函數計算量大且耗時,如果每次調用都重新計算,那影響使用者體驗,這時就需要道閉包。他可以不釋放外部的引用,将第一次計算的結果緩存到本地友善下次調用直接拿取。
閉包的弊端:由于閉包的變量被儲存在記憶體中,是以記憶體消耗很大,容易導緻記憶體洩漏;容易改變父函數内部變量的值。
變量類型,深淺拷貝
變量類型有:原始類型(let ,string , number , boolean , symbol),和引用類型(object , array , null ,function )。原始類型的存儲改變發生在桟中執行,使用具體數值直接存儲;引用類型則在堆中,且存儲的資料是位址而非具體數值。
// 原始類型
let a = 10;
let b = a;
a = 20;
console.log('b:', b); //輸出b: 10
// 引用類型
let c = { age: 20 };
let d = c;
c.age = 21;
console.log('d:', d); //輸出d: { age: 21 }
如在下面的例子中,原始類型的變量有私有記憶體各自存儲數值,一方的改變不會牽動自身;引用類型d指向c的存儲位址,c值改變後,d用的依舊是c的位址是以輸出21。
上面其實引出了深拷貝和淺拷貝的概念,淺拷貝:複制的同時一方變另一放跟着變(修改的是堆記憶體中的同一個值),深拷貝,完全複制了一個對象,一方的改變不會牽動另一方(修改堆的不同的值)。
如何實作淺拷貝?
如果是數組,可以使用
slice,concat,Array.from , 展開運算符
,來傳回一個新數組的特性實作拷貝。但是如果數組嵌套了其他引用類型,
concat
方法将複制不完整。
深拷貝才是重點:
JSON大法不僅适用于數組還适用于對象(非常好用):
原理是JOSN對象中的stringify可以把一個js對象序列化為一個JSON字元串,parse可以把JSON字元串反序列化為一個js對象,通過這兩個方法,也可以實作對象的深複制,但是這個方法不能夠拷貝函數 。
手寫一個深拷貝(鞏固了解):
function deepClone (obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
//判斷ojb子元素是否為對象,如果是,遞歸複制,如果不是,簡單複制
objClone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
}
return objClone;
}
let a = [1, 2, 3, 4],
b = deepClone(a);
a[0] = 2;
console.log(a, b);
深拷貝是拷貝對象各個層級的屬性,看對象的屬性是否是對象或數組類型,進行相應屬性的一一複制。
原型和原型鍊
原型:
每一個JavaScript對象在建立的時候就會預制管理另一個對象,這個對象就是我們所說的原型,每一個對象都會從原型繼承屬性。可以這麼了解:你是你爸創造出來的執行個體對象,你爸就是你的原型,你以後要找什麼方法,會通過原型鍊找到你爸,懂了吧。
那原型在實際中有什麼用呢?簡單來說就是給原型添加方法,好給後面調用。
原型鍊怎麼找?你爸創造你時,就給了你一個法寶(
__proto__
),你隻要通過這個法寶喊一聲“ 爸” ,他就會理你。當然你還有爺爺,也能通過這個找到你爺爺,這就看你爸的(
prototype
)給不給了,哈哈。
原型鍊:
每個對象都有屬于自己的隐式原型
__proto__
,每個函數或方法都有自己的顯示原型
prototype
。執行個體對象繼承使用原型方法的時候通過隐式原型向上查找,如果在原型的顯示原型中找到,就不用在通過原型的隐式原型再向上找。
這麼說有點拗口,其實蠻好了解的。下面通過一張圖說明:
如圖所示,執行個體 teacher 要想調用 teach 方法,首先通過顯示原型查找有沒有 teach 這個方法,發現沒有就通過
_proto_
隐式原型查找他的上一級原型(Teacher),看他有沒有。Teacher同樣先看自己的顯示原型,發現有就傳回。同理,如果要找 drink 方法,則還要通過
_proto_
再向上找原型(Person)看他有沒有,找到就層層傳回。
如果找到盡頭都沒有那個方法,就回傳回null。這樣,一條完整的方法查找路徑就是原型鍊。