- 作者:陳大魚頭
- github: KRISACHAN
雖然 2020 的今天,各種前端框架、工具林立,而這些框架跟工具也幫我們提前解決了不少麻煩的問題,但是工具始終是工具,紮實的基本功纔是最核心的,現在一起來通過幾個實際的代碼片段來提高我們原生 JS 的編碼水平。
判斷數據類型
首先來提問一個:typeof
是否能正確判斷類型?
答案是:不可以,因爲由於歷史原因,在判斷原始類型時,typeof null
會等於object
。而且對於對象來說,除了函數,都會轉換成object
。例子如下:
typeof 1 // 'number'
typeof "1" // 'string'
typeof null //
typeof [] // 'object'
typeof {} // 'object'
typeof window.alert // 'function'
再來提問一個,instanceof
是否能正確判斷類型?
答案是:還是不可以,雖然instanceof
是通過原型鏈來判斷的,但是對於對象來說,Array
也會被轉換成Object
,而且也不能區分基本類型string
和boolean
。例如:
function Func() {}
const func = new Func()
console.log(func instanceof Func) // true
const obj = {}
const arr = []
obj instanceof Object // true
arr instanceof Object // true
arr instanceof Array // true
const str = 'abc'
const str2 = new String('abc')
str instanceof String // false
str2 instanceof String // true
所以該怎麼辦呢?
這時候我們可以使用:Object.prototype.toString.call()
所以爲什麼?
因爲每個對象都有一個toString()
方法,當要將對象表示爲文本值或以預期字符串的方式引用對象時,會自動調用該方法。默認情況下,從Object
派生的每個對象都會繼承toString()
方法。如果此方法未在自定義對象中被覆蓋,則toString()
返回[Object type]
,其中type
是對象類型。所以就有以下例子:
Object.prototype.toString.call(new Date()) // [object Date]
Object.prototype.toString.call('1') // [object String]
Object.prototype.toString.call(1) // [object Numer]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]
所以綜合上述知識點,我們可以封裝出以下通用類型判斷方法:
var type = function(data) {
var toString = Object.prototype.toString;
var dataType = data instanceof Element
? 'element' // 爲了統一DOM節點類型輸出
: toString
.call(data)
.replace(/\[object\s(.+)\]/, ''$1')
.toLowerCase()
return dataType
};
使用方法如下:
type('a') // string
type(1) // number
type(window) // window
type(document.querySelector('h1')) // element
通用的數組/類數組對象封裝
如果我們使用 ES5/ES6+的數組 API,很容易就能夠對數組進行各類的循環操作,但是如果我們要循環一個類數組對象呢?
例如NodeList
。直接循環是會報錯的:
document.querySelectorAll('div').map(e => e) // Uncaught TypeError: document.querySelectorAll(...).map is not a function
當然我們可以用擴展運算符:
[...document.querySelectorAll('div')].map(e => e)
那如果我們不用擴展運算符呢?
那麼我們就可以利用call
的特性,將NodeList
裏的元素一個一個的插入到數組中,例子如下:
var listMap = function(array, type, fn) {
return !fn ? array : Array.prototype[type]['call'](array, fn)
}
使用方法如下:
var divs = document.querySelectorAll('div')
listMap(divs, 'forEach', function(e) {
console.log(e)
})
獲取 dom 元素節點的偏移量
如果有用過jQuery
的童鞋,就一定不會忘記$('').offset()
這個 api 的強大功能,這個 api 可以輕易獲取元素的偏移量,那麼如果我們不用jQuery
該怎麼實現呢?
我們先來看看例子:
var getOffset = function(el) {
var scrollTop =
el.getBoundingClientRect().top +
document.body.scrollTop +
document.documentElement.scrollTop
var scrollLeft =
el.getBoundingClientRect().left +
document.body.scrollLeft +
document.documentElement.scrollLeft
return {
top: scrollTop,
left: scrollLeft
}
}
首先我們先來看getBoundingClientRect()
這個方法。
getBoundingClientRect()
方法返回元素的大小及其相對於視口的位置。返回值是一個 DOMRect
對象,是與該元素相關的 CSS 邊框集合 。
然後就是document.body.scrollTop
跟 document.documentElement.scrollTop
這兩個是一個功能,只不過在不同的瀏覽器下會有一個始終爲 0,所以做了以上的兼容性處理。所以當我們做拖拽功能的時候,就可以依賴上以上屬性。
使用方法如下:
var el = document.querySelector('.moveBox')
getOffset(el) // {top: xxx, left: xxx}
我們可以看上面的搖桿效果,這裏就是利用了offset()
去做位置判斷。具體實現代碼可以看:https://codepen.io/krischan77/pen/zYxPNPy
Fade 特效
// Fade in
var fadeIn = function (el) {
el.style.opacity = 0
var last = +new Date()
var tick = function() {
el.style.opacity = +el.style.opacity + (new Date() - last) / 400
last = +new Date()
if (+el.style.opacity < 1) {
requestAnimationFrame(tick))
}
}
tick()
}
// Fade out
var fadeOut = function (el) {
el.style.opacity = 1
var last = +new Date()
var tick = function() {
el.style.opacity = +el.style.opacity - (new Date() - last) / 400
last = +new Date()
if (+el.style.opacity > 0) {
requestAnimationFrame(tick)
}
}
tick()
}
上述是淡入淡出效果的具體實現,這裏是利用requestAnimationFrame
對opacity
通過遞歸的方式進行修改。
其實這裏需要提一個概念,就是時間分片。
這是一個非常重要的概念,例如 React 的 Fiber 核心實現就是時間分片。它會將一個長任務切分成一個含有若干小任務的任務隊列,然後一個接着一個的執行。
requestAnimationFrame
就是這樣一個 API,它可以根據系統來決定回調函數的執行時機,其實也就是在下一次重繪之前更新動畫幀,因爲有這樣的機制,所以能防止丟幀。
利用隊列的概念進行數據操作
隊列(queue),是先進先出(FIFO, First-In-First-Out)的線性表。在具體應用中通常用鏈表或者數組來實現。隊列只允許在後端(稱爲 rear)進行插入操作,在前端(稱爲 front)進行刪除操作。
雖然很多人覺得了解數據結構對前端作用不大,但是如果我們懂一些基礎的概念,是否在編碼時能夠更加擴散我們的思維呢?我們看下面兩個例子:
獲取節點在該父節點下的座標。
如果我們要操作原生 DOM,那麼是繞不開獲取節點在該父節點的下標的這個功能的,那麼我們該如何實現呢?
當然就是利用我們的循環啦,對子元素集合進行遍歷,直到確定下標爲止,代碼如下:
var index = function(el) {
if (!el) {
return -1
}
var i = 0
while (el = el.previousElementSibling) {
i++
}
return i
}
清空子節點
如果我們要清空某個 DOM 節點的子節點,我們有以下的方法:
var empty = function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
}
利用 reduce 進行數據優化
數組去重
沒錯,又是一個老生常談的問題,數組去重,但是我們這次去除的不僅僅是單個的數據,而是擁有某個相同鍵值的對象集合。例如下面的例子,我們有以下的數據:
牛逼的 reduce
數據去重
首先我們來看看一個老生常談的問題,我們假設有這樣的一個對象:
const data = [
{
name: 'Kris',
age: '24'
},
{
name: 'Andy',
age: '25'
},
{
name: 'Kitty',
age: '25'
},
{
name: 'Andy',
age: '25'
},
{
name: 'Kitty',
age: '25'
},
{
name: 'Andy',
age: '25'
},
{
name: 'Kitty',
age: '25'
}
]
現在我們要去重裏面name
重複的對象,這時候我們可以利用reduce
,例子如下:
const dataReducer = (prev, cur, idx) => {
let obj = {};
const { name } = cur;
obj[name] = cur;
return {
...prev,
...obj
}
}
const reducedData = data.reduce(dataReducer, {})
let newData = Object.values(reducedData)
批量生成對象元素
在魚頭的實際業務中,有一個操作是需要對類似以下的對象進行操作的:
{
a1: 'data',
a2: 'data',
...,
an: 'data'
}
像我這麼懶的魚,肯定不會一個個手寫,所以就有了以下方法
const createList = (item, idx) => {
let obj = {}
obj[`a{idx}`] =
data'
return obj
}
const listReducer = (acc, cur) => (!acc ? { ...cur } : { ...cur, ...acc })
const obj = Array.from(new Array(20), createList).reduce(listReducer)
如果你喜歡探討技術,或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。
魚頭的微信號是:krisChans95
也可以掃碼添加好友,備註“csdn”就行