前端中的那些 This vs That,你知道嗎?
前端知識(shí)中有很多相近的概念或 API,相信不少人在開(kāi)發(fā)中有注意到這些相近的概念或 API,但是有時(shí)不會(huì)深入去了解異同,只要某個(gè) API 能滿(mǎn)足開(kāi)發(fā)需求即可。
本文將介紹一些相近的概念和 API,讓你能更清晰地了解它們的異同,在使用時(shí)更游刃有余。
1. cookie vs localStorage vs sessionStorage
前端開(kāi)發(fā)中,這三個(gè)本地存儲(chǔ)方案可以說(shuō)是很常見(jiàn)的,用一張圖說(shuō)明下它們的區(qū)別:
comparison-table
圖片來(lái)源:local-storage-vs-session-storage-vs-cookies[1]
圖中從存儲(chǔ)大小、是否自動(dòng)過(guò)期、服務(wù)端是否可以獲取、是否支持 HTTP 請(qǐng)求傳輸和數(shù)據(jù)持久性方面進(jìn)行對(duì)比。除了圖中幾個(gè)部分,在作用域方面,cookie 由域名和路徑?jīng)Q定,localStorage 和 sessionStorge 都是遵守同源策略。
最后再提幾個(gè)關(guān)于在使用 sessionStorage 的時(shí)偶爾會(huì)陌生的知識(shí)點(diǎn):
- sessionStorage 數(shù)據(jù)在各個(gè)直接打開(kāi)的瀏覽器頁(yè)簽中是不會(huì)同步的,這意味著你打開(kāi)了兩個(gè)同域名的網(wǎng)站,在其中一個(gè)設(shè)置了 sessionStorage 數(shù)據(jù),另一個(gè)頁(yè)面是不會(huì)同步這個(gè)數(shù)據(jù)的(而 localStorage 會(huì)),也就是說(shuō) sessionStorage 除了關(guān)閉瀏覽器時(shí)不會(huì)保留數(shù)據(jù),各個(gè)頁(yè)簽數(shù)據(jù)的同步也和 localStorage 不一樣。
- 如果你在當(dāng)前頁(yè)設(shè)置了一些 sessionStorage 數(shù)據(jù),然后通過(guò) window.open 或 <a> 標(biāo)簽打開(kāi),新頁(yè)簽會(huì)同步一份當(dāng)前頁(yè)副本,隨后兩個(gè)頁(yè)簽的 sessionStorage 又會(huì)是獨(dú)立的,不過(guò)要注意打開(kāi)新頁(yè)簽的 rel 屬性(用于指定當(dāng)前文檔與被鏈接文檔的關(guān)系)要設(shè)置為 opener。
圖片
2. querySelectorAll vs getElementsByTagName
querySelectorAll 可以根據(jù)傳入的 CSS 選擇器查找 HTML 元素,使用上比 getElementsByTagName 更靈活。
它們之間的不同點(diǎn)在于:querySelectorAll 返回的是一個(gè)靜態(tài)的 NodeList,而 getElementsByTagName 返回的是動(dòng)態(tài)的。
來(lái)看下面這個(gè)示例:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>接下來(lái)使用兩個(gè)方法獲取 li 元素類(lèi)數(shù)組,然后再動(dòng)態(tài)插入一個(gè) li,最后查看兩個(gè)類(lèi)數(shù)組的長(zhǎng)度。
const listItems = document.querySelectorAll('li');
const listItems2 = document.getElementsByTagName('li');
console.log(listItems.length, listItems2.length); // 3,3
const list = document.querySelector('ul');
const li = document.createElement('li');
li.innerHTML = '4';
list.appendChild(li);
console.log(listItems.length, listItems2.length); // 3, 4可以看到 querySelectorAll 方法獲取的類(lèi)數(shù)組長(zhǎng)度在動(dòng)態(tài)添加 li 后還是 3,而 getElementsByTagName 的為 4。
常用的獲取元素方法中g(shù)etElementsByClassName 方法、element.childNodes 和 element.children 返回的也是動(dòng)態(tài) NodeList。
3. children vs childNodes
children 和 childNodes 都可以用來(lái)獲取元素的子節(jié)點(diǎn),不同的是 children 只會(huì)獲取 HTML 元素節(jié)點(diǎn),而 childNodes 會(huì)獲取到非 HTML 元素節(jié)點(diǎn),包括文本、注釋節(jié)點(diǎn)等。
<ul>
<!-- 這里有有些內(nèi)容 -->
<li>A</li>
<li>B</li>
<li>C</li>
</ul>const parent = document.querySelector('ul');
// 輸出 HTMLCollection(3) [li, li, li]
console.log(parent.children)
// 輸出 NodeList(10) [text, comment, text, text, li, text, li, text, li, text]
console.log(parernt.childNodes)4. microtasks vs macrotasks
宏任務(wù)和微任務(wù)概念也經(jīng)常在前端中出現(xiàn),與之相關(guān)的就是事件循環(huán)機(jī)制。事件循環(huán)機(jī)制是必須掌握的,宏任務(wù)和微任務(wù)也可以了解下,實(shí)際開(kāi)發(fā)中碰到相關(guān)問(wèn)題能反應(yīng)過(guò)來(lái)是宏任務(wù)和微任務(wù)的不同即可。
宏任務(wù)包括:
- setTimeout and setInterval 的回調(diào)
- DOM 操作
- I/O 操作 (Node 中讀寫(xiě)文件)
- requestAnimationFrame
微任務(wù)包括:
- Promises 的 resolve 和 reject
- MutationObserver 回調(diào)
- Node 中的 process.nextTick
事件循環(huán)機(jī)制如下圖:
圖片
宏任務(wù)微任務(wù)執(zhí)行順序如下圖:
圖片
最后配合一個(gè)例子看下效果:
console.log('Script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('Promise')
}).then(function () {
console.log('Promise then')
})
console.log('Script end')
// 輸出順序?yàn)? Script start、Promise、Script end、Promise then、setTimeout一個(gè)更清晰的圖(源[2]):
圖片
5. setTimeout(0) vs requestAnimationFrame
setTimeout(0) 和 requestAnimationFrame 都能把代碼延遲到下一個(gè)動(dòng)畫(huà)幀運(yùn)行,它們的不同在于:
- setTimeout(0) 將代碼推到事件循環(huán)的任務(wù)隊(duì)列中,如果任務(wù)隊(duì)列中有大量任務(wù),setTimeout(0) 就不會(huì)立即執(zhí)行。
- requestAnimationFrame 會(huì)在下一次渲染前執(zhí)行,而不是在事件循環(huán)中執(zhí)行,它能自動(dòng)與顯示器刷新率同步。不過(guò),它只有在瀏覽器準(zhǔn)備好渲染新幀時(shí)才會(huì)執(zhí)行,如果標(biāo)簽頁(yè)處于非激活狀態(tài),它就不會(huì)運(yùn)行。
處理動(dòng)畫(huà)時(shí),requestAnimationFrame 更合適, 如果你要延遲執(zhí)行代碼的話(huà),可以直接使用 setTimeout(0)。
補(bǔ)充一個(gè)小點(diǎn):setTimeout 的語(yǔ)法是 setTimeout(functionRef, delay, param1, param2, /* … ,*/ paramN),除了回調(diào)函數(shù)和延遲時(shí)間,后續(xù)參數(shù)都會(huì)作為回調(diào)函數(shù)的參數(shù)。
// 1 秒后輸出 delay 1s
setTimeout(console.log, 1000, 'delay 1s')6. naturalWidth vs width
naturalWidth 是元素的自然寬度,它永遠(yuǎn)不會(huì)改變。例如,一張 100px 寬的圖片的 naturalWidth 始終是 100px,即使通過(guò) CSS 或 JavaScript 調(diào)整圖片大小后也不變。
而 width 是可以改變的,可以通過(guò) CSS 或 JavaScript 設(shè)置。
圖片
7. stopImmediatePropagation vs stopPropagation
stopImmediatePropagation() 方法與 stopPropagation() 方法一樣,可阻止事件冒泡。但是,stopImmediatePropagation() 方法會(huì)阻止元素同一事件的其他監(jiān)聽(tīng)器。
button.addEventListener('click', function () {
console.log('foo')
})
button.addEventListener('click', function (e) {
console.log('bar')
e.stopImmediatePropagation()
})
button.addEventListener('click', function () {
console.log('baz')
})上面代碼中按鈕點(diǎn)擊后只會(huì)輸出 foo and bar,baz 的事件監(jiān)聽(tīng)函數(shù)不會(huì)觸發(fā)。
8. HTML 字符實(shí)體 vs Unicode 字符
HTML 實(shí)體是特殊字符序列,用來(lái)表示可能被誤認(rèn)為是 HTML 代碼的字符,如小于號(hào) (<) 或雙引號(hào) (&)。
下面是一些常見(jiàn)的 HTML 實(shí)體:
- < 代表小于號(hào) <
- > 代表大于號(hào) >
- & 代表于符號(hào) &
- " 代表雙引號(hào) "
- ' 或 ' 代表單引號(hào) '
- 代表空格
HTML 字符實(shí)體相比 Unicode 字符會(huì)更好記些,同時(shí)瀏覽器對(duì) HTML 字符實(shí)體支持更好。
Unicode 是表示字符或符號(hào)的特定代碼,它們用于顯示標(biāo)準(zhǔn)字符集中可能沒(méi)有的字符,如非拉丁字母或特殊符號(hào)。
一些 Unicode 字符示例:
- \u00A9 表示版權(quán)符號(hào) (?)
- \u2192 表示右箭頭 (→)
- \u2615 代表咖啡杯 (?)
- \u1F60E 代表戴著墨鏡的笑臉 (??)
- \u2764 表示一顆紅心 (?)
Unicode 可以表示任何語(yǔ)言的任何字符或符號(hào),不過(guò)舊版本瀏覽器的支持性可能沒(méi)那么好。
9. script async vs script defer
當(dāng)瀏覽器碰到 script 標(biāo)簽時(shí),會(huì)執(zhí)行以下步驟:
- 暫停文檔解析
- 創(chuàng)建一個(gè)新請(qǐng)求來(lái)下載腳本
- 下載完成后執(zhí)行腳本
- 繼續(xù)解析文檔
script 標(biāo)簽會(huì)阻塞整個(gè)文檔的解析,為了提供更好的體驗(yàn),HTML5 為 script 標(biāo)簽提供了兩個(gè)屬性,它們是 async 和 defer。
<script src="/path/to/script.js" async></script>
<script src="/path/to/script.js" defer></script>這兩個(gè)屬性讓瀏覽器知道,該腳本與文檔解析可以同時(shí)進(jìn)行。
async 和 defer 的效果如上圖。
async 會(huì)在下載完成后立即執(zhí)行(下載不阻塞 HTML 解析,執(zhí)行會(huì)),所以多個(gè) script 標(biāo)簽都使用 async 屬性的話(huà),是不能保證多個(gè) script 的執(zhí)行順序,而使用 defer 的話(huà),下載完后會(huì)等待 HTML 解析完成再執(zhí)行,可以保證多個(gè) script 的執(zhí)行順序。
所以 async 一般在獨(dú)立的腳本上使用,如埋點(diǎn)腳本。
還有一點(diǎn),動(dòng)態(tài)加載的腳本 async 默認(rèn)為 true,如果你不需要,可以設(shè)置為 false:
const script = document.createElement('script');
script.src = '/path/to/script.js';
script.async = false;
document.head.appendChild(script);10. __proto__ vs prototype
__proto__ 和 prototype 的區(qū)別很簡(jiǎn)單:
- __proto__ 是對(duì)象實(shí)例的屬性
- prototype 是構(gòu)造函數(shù)的屬性
當(dāng)你使用 __proto__ 時(shí),你是正在查找對(duì)象原型鏈上的屬性和方法,而 prototype 對(duì)象定義了所有實(shí)例都將擁有的共享屬性和方法。
圖片
如上圖,Letter 函數(shù)的 prototype 屬性和其三個(gè)實(shí)例的 __proto__ 屬性都是指向 Letter 的原型鏈對(duì)象 Letter.prototype。
11.Dependencies vs devDependencies vs peerDependencies
dependencies 代表依賴(lài)項(xiàng)是項(xiàng)目中的一部分,最終會(huì)被一起打包到生產(chǎn)代碼中,當(dāng)你執(zhí)行 npm install 時(shí),你依賴(lài)的那個(gè)包的依賴(lài)也會(huì)自動(dòng)安裝,比如你項(xiàng)目使用到了 antd, npm install 時(shí) antd 的依賴(lài)項(xiàng) classnames 也會(huì)被安裝,這就是你有時(shí)候沒(méi)安裝一些庫(kù),但是也可以使用的原因。
"dependencies": {
"lodash": "^4.17.21"
}devDenpendencies 代表依賴(lài)項(xiàng)是僅在開(kāi)發(fā)過(guò)程中才需要的,代碼的最終生產(chǎn)版本并不需要這些依賴(lài)項(xiàng)。
"devDependencies": {
"jest": "^29.6.4"
}peerDependencies 代表使用這個(gè)庫(kù)時(shí)需要的依賴(lài)項(xiàng),和 dependencies 不同的是,它不會(huì)在 npm install 時(shí)被安裝,需要你顯式的在自己項(xiàng)目下安裝。各個(gè)包管理器的各個(gè)版本對(duì) peerDependencies 的處理可能都不同,有興趣的可以繼續(xù)深入了解。
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},12. isNaN vs Number.isNaN
isNaN 是一個(gè)全局函數(shù),用于判斷參數(shù)是否為 NaN,不過(guò),在判斷參數(shù)是否為 NaN 之前,它會(huì)嘗試先將參數(shù)轉(zhuǎn)換為數(shù)字。
isNaN('hello'); // true
isNaN(undefined); // true
isNaN({}); // true
isNaN([]); // false +[] === 0
isNaN(42); // false在 ES6 中引入了 Number.isNaN 函數(shù),與 isNaN 不同的是,在判斷前 Number.isNaN 不會(huì)轉(zhuǎn)換參數(shù)。
Number.isNaN('hello'); // false
Number.isNaN(undefined); // false
Number.isNaN({}); // false
Number.isNaN([]); // false
Number.isNaN(42); // false
Number.isNaN(NaN); // true一般來(lái)說(shuō),使用 Number.isNaN 比 isNaN 更準(zhǔn)確。
13. 默認(rèn)參數(shù) vs 或操作符
JavaScript 提供了兩種為函數(shù)參數(shù)設(shè)置默認(rèn)值的方法:使用默認(rèn)參數(shù)或 OR (||) 操作符,兩者在最終效果上會(huì)有一些不同。
先來(lái)看默認(rèn)參數(shù):
const sayHello = (name = 'World') => {
console.log(`Hello, ${name}!`);
};
sayHello(); // `Hello, World!`
sayHello(undefined); // `Hello, World!`
sayHello(null); // `Hello, null!`
sayHello(''); // `Hello, !`
sayHello("Phuoc Nguyen"); // `Hello, Phuoc Nguyen!`可以看到默認(rèn)參數(shù)只有為 undefined 的時(shí)候,默認(rèn)參數(shù)才會(huì)生效。不傳和傳 undefined 效果一致。
再來(lái)看或操作符:
const sayHello2 = (name) => {
const withDefaultName = name || 'World';
console.log(`Hello, ${withDefaultName}!`);
};
sayHello2(); // `Hello, World!`
sayHello2(undefined); // `Hello, World!`
sayHello2(null); // `Hello, World!`
sayHello2(''); // `Hello, World!`
sayHello2("Phuoc Nguyen"); // `Hello, Phuoc Nguyen!`可以看到參數(shù)只要是 falsy 值(undefined、null、NaN、0、""和 false),都會(huì)使用代碼中默認(rèn)參數(shù),這個(gè)就是和 ES6 默認(rèn)參數(shù)不同的地方。
14. null vs undefined
null 和 undefined 的不同點(diǎn)如下:
- undefined 表示變量已經(jīng)被聲明,但未被賦值;null 用來(lái)表示變量沒(méi)有值。
let foo;
console.log(foo); // undefined
let foo = null;
console.log(foo); // null- undefined 和 null 代表的類(lèi)型不同。
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object'除了以上兩點(diǎn)不同之外,還有兩點(diǎn)值得關(guān)注的:
- undefined 和 null 進(jìn)行比較的結(jié)果。
null == undefined; // true
null === undefined; // false- JSON.stringify 會(huì)忽略 undefined, 但是會(huì)保留 null。
JSON.stringify({
name: 'John',
address: null,
age: undefined,
});
// {"name":"John","address":null}小結(jié)
前端中還有很多相近的概念和 API,在業(yè)務(wù)開(kāi)發(fā)時(shí)可能沒(méi)時(shí)間去了解,但是有空的時(shí)候還是可以花點(diǎn)時(shí)間去掌握其中的異同,扎實(shí)自己的前端基礎(chǔ)。
參考資料
[1]local-storage-vs-session-storage-vs-cookies: https://www.loginradius.com/blog/engineering/guest-post/local-storage-vs-session-storage-vs-cookies/
[2]圖片源: https://medium.com/@francesco_rizzi/javascript-main-thread-dissected-43c85fce7e23

































