
英文 | https://blog.bitsrc.io/20-javascript-tips-front-end-engineers-need-to-know-b5626900a22
1.确定對象的資料類型
function myType(type) {
return Object.prototype.toString.call(type).slice(8, -1);
使用Object.prototype.toString,通過傳入不同類型的判斷傳回不同的判斷函數,一行代碼,簡潔優雅靈活;
2.循環周遊數組map方法
const myMap = function (fn, context) {
let arr = Array.prototype.slice.call(this);
let resultArr = Array();
for (let i = 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue;
resultArr[i] = fn.call(context, arr[i], i, this);
}
return resultArr;
};
Array.prototype.myMap = myMap;
let arr = [1, 2, 3];
console.log(arr.myMap((item) => item + 1)); // 2,3,4
值得注意的是,map第二個參數在第一個參數回調中指向this。如果第一個參數是箭頭函數,則第二個 this 的設定無效。
3.循環周遊數組過濾方法
const myFilter = function (fn, context) {
let arr = Array.prototype.slice.call(this)
let resultArr = []
for (let i = 0; i < arr.length; i++) {
if(!arr.hasOwnProperty(i)) continue;
fn.call(context, arr[i], i, this) && resultArr.push(arr[i])
}
return resultArr
}
Array.prototype.myFilter = myFilter
let arr = [1, 2, 3]
console.log(arr.myFilter(item => item === 2)) // [2]
4.使用reduce實作數組過濾方法
const myFilter2 = function (fn, context) {
return this.reduce((total, current, index) => {
return fn.call(context, current, index, this) ? [...total, current] : [...total]
}, [])
}
5.周遊數組的一些方法
const mySome = function (fn, context) {
let arr = Array.prototype.slice.call(this);
// The empty array returns false directly, and the every method of the array returns true conversely
if (!arr.length) return false;
for (let i = 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue;
let res = fn.call(context, arr[i], i, this);
if (res) return true;
}
return false;
};
Array.prototype.mySome = mySome;
let arr = [1, 2, 3];
console.log(arr.mySome((item) => item === 2));
執行 some 的數組如果是空數組總是傳回 false,而另一個數組的 every 方法中的數組如果是空數組總是傳回 true。
6.通過循環實作數組的reduce方法
Array.prototype.myReduce = function (fn, initialValue) {
let arr = Array.prototype.slice.call(this)
let startItem
let startIndex
if (initialValue === undefined) {
// Finds the element and subscript of the first non-empty (real) unit
for (let i = 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue
startIndex = i
startItem = arr[i]
break
}
} else {
startItem = initialValue
}
// The starting point for traversal is the real element after the real element found in the previous step
// Each iteration skips the elements of the empty cell
for (let i = ++startIndex || 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue
startItem = fn.call(null, startItem, arr[i], i, this)
}
return startItem
}
Array.prototype.myReduce = myReduce
let arr = [1, 2, 3]
console.log(arr.myReduce((acc, cur) => acc + cur)) // 6
console.log(arr.reduce((acc, cur) => acc + cur)) // 6
7.使用reduce實作array的flat方法
// reduce implements array.prototype.flat, Array flat
const myFlat = function (depth = 1) {
let arr = Array.prototype.slice.call(this)
if (depth === 0) return arr
return arr.reduce((total, current) => {
if (Array.isArray(current)) {
// You need to bind this with call, otherwise it points to the window
return [...total, ...myFlat.call(current, depth-1)]
} else {
return [...total, current]
}
}, [])
}
Array.prototype.myFlat = myFlat
let arr = [1, 2, [3, 4, [5, 6,['a','b','c',['d']], 7, 8], 9], 10, 11, 12, [13, 14]]
console.log(arr.myFlat())
因為myFlat依賴這個指向,是以需要在reduce周遊的時候指定myFlat的這個指向;否則預設指向window,會報錯。
當數組的元素還是數組時,使用ES6的擴充運算符對其進行降維(ES5中可以使用concat方法)。但是數組元素内部可能有嵌套數組,是以需要遞歸調用selfFlat。
同時,原生的 Flat 方法支援一個深度參數來表示降維的深度。預設值為1,表示數組減少一維。
傳遞 Infinity 将傳遞的數組變成一維數組:
8. 實作 ES6 類文法
function Animal(name) {
this.name = name
}
Animal.staticFunc = function () {
console.log('staticFunc')
}
Animal.prototype.sleep = function () {
console.log('animal is sleeping')
}
//Parasitic combinatorial inheritance + inheritance between constructors
function Dog(name, color) {
Animal.call(this, name)
this.color = color
}
function inherit(subType, superType) {
//Due to the nature of JavaScript reference types and functions passing by value, you cannot change the reference address of subType
subType.prototype = Object.create(superType.prototype, {
constructor: {
enumerable: false,
configurable: true,
writable: true,
// Points to subclasses, consistent with the default inheritance behavior
value: subType
}
})
//The child constructor inherits the parent constructor (the child inherits the static methods and static properties of the parent class)
Object.setPrototypeOf(subType, superType)
}
inherit(Dog, Animal)
//You need to add the prototype method to Dog after inheritance, otherwise it will be overwritten
Dog.prototype.barking = function () {
console.log('wang!')
}
let brownTeddy = new Dog('teddy', 'brown')
Dog.staticFunc()
console.log(brownTeddy)
brownTeddy.sleep()
brownTeddy.barking()
Create 方法建立一個空 Object,并從 Object.create 方法的參數中繼承這個空 Object。然後讓子類的原型(subType)等于空對象,就可以實作子類的原型等于空對象,空對象等于父類的繼承原型。
Object.create 支援第二個參數,它為生成的空對象定義屬性和屬性/通路器描述符。我們可以給這個空對象一個更符合預設繼承行為的構造函數屬性。它也是一個不能枚舉的内部屬性(Enumerable: False)。
ES6 類允許子類從父類繼承靜态方法和靜态屬性,而普通的寄生組合繼承隻能在執行個體之間實作。對于類到類的繼承,需要定義額外的方法。
這裡我們使用 Object.setProtoTypeof 将 superType 設定為 subType 的原型,進而能夠從父類繼承靜态方法和靜态屬性。
9. 函數的焦化
const display = (a, b, c, d, e, f) => [a, b, c, d, e, f];
/**
* @description Currization of a function (How many times a currization function needs to be executed according to the number of parameters of the function before currization)
* @param {function} fn -The Currified function
*/
function curry(fn) {
if (fn.length <= 1) return fn;
const generator = (...args) => {
if (fn.length === args.length) {
//Executes fn and returns the execution result
return fn(...args)
} else {
return (...args2) => {
//Return generator function
return generator(...args, ...args2)
}
}
}
return generator
}
const curriedDisplay = curry(display);
console.log("curriedDisplay", curriedDisplay(1)(2)(3)(4)(5)(6));
Currization 是函數式程式設計中的一項重要技術,該技術将一個接受多個參數的函數轉換為一系列接受一個參數的函數。
函數式程式設計 compose 另一個重要的功能,并且要能夠進行函數組合,函數的組合隻接受一個參數,是以如果你必須接受多個函數的需求并且需要使用 compose 函數組合,就需要使用 compose 的部分 curry 準備複合函數,讓它總是隻接受一個參數。
10. 函數修正(占位符支援)
const curry3 = (fn, placeholder = "_") => {
curry3.placeholder = placeholder
if (fn.length <= 1) return fn;
let argsList = []
const generator = (...args) => {
let currentPlaceholderIndex = -1
args.forEach(arg => {
let placeholderIndex = argsList.findIndex(item => item === curry3.placeholder)
if (placeholderIndex < 0) {
currentPlaceholderIndex = argsList.push(arg) - 1
// (1,'_')('_',2)
} else if (placeholderIndex !== currentPlaceholderIndex) {
argsList[placeholderIndex] = arg
} else {
argsList.push(arg)
}
})
let realArgsList = argsList.filter(arg => arg !== curry3.placeholder)
if (realArgsList.length >= fn.length) {
return fn(...argsList)
} else {
return generator
}
}
return generator
}
const curriedDisplay3 = curry3(display);
console.log("curriedDisplay3", curriedDisplay3('_', 2)(1, '_', 4)(3, '_',)('_', 5)(6)(7, 8))
如果目前輪參數包含占位符,則将其放置在内部儲存數組的末尾。目前輪的元素不填充目前輪參數的占位符,而隻填充之前傳入的占位符。
11. 斐波那契數列及其優化
const speed = function (fn, num) {
console.time('time')
let value = fn(num)
console.timeEnd('time')
console.log(`result:${value}`)
}
/**
* @description Fibonacci numbers
* @param {number} n -Number of positions
* @return {number} The argument corresponds to a number in a sequence
**/
let fibonacci = function (n) {
if (n < 1) throw new Error('Parameter is wrong')
if (n === 1 || n === 2) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
speed(fibonacci, 40)
//Memory function
const memory = function (fn) {
let obj = {}
return function (n) {
if (obj[n] === undefined) obj[n] = fn(n)
return obj[n]
}
}
fibonacci = memory(fibonacci)
speed(fibonacci, 40)
/**
* @description Fibonacci dynamic programming version (Optimal)
**/
function fibonacci_DP(n) {
let res = 1
if (n === 1 && n === 2) return res
n = n - 2
let cur = 1
let pre = 1
while (n) {
res = cur + pre
pre = cur
cur = res
n--
}
return res
}
speed(fibonacci_DP, 40)
使用函數記憶體,可以為經常依賴先前結果的計算節省大量時間,例如斐波那契數列。缺點是閉包中的 obj 對象占用了額外的記憶體。
另外,動态規劃的空間複雜度比前者低,也是比較推薦的方案。
12.實作綁定方法
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
// Implement a simple bind
const myBind = function (bindTarget, ...args1) {
if (typeof this !== 'function') throw new TypeError('Bind must be called on a function')
const originFunc = this
const boundFunc = function (...args2) {
// Calls using the new keyword return a new object
if (new.target) {
let res = originFunc.call(this, ...args1, ...args2)
//If the constructor returns an object, that object is returned
if (isComplexDataType(res)) return res
//Otherwise, the newly created object is returned
return this
} else {
return originFunc.call(bindTarget, ...args1, ...args2)
}
}
if (originFunc.prototype) {
boundFunc.prototype = originFunc.prototype
}
const desc = Object.getOwnPropertyDescriptors(originFunc)
Object.defineProperties(boundFunc, {
length: desc.length,
name: Object.assign(desc.name, {
value: `bound ${desc.name.value}`
})
})
return boundFunc
}
實作函數的bind方法的核心使用調用綁定指向this,同時考慮到其他情況如:
- 當bind傳回的函數作為構造函數被new調用時,綁定值失效,變為new指定的對象。
- 定義綁定函數的length和name屬性(不可枚舉的屬性)。
- 綁定函數的prototype必須指向prototype原函數的 。
13.實作調用方法
const myCall = function (context, ...args) {
let func = this
context || (context = window)
if (typeof func !== 'function') throw new TypeError('this is not function')
let caller = Symbol('caller')
context[caller] = func
let res = context[caller](...args)
delete context[caller]
return res
}
原理是将函數作為context傳入參數的屬性執行。ES6 Symbol 類型用于防止屬性沖突。
14.簡單的CO子產品
//Self-executing generator functions
const data = "{a:1,b:2}";
const data2 = "{c:3,d:4}";
const data3 = "{e:5,f:6}";
const api = function (data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, 1000);
});
};
function* func() {
let res = yield api(data);
console.log(res);
let res2 = yield api(data2);
console.log(res2);
let res3 = yield api(data3);
console.log(res3);
console.log(res, res2, res3);
}
function makePromisify(source) {
if (source.then && typeof source.then === "function") return source;
return Promise.resolve(source);
}
function run(generatorFunc) {
let it = generatorFunc();
let result = it.next();
return new Promise((resolve, reject) => {
const next = function (result) {
if (result.done) {
return resolve(result.value);
}
result.value = makePromisify(result.value);
result.value
.then((res) => {
let result = it.next(res);
//Recursively execute the next function
next(result);
})
.catch((err) => {
reject(err);
});
};
next(result);
});
}
run(func);
run函數接受一個生成器函數,每次run函數包裹的生成器函數遇到yield關鍵字時停止,當yield後的promise成功解析時,自動調用next方法執行到下一個yield關鍵字。
最後,每次成功解析一個promise,都會解析下一個promise。
當所有的結果都解析成功後,所有解析的結果都會被列印出來,演變成今天最常用的 async/await 文法。
15.功能防抖
/**
* @description debounce
* @param {Function} func -Functions that need function stabilization
* @param {Number} time -Delay time
* @param {Options} options -Configuration items
* @return {Function} -A function that has been shaken out
**/
/**
* @typedef {Object} Options -Configuration items
* @property {Boolean} leading -Whether an extra trigger is required to start
* @property {Boolean} trailing -Whether an additional trigger is required after the end
* @property {this} context -this
**/
const debounce = (func, time = 20, options = {
leading: true,
context: null
}) => {
let timer;
const _debounce = function (...args) {
if (timer) {
clearTimeout(timer)
}
if (options.leading && !timer) {
timer = setTimeout(null, time)
func.apply(options.context, args)
}else{
timer = setTimeout(() => {
func.apply(options.context, args)
timer = null
}, time)
}
};
_debounce.cancel = function () {
clearTimeout(timer)
timer = null
};
return _debounce
};
16.函數節流
/**
* @description throttle
* @param {Function} func -Functions that require function throttling
* @param {Number} time -Delay time
* @param {Options} options -Configuration items
* @return {Function} -經過節流處理的函數
**/
/**
* @typedef {Object} Options -Configuration items
* @property {Boolean} leading -Whether an extra trigger is required to start
* @property {Boolean} trailing -Whether an additional trigger is required after the end
* @property {this} context -this
**/
const throttle = (func, time = 17, options = {
// leading 和 trailing 無法同時為 false
leading: true,
trailing: false,
context: null
}) => {
let previous = new Date(0).getTime()
let timer;
const _throttle = function (...args) {
let now = new Date().getTime();
if (!options.leading) {
if (timer) return
timer = setTimeout(() => {
timer = null
func.apply(options.context, args)
}, time)
} else if (now - previous > time) {
func.apply(options.context, args)
previous = now
} else if (options.trailing) {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(options.context, args)
}, time)
}
};
_throttle.cancel = () => {
previous = 0;
clearTimeout(timer);
timer = null
};
return _throttle
};
添加尾随選項以訓示是否在序列結束時觸發附加事件。
17. 圖檔的延遲加載
// getBoundingClientRect lazy Load
let imgList1 = [...document.querySelectorAll(".get_bounding_rect")]
let num = imgList1.length
let lazyLoad1 = (function () {
let count = 0
return function () {
let deleteIndexList = []
imgList1.forEach((img,index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
// Add the image to the remove list after loading successfully
deleteIndexList.push(index)
count++
if (count === num) {
//Unbind the Scroll event when all images are loaded
document.removeEventListener('scroll',lazyLoad1)
}
}
})
// Delete images that have been loaded
imgList1 = imgList1.filter((_,index)=>!deleteIndexList.includes(index))
}
})()
// The throttling function of throttle.js is referenced here
lazyLoad1 = proxy(lazyLoad1, 100)
document.addEventListener('scroll', lazyLoad1)
// Manually load the image once. Otherwise, the image on the first screen cannot be loaded without triggering scrolling
lazyLoad1()
// intersectionObserver lazy Load
let imgList2 = [...document.querySelectorAll(".intersection_observer")]
let lazyLoad2 = function () {
// instantiation observer
let observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
})
imgList2.forEach(img => {
observer.observe(img)
})
}
lazyLoad2()
getBoundClientRect 的實作監聽滾動事件(建議為監聽事件添加節流)。圖檔加載完成後,會從 img 标簽組成的 DOM 清單中删除。最後,加載監聽器事件後,所有圖像都需要解除綁定。
IntersectionObserver 是通過執行個體化一個intersectionObserver 并使其觀察所有IMG 标簽來實作的。
當img标簽進入檢視區域時,執行個體化時執行回調。 同時傳入一個回調,儲存執行個體來觀察所有元素的某種狀态,比如每個元素的邊界,目前元素對應的DOM節點,目前元素進入檢視區域的比例。每當一個元素進入檢視區域時,将真實圖像配置設定給目前 IMG 标簽,同時不觀察它。
18. 新關鍵字
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
const myNew = function (fn, ...rest) {
let instance = Object.create(fn.prototype)
let res = fn.call(instance, ...rest)
return isComplexDataType(res) ? res : instance
}
function Person(name, sex) {
this.name = name
this.sex = sex
}
let newPerson = new Person('tony', 'woman')
let myNewPerson = myNew(Person, 'tony1', 'man')
console.log(newPerson)
console.log(myNewPerson)
19.實作對象配置設定
"use strict"
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
const myAssign = function (target, ...source) {
if (target == null) throw new TypeError('Cannot convert undefined or null to object')
return source.reduce((acc, cur) => {
isComplexDataType(acc) || (acc = new Object(acc));
if (cur == null) return acc;
[...Object.keys(cur), ...Object.getOwnPropertySymbols(cur)].forEach(key => {
acc[key] = cur[key]
})
return acc
}, target)
}
Object.myAssign = myAssign
let target = {
a: 1,
b: 1
}
let obj1 = {
a: 2,
b: 2,
c: undefined
}
let obj2 = {
a: 3,
b: 3,
[Symbol("a")]: 3,
d: null
}
console.log(Object.myAssign(target, obj1, obj2))
console.log(Object.myAssign("abd", null, undefined))
20. 執行個體化
const myInstanceof = function (left, right) {
let proto = Object.getPrototypeOf(left)
while (true) {
if (proto == null) return false
if (proto === right.prototype) {
return true
}
proto = Object.getPrototypeOf(proto)
}
}
console.log(myInstanceof({}, Array))
結論
到這裡,我們我們終于得到它了。20 個出色的技巧,可幫助您編寫更好、更高效的代碼。閱讀前你知道多少?
如果您覺得這很有用,請務必點贊、評論和分享。
謝謝!
學習更多技能
請點選下方公衆号