讀 javascript 忍者秘籍
本文是作者閱讀 javascript 忍者秘籍這本書(shū)過(guò)程中所記錄零散知識(shí)和學(xué)習(xí)的一部分,也相當(dāng)于收錄一下個(gè)人認(rèn)為簡(jiǎn)潔有趣的代碼吧。
我很喜歡這本書(shū),客觀地說(shuō),它能夠?qū)⑽淖趾痛a完美結(jié)合,表現(xiàn)出一種緊湊、凝練很簡(jiǎn)潔,知識(shí)多、用法巧且易懂的效果,為我在前端行走地道路上掃掉了很多盲點(diǎn),同時(shí)也開(kāi)拓了解決問(wèn)題的思路。
一、性能分析
將我們編寫(xiě)出來(lái)的代碼進(jìn)行時(shí)間性能測(cè)試,計(jì)算出執(zhí)行該業(yè)務(wù)函數(shù)所需的時(shí)間成本。另外還有 console.profile 測(cè)試,該 api 提供的信息和分析入手點(diǎn)更多。下面是一個(gè)簡(jiǎn)單測(cè)試參數(shù) fn 函數(shù)的執(zhí)行時(shí)間,執(zhí)行完后輸出執(zhí)行所消耗的時(shí)間。
function timer(fn, arg) {
console.time(fn.name + ' PT')
fn(arg)
console.timeEnd(fn.name + ' PT')
}
二、遞歸遍歷 DOM 節(jié)點(diǎn)樹(shù)
參數(shù)一 ele 是根節(jié)點(diǎn),以該節(jié)點(diǎn)為起始點(diǎn)向下遍歷所有子節(jié)點(diǎn)。參數(shù)二 fn 是個(gè)函數(shù),在遍歷節(jié)點(diǎn)樹(shù)中為每個(gè)節(jié)點(diǎn)調(diào)用該函數(shù)一次,至于函數(shù)要對(duì)該節(jié)點(diǎn)動(dòng)什么手腳,取決于你要傳入的函數(shù)。
function traverseDOM(ele, fn) {
// 處理當(dāng)前節(jié)點(diǎn)
fn(ele)
ele = ele.firstElementChild
while (ele) {
traverseDOM(ele, fn)
ele = ele.nextElementSibling
}
}
下面也是一個(gè)擁有同樣功能的函數(shù),每調(diào)用它一次都會(huì)獲得一個(gè)返回值直至無(wú)生成值產(chǎn)生,我們通常將這類(lèi)函數(shù)稱(chēng)為生成器,即調(diào)用時(shí)計(jì)算一次并返回該值,而不是一次性將整個(gè)倉(cāng)庫(kù)丟給你。我們可以通過(guò)DOMTraversal.next().value 不斷獲取下一個(gè)生成的值,直到最后一個(gè)返回值 undefined 出現(xiàn)。
該函數(shù)只接收一個(gè)參數(shù) ele,即根節(jié)點(diǎn)。從該節(jié)點(diǎn)開(kāi)始向下遍歷所有子節(jié)點(diǎn),每調(diào)用一次返回一個(gè)節(jié)點(diǎn),所以需要外部調(diào)用該生成器并接受其返回值。如果要為根節(jié)點(diǎn)下所有子節(jié)點(diǎn)調(diào)用一函數(shù),比如 fn,則需要在外部遍歷生成器的同時(shí)將生成器返回的節(jié)點(diǎn)作為遍歷體中 fn 函數(shù)的參數(shù)即可。
function* DOMTraversal(ele) {
yield ele
ele = ele.firstElementChild
while (ele) {
// yield* 將迭代控制轉(zhuǎn)移到另一個(gè) DOMTraversal 生成器上
yield* DOMTraversal(ele)
ele = ele.nextElementSibling
}
}
三、Promise
實(shí)現(xiàn)異步獲取服務(wù)器數(shù)據(jù)
異步處理 ajax 請(qǐng)求,如果該承諾成功兌現(xiàn),resolve 函數(shù)將會(huì)改變 Promise 對(duì)象的狀態(tài),并將成功的結(jié)果作為then 第一參數(shù)(onResolved 函數(shù))的參數(shù),即我們獲取請(qǐng)求數(shù)據(jù)后要如何處理這些數(shù)據(jù)的一個(gè)函數(shù)。如果失敗,通過(guò) reject 函數(shù)改變狀態(tài),同樣將失敗原因作為 catch 第一參數(shù)的參數(shù)(如果有的話),也可以在 then 的第二參數(shù)實(shí)現(xiàn)該失敗處理函數(shù)。
function getJSON(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest()
request.open('GET', url)
request.onload = () => {
try {
if (this.status === 200) {
resolve(JSON.stringify(this.response))
} else {
reject(this.status + ' ' + this.responseText)
}
} catch (e) {
reject(e.message)
}
}
request.onerror = () => {
reject(this.status + ' ' + this.responseText)
}
request.send()
})
}
getJSON('data/ninjas.json')
.then(ninjas => {
assert(ninjas !== null, 'Ninjas obtained')
})
.catch(e => {
console.log('Something wrong.')
})
// 鏈?zhǔn)秸{(diào)用 Promise,每個(gè) then 都會(huì)返回一個(gè) Promise 對(duì)象,該對(duì)象擁有上一個(gè) Promise 對(duì)象的結(jié)果和狀態(tài)
getJSON('data/ninjas.json')
.then(ninjas => getJSON([ninjas[0].missionsUrl]))
.then(missions => getJSON([missions[0].detailUrl]))
.then(mission => assert(mission !== null, 'Ninjas mission obtained'))
.catch(e => {
console.log('Something wrong.')
})
四、了解 setPrototypeOf defineProperty API
setPrototypeOf
const yoshi = { skulk: true}
const hattori = { sneak: true}
const kuma = { creep: true}
assert('skulk' in yoshi, 'Yoshi can skulk')
assert('sneak' in hattori, 'hattori can skulk')
// 將對(duì)象 hattori 設(shè)置為 yoshi 對(duì)象的原型,即改變了 yoshi 的原型指向
Object.setPrototypeOf(yoshi, hattori)
assert('sneak' in yoshi, 'yoshi can skulk')
assert(!('creep' in hattori), 'hattori can not skulk')
Object.setPrototypeOf(hattori, kuma)
assert('creep' in hattori, 'hattori can skulk')
assert('creep' in yoshi, 'yoshi alse can skulk')
defineProperty
let ninja = {}
ninja.weapon = 'kusarigama'
// 為 ninja 對(duì)象自定義 sneaky 屬性的規(guī)則,可以約束遍歷、寫(xiě)讀等操作
Object.defineProperty(ninja, 'sneaky', {
configurable: false,
enumerable: false,
value: true,
writable: true
})
assert('sneaky' in ninja, 'We can access the new property.')
for (let prop in ninja) {
assert(prop != undefined, 'An enumerated property: ' + prop)
}
// 繼承,解決 constructor 屬性被覆蓋的問(wèn)題
function Food() {}
Food.prototype.sugar = function () {}
function Melon() {}
Melon.prototype = new Food()
// 繼承鏈時(shí)不建議使用 Melon.prototype = Food.prototype
// 因?yàn)?Food 原型上發(fā)生的變化都會(huì)同步到 Melon 原型上
// 定義一個(gè)新的不可枚舉的 constructor 屬性,屬性值為 Food 對(duì)象,避免被覆蓋
// 不設(shè)置的話,constructor 指向仍為 Melon
Object.defineProperty(Melon.prototype, 'constructor', {
enumerable: false,
value: Food,
writable: true
})
let melon = new Melon()
assert(melon.constructor === Food, 'restablished!')
for (let prop in Melon.prototype) {
assert(prop === 'sugar', 'the only enumerate property is sugar!')
}
五、了解 getter 和 setter
對(duì)讀取、設(shè)置對(duì)象屬性值作出一定的約束,符合規(guī)則才能獲取或修改。
const ninjiaCollection = {
ninjas: ['Yoshi', 'Kuma', 'Hattori'],
// es6 class 中也可以做出同樣的約束,而且語(yǔ)法相同
get firstNinja() {
console.log('Getting firstNinja')
return this.ninjas[0]
},
set firstNinja(val) {
console.log('Setting firstNinja')
this.ninjas[0] = val
}
}
assert(ninjiaCollection.firstNinja === 'Yoshi', 'Yoshi is the first ninja')
ninjiaCollection.firstNinja = 'Hachi'
assert(ninjiaCollection.firstNinja === 'Hachi' && ninjiaCollection.ninjas[0] === 'Hachi', 'Now Hachi is the first ninja')
// or 用 defineProperty 定義 getter、setter
function Ninja() {
let _skillLevel = 0
Object.defineProperty(this, 'skillLevel', {
get: () => {
console.log('Getting firstNinja')
return _skillLevel
},
set: val => {
console.log('Setting firstNinja')
_skillLevel = val
}
})
}
const ninja = new Ninja()
assert(typeof ninja._skillLevel === undefined, 'Cannot access a private property')
assert(ninja.skillLevel === 0, 'The getter works fine!')
ninja.skillLevel = 10
assert(ninja.skillLevel === 10, 'The value was updated')
六、代理 Proxy
控制對(duì)另一個(gè)對(duì)象屬性的訪問(wèn)
// 類(lèi)似的,只是這次為通過(guò)代理約束。
const emperor = {name: 'Komei'}
const representative = new Proxy(emperor, {
get: (target, key) => {
console.log('Reading ' + key + ' through a proxy')
return key in emperor ? target[key] : 'Do not bother the emperor!'
},
set: (target, key, value) => {
console.log('Writing ' + key + ' through a proxy')
target[key] = value
}
})
assert(emperor.name === 'Komei', 'name is Komei')
assert(representative.name === 'Komei', 'get the name property through a proxy')
assert(emperor.nickname === undefined, 'emperor does not have a nickname')
assert(representative.nickname === 'Do not bother the emperor!', 'The proxy jumps in when we make inproper requests')
representative.nickname = 'Tenno'
assert(emperor.nickname === 'Tenno', 'emperor does not have a nickname')
assert(representative.nickname === 'Tenno', 'The nickname is alse accesible through the proxy')
實(shí)現(xiàn)數(shù)組負(fù)索引功能
Python 的列表可以通過(guò) list[-1] 訪問(wèn)最后一個(gè),list[-2] 訪問(wèn)倒數(shù)第二個(gè),甚至 list[1:] 切片技術(shù)截取從索引 1 到最后一個(gè)。這里簡(jiǎn)單實(shí)現(xiàn)了數(shù)組的負(fù)索引,至于切片效果,通過(guò) array.slice(1, array.length) 進(jìn)行獲取,大體也差不多的。
function createNegativeArrayProxy(array) {
if (!Array.isArray(array)) {
throw new TypeError('Expected an array')
}
return new Proxy(array, {
get: (target, index) => {
index = +index
return target[index < 0 ? target.length + index : index]
},
set: (target, index, val) => {
index = +index
return target[index < 0 ? target.length + index : index] = val
}
})
}
const ninjas = ['Yoshi', 'Kuma', 'Hattori']
const proxiedNinjas = createNegativeArrayProxy(ninjas)
assert(proxiedNinjas[0] === 'Yoshi', 'found')
assert(proxiedNinjas[2] === 'Hattori', 'found')
assert(proxiedNinjas[-1] === 'Hattori', 'found')
assert(proxiedNinjas[-2] === 'Kuma', 'found')
assert(proxiedNinjas[-3] === 'Yoshi', 'found')
七、了解 Map And Set
map
根據(jù)鍵獲取鍵對(duì)應(yīng)的值,這里不同于 {} 對(duì)象,Map 的鍵甚至可以是對(duì)象和特殊值,而不僅限于字符串。下面為建立一字典容器,設(shè)置鍵值對(duì),獲取鍵值,容器大小,以及刪除已有鍵值對(duì)和清空整個(gè)容器。
const ninjaIslandMap = new Map()
const ninjas1 = {name: 'Yoshi'}
const ninjas2 = {name: 'Hattori'}
const ninjas3 = {name: 'Kuma'}
ninjaIslandMap.set(ninjas1, {homeIsland: 'Honshu'})
ninjaIslandMap.set(ninjas2, {homeIsland: 'Hokkaido'})
assert(ninjaIslandMap.get(ninjas1).homeIsland === 'Honshu')
assert(ninjaIslandMap.get(ninjas2).homeIsland === 'Hokkaido')
assert(ninjaIslandMap.get(ninjas3).homeIsland === undefined)
assert(ninjaIslandMap.size === 2)
assert(ninjaIslandMap.has(ninjas1) && ninjaIslandMap.has(ninjas2))
ninjaIslandMap.delete(ninjas1)
assert(!ninjaIslandMap.has(ninjas1))
ninjaIslandMap.clear()
assert(ninjaIslandMap.size === 0)
set
集合結(jié)構(gòu),即相同項(xiàng)只能有一項(xiàng),不可共存,下面為建立集合容器,檢測(cè)數(shù)據(jù)是否存在,移除數(shù)據(jù),支持 for of 遍歷,除此之外還有,還有并集、交集、差集運(yùn)算。
const ninjas = new Set(['Yoshi', 'Hattori', 'Kuma', 'Hattori'])
assert(ninjas.has('Hattori') && ninjas.size === 3)
ninjas.add('Yagyu')
assert(ninjas.has('Yagyu'))
ninjas.remove('Hattori')
for (let ninja of ninjas) {
console.log(ninja)
}
八、正則捕獲引用,反向引用
反向引用匹配 HTML 標(biāo)簽的內(nèi)容
有時(shí)候,我們需要檢測(cè)、匹配一些前后一致的字符串,但這些字符串是未確定的,例如標(biāo)簽對(duì)。在此我們可以使用正則的捕獲引用,以第一個(gè)匹配的結(jié)果作為后一個(gè)的匹配規(guī)則,下面是利用捕獲引用獲取標(biāo)簽對(duì)內(nèi)的文本節(jié)點(diǎn)。
let html = '<b class="hello">Hello</b><i>world!</i>'
// 捕獲的引用,\1 指向第一個(gè)捕獲
const pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g
let match = pattern.exec(html)
assert(match[0] === '<b class="hello">Hello</b>')
assert(match[1] === 'b')
assert(match[2] === ' class="hello"')
// 目標(biāo)內(nèi)容
assert(match[3] === 'Hello')
// 再執(zhí)行一次,則得到類(lèi)似的結(jié)果(<i>world!</i>)
match = pattern.exec(html)
console.log(match)
駝峰字符串轉(zhuǎn)化成短橫線鏈接字符串
// 對(duì)替代字符串內(nèi)獲取捕獲,而不是使用反向引用,使用 $1、$2 等標(biāo)記捕獲序號(hào)
let test = 'fontFamily'
assert(test.replace(/([A-Z])/g, '-$1').toLowerCase() === 'font-family')
短橫線鏈接字符串轉(zhuǎn)化成駝峰字符串
function upper(all, letter) {
return letter.toUpperCase()
}
let test2 = 'border-bottom-width'
assert(test2.replace(/-(\w)/g, upper) === 'borderBottomWidth')
一種查詢(xún)字符串壓縮技術(shù)
該壓縮技術(shù)即是對(duì) URL 地址后面的參數(shù)進(jìn)行壓縮,整合冗余的參數(shù)鍵值對(duì)到單個(gè) = 號(hào)上,用 , 分隔鍵值。
function compress(source) {
const keys = {}
source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
keys[key] = (keys[key] ? keys[key] + ',' : '') + value
})
const result = []
for (let key in keys) {
result.push(key + '=' + keys[key])
}
return result.join('&')
}
assert(compress('foo=1&foo=2&blah=a&blah=b&foo=3') === 'foo=1,2,3&blah=a,b')
匹配 Unicode
根據(jù)特定的轉(zhuǎn)義字符和指定一個(gè)范圍匹配 Unicode 字符串。
let text = '\u5FCD\u8005\u30D1\u30EF\u30FC'
const matchAll = /[\w\u0080-\uFFFF_-]+/g
assert(text.match(matchAll), 'Regexp matches non-ASCII!')
九、確定 DOM 自閉合元素被正確解析
編寫(xiě) HTMl 頁(yè)面時(shí),我們有可能會(huì)疏忽掉未閉合的標(biāo)簽,或者干脆不寫(xiě),兼容性處理好的瀏覽器可能會(huì)幫我們自動(dòng)補(bǔ)全那些需要成對(duì)出現(xiàn)但未成對(duì)的標(biāo)簽,如果閉合不符合規(guī)范將會(huì)對(duì)文檔結(jié)構(gòu)的解析造成很大的影響,現(xiàn)在我們大概可以將該處理函數(shù)簡(jiǎn)化成下面這種方式,給定一個(gè)包含不需要自閉的標(biāo)簽字符串,通過(guò)正則匹配傳入的字符串來(lái)決定哪些標(biāo)簽需補(bǔ)全,哪些不用,最終返回處理后的結(jié)果。
function convert(html) {
const tags = /^(area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i
return html.replace(/(<(\w+)[^>]*?)\/>/g, (all, front, tag) => {
return tags.test(tag) ? all : front + '></' + tag + '>'
})
}
assert(convert('<a/>') === '<a></a>', 'Check anchor conversion.')
assert(convert('<hr/>') === '<hr/>', 'Check hr conversion.')
十、將元素標(biāo)簽轉(zhuǎn)為一系列 DOM 節(jié)點(diǎn)
個(gè)人覺(jué)得這是一個(gè)不實(shí)用的方法,依靠函數(shù)補(bǔ)全父節(jié)點(diǎn)的不完整性,并不是件值得贊揚(yáng)的事兒。當(dāng)然了,這里只是練練手,通過(guò)匹配傳入的字符串可以確定標(biāo)簽應(yīng)有的標(biāo)簽深度(嵌套層數(shù)),從而決定是否需要填補(bǔ)父級(jí)標(biāo)簽對(duì)使其完整。
function getNodes(htmlString, doc) {
const map = {
'<td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
'<th': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
'<tr': [2, '<table><thead>', '</thead></table>'],
'<option': [1, '<select multiple>', '</select>'],
'<optgroup': [1, '<select multiple>', '</select>'],
'<legend': [1, '<fieldset>', '</fieldset>'],
'<thead': [1, '<table>', '</table>'],
'<tbody': [1, '<table>', '</table>'],
'<tfoot': [1, '<table>', '</table>'],
'<colgroup': [1, '<table>', '</table>'],
'<caption': [1, '<table>', '</table>'],
'<col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
}
const tagName = htmlString.match(/<\w+/)
let mapEntry = tagName ? map[tagName[0]] : null
if (!mapEntry) {
mapEntry = [0, ' ', ' ']
}
let div = (doc || document).createElement('div')
div.innerHTML = mapEntry[1] + htmlString + mapEntry[2]
while (mapEntry[0]--) {
div = div.lastChild
}
return div.childNodes
}
assert(getNodes('<td>test</td><td>test2</td>').length === 2, 'Get two nodes back froum the method.')
assert(getNodes('<td>test</td>')[0].nodeName === 'TD', 'Verify that we are getting the rigth node.')
十一、獲取樣式屬性計(jì)算后的值
一個(gè)獲取元素樣式屬性計(jì)算過(guò)后的屬性值。
function fetchComputedStyle(element, property) {
const computedStyles = getComputedStyle(element)
if (computedStyles) {
// 將駝峰轉(zhuǎn)為中橫線分隔
property = property.replace(/([A-Z])/g, '-$1').toLowerCase()
return computedStyles.getPropertyValue(property)
}
}
十二、自定義事件
當(dāng)瀏覽器提供的內(nèi)置事件不能滿足于我們的需求時(shí),可以憑借 CustomEvent 構(gòu)造函數(shù)自定義我們想要的事件,第一參數(shù) eventType 是指事件的類(lèi)型(名字),第二參數(shù)是一個(gè)對(duì)象,里面囊括所要描述的信息。這里自定義了 ajax-start 和 ajax-complete 兩個(gè)事件,可輕易地從事件名判斷出事件監(jiān)聽(tīng)的時(shí)間點(diǎn)或觸發(fā)點(diǎn),而且每個(gè)事件都是完全獨(dú)立的。
function triggerEvent(target, eventType, eventDetail) {
const event = new CustomEvent(eventType, {
detail: eventDetail
})
target.dispatchEvent(event)
}
function performAjaxOperation() {
triggerEvent(document, 'ajax-start', {
url: 'my-url'
})
setTimeout(() => {
triggerEvent(document, 'ajax-complete')
}, 5000)
}
// 示例測(cè)試
const btn = document.getElementById('clickMe')
btn.addEventListener('click', () => {
performAjaxOperation()
})
document.addEventListener('ajax-start', e => {
document.getElementById('whirlyThing').style.display = 'inline-block'
assert(e.detail.url === 'my-url', 'pass in event data')
})
document.addEventListener('ajax-complete', e => {
document.getElementById('whirlyThing').style.display = 'none'
})