20 个前端开发人员需要掌握的JavaScript 技巧
来源 | https://blog.bitsrc.io/20-javascript-tips-front-end-engineers-need-to-know-b5626900a22
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指定的对象。
定义绑定函数的长度和名称属性(不可枚举的属性)。
绑定函数的原型必须指向原函数的原型。
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
}
原理是将函数作为传入的上下文参数的属性执行。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、实现对象分配
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))
学习更多技能