JavaScript 进阶必会的几个技巧
1 函数柯里化
函数柯里化的是一个为多参函数实现递归降解的方式。其实现的核心是:
- 要思考如何缓存每一次传入的参数
- 传入的参数和目标函数的入参做比较
这里通过闭包的方式缓存参数,实现如下:
const curry = (fn) => {
let params = []
const next = (...args) => {
console.log(`params: ${params}, args: ${args}`);
params = [...params, ...args]
// console.log(`params: ${params}`);
if (params.length < fn.length) {
return next
} else {
return fn.apply(fn, params)
}
}
return next
}
使用方式如下:
const sum=(a,b,c,d)=>{
// console.log(`a:${a}, b:${b}, c:${c}, d:${d}`);
return a + b + c + d
}
const fn = curry(sum)
// console.log(fn);
const res = fn(1)(2)(3)
console.log(`res: ${res}`)
👆这个问题,有必要去思考一下。其实利用函数柯里化这种思想,我们可以更好的实现函数的封装。
就比如有监听某一事件那么就会有移除该事件的操作,那么就可以利用柯里化的思想去封装代码了。
或者说一个输入 A 有唯一并且对应的输出 B,那么从更大的角度去思想这样的工程项目是更安全,独立的。也便于去维护。
2 关于数组
2.1 实现 map 方法
map() 方法根据回调函数映射一个新数组
Array.prototype.map = function(fn) {
const result=[]
for (let i = 0; i < this.length; i++) {
if(!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
result.push(fn(this[i], i, this))
}
return result
}
// 使用
const arr=[1,2,3,,5]
const mapArr = arr.map(item=> item* 2)
console.log(mapArr);
2.2 实现 filter 方法
filter() 方法返回一个数组,返回的每一项是在回调函数中执行结果 true。
Array.prototype.filter = function(fn) {
const result=[]
for (let i = 0; i < this.length; i++) {
if(!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
fn(this[i], i, this) && result.push(this[i])
}
return result
}
const arr=[1,2,3,,5]
const filterArr = arr.filter(item=> item > 2)
console.log(filterArr)
filter 和 map 的区别:filter 是映射出条件为 true 的 item,map 是映射每一个 item。
2.3 实现 reduce 方法
reduce() 方法循环迭代,回调函数的结果都会作为下一次的形参的第一个参数。
Array.prototype.reduce = function (fn, initValue) {
let result = initValue ? initValue : this[0]
for (let i = 0; i < this.length; i++) {
if (!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
result = fn(result, this[i], i, this)
}
return result
}
// 使用
const arr = [1,2,3,,5]
const reduceArr = arr.reduce((a,b)=> a * b, 2)
console.log(reduceArr)
2.4 实现 every 方法
every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
Array.prototype.every = function (fn) {
let result = true
for (let i = 0; i < this.length; i++) {
if (!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
if (!fn(this[i], i, this)) {
result = false
break
}
}
return result
}
// 使用
const arr = [1, 2, 3, 4, 5]
const everyArr = arr.every(item => item > 2)
console.log(everyArr)
2.5 实现 some 方法
some() 方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。
Array.prototype.some = function (fn) {
let result = false
for (let i = 0; i < this.length; i++) {
if (!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
if (fn(this[i], i, this)) {
result = true
break
}
}
return result
}
// 使用
const arr = [1, 2, 3, 4, 5]
const someArr = arr.some(item => item > 3)
console.log(someArr);
2.6 实现 find 方法
find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
Array.prototype.find = function (fn) {
let result = false
for (let i = 0; i < this.length; i++) {
if (!this.hasOwnProperty(i)) continue; // 处理稀疏数组的情况
if (fn(this[i], i, this)) {
result = this[i]
break
}
}
return result
}
const arr = [1, 2, 3, 4, 5]
const findArr = arr.find(item => item > 3)
console.log(findArr)
2.7 实现拉平数组
将嵌套的数组扁平化,在处理业务数据场景中是频率出现比较高的。
- 利用 ES6 语法 flat(num) 方法将数组拉平。
该方法不传参数默认只会拉平一层,如果想拉平多层嵌套的数组,需要传入一个整数,表示要拉平的层级。该返回返回一个新的数组,对原数组没有影响。
// 拉平数组
Array.prototype.flattening = function (num=1){
if(!Array.isArray(this)) return
return this.flat(num)
}
// 使用
const arr = [1,[ [2, 3], 4], 5]
const flatArr = arr.flattening(Infinity)
console.log(flatArr) // [1, 2, 3, 4, 5]
- 模拟栈实现数组拉平
该方法是模拟栈,在性能上相对最优解。
Array.prototype.flattening = function() {
if (!Array.isArray(this)) return
const stack = [...this]
console.log(`stack: ${stack}`)
const res = []
while (stack.length) {
let value = stack.shift()
Array.isArray(value) ? stack.push(...value) : res.push(value)
console.log(`while stack: ${stack}`)
}
return res
}
// 使用
const arr = [1, [[2, 3], 4], 5]
const flatArr = arr.flattening()
console.log(flatArr); // [1, 5, 4, 2, 3]
3 实现懒加载
实现图片懒加载其核心的思想就是将 img 的 src 属性先使用一张本地占位符,或者为空。然后真实的图片路径再定义一个 data-set 属性存起来,待达到一定条件的时将 data-img 的属性值赋给 src。
如下是通过scroll滚动事件监听来实现的图片懒加载,当图片都加载完毕移除事件监听,并且将移除 html 标签。
const lazyLoad = function (imgs) {
let count = 0
const deleteImgs = []
const handler = () => {
imgs.forEach((img, index) => {
const react = img.getBoundingClientRect()
if (react.top < window.innerHeight) {
img.src = datset.src
count++
deleteImgs.push(index)
if (count === imgs.length) {
document.removeEventListener('scroll', lazyLoad)
}
}
})
imgs = imgs.filter((_, index) => !deleteImgs.includes(index))
}
return handler()
}
scroll 滚动事件容易造成性能问题。那可以通过 IntersectionObserver 自动观察 img 标签是否进入可视区域。
实例化 IntersectionObserver 实例,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
当 img 标签进入可视区域时会执行实例化时的回调,同时给回调传入一个 entries 参数,保存着实例观察的所有元素的一些状态,比如每个元素的边界信息,当前元素对应的 DOM 节点,当前元素进入可视区域的比率,每当一个元素进入可视区域,将真正的图片赋值给当前 img 标签,同时解除对其的观察。
const lazyLoad = function (imgs) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.src = dataset.src
observer.unobserve(entry.target)
})
})
imgs.forEach((img, index) =>observer.observe(img))
}
如上是懒加载图片的实现方式。
值得思考的是,懒加载和惰性函数有什么不一样嘛?
答:我所理解的懒加载顾名思义就是需要了才去加载,懒加载正是惰性的一种,但惰性函数不仅仅是懒加载,它还可以包含另外一种方向。
惰性函数的另一种方向是在重写函数,每一次调用函数的时候无需在做一些条件的判断,判断条件在初始化的时候执行一次就好了,即下次在同样的条件语句不需要再次判断了,比如在事件监听上的兼容。
4 实现预加载
预加载顾名思义就是提前加载,比如提前加载图片。
let images = [...document.querySelectorAll('img')]
const loadImage = function (...imgs) {
const imagesArr = []
let count = 0
for (let i = 0; i < imgs.length; i++) {
const img = new Image()
img.onload = function () {
imgs[i].src = imagesArr[i]
count++
if (count === imagesArr.length) {
console.log('加载完毕');
}
}
}
return {
setSrc: function (...args) {
imgs.forEach(img=> img.src='///loding.png')
imagesArr = args
}
}
}
当用户需要查看时,可直接从本地缓存中取。预加载的优点在于如果一张图片过大,那么请求加载图片一定会慢,页面会出现空白的现象,用户体验感就变差了,为了提高用户体验,先提前加载图片到本地缓存,当用户一打开页面时就会看到图片。
5 实现节流 & 防抖
针对高频的触发的函数,我们一般都会思考通过节流或者防抖去实现性能上的优化。
节流实现原理是通过定时器以和时间差做判断。定时器有延迟的能力,事件一开始不会立即执行,事件结束后还会再执行一次;而时间差事件一开始就立即执行,时间结束之后也会立即停止。
结合两者的特性封装节流函数:
const throttle = (fn, waiting = 1000, options) => {
let preTime = new Date(0).getTime()
options = options || { firstTime: true, endTime: false }
let timer
let _throttle = (...args) => {
let newTime = new Date().getTime()
if (!options.firstTime) {
if (timer) {
return
}
timer = setTimeout(() => {
fn.apply(fn, args)
console.log(`!options.firstTime: ${!options.firstTime}`);
timer = null
}, waiting)
} else if (newTime - preTime > waiting) {
fn.apply(fn, args)
console.log(`newTime - preTime > waiting: ${newTime - preTime}`);
preTime = newTime
} else if (options.endTime) {
timer = setTimeout(() => {
fn.apply(fn, args)
console.log(`options.endTime: ${options.endTime}`);
timer = null
}, waiting)
}
}
_throttle.cancel = () => {
preTime = 0
clearTimeout(timer)
timer = null
}
return _throttle
}
// 使用
let print = () => {
console.log('object');
}
let throttleFun = throttle(print)
window.onresize = throttleFun
函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。
防抖实现原理是通过定时器,如果在规定时间内再次触发事件会将上次的定时器清除,即不会执行函数并重新设置一个新的定时器,直到超过规定时间自动触发定时器中的函数。
const debounce = function (fn, waiting = 500, immediate = true) {
let timer
let firstTime = immediate
let _debounce = (...args) => {
if (timer) {
clearTimeout(timer)
timer = null
}
if (firstTime) {
firstTime = false
fn.apply(fn, args)
} else {
timer = setTimeout(() => {
fn.apply(fn, args)
timer = null
}, waiting)
}
}
_debounce.cancel = () => {
clearTimeout(timer)
timer = null
}
console.log(_debounce);
return _debounce
}
let print = () => {
console.log('object');
}
window.onresize = debounce(print)
6 实现 new 关键字
6.1 new 操作中发生了什么?
当我们new一个构造器,主要有三步:
- 以构造器的prototype属性为原型,创建新对象;
- 将this(也就是上一句中的新对象)和调用参数传给构造器,执行;
- 如果构造器没有手动返回对象,则返回第一步创建的对象
// ES5构造函数
let Parent = function (name, age) {
//1.创建一个新对象,赋予this,这一步是隐性的,
// let this = {};
//2.给this指向的对象赋予构造属性
this.name = name;
this.age = age;
//3.如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性的
// return this;
};
const child = new Parent();
6.2 实现一个简单的 new 方法
// 自定义 new 函数
let new2 = function (fn, ...other) {
// 1.以构造器的prototype属性为原型,创建新对象;
const target = Object.create(fn.prototype)
// 2.将this和调用参数传给构造器执行
const res = fn.apply(target, other)
if(res && (typeof res === 'object' || typeof res === 'function')){
return res
}
// 3.如果没有手动返回对象,返回第一步的对象
return target
}
// 构造器函数
let Parent = function (name, age) {
this.name = name
this.age = age
}
Parent.prototype.sayName = function () {
console.log(this.name)
}
// 实例化 Parent 对象
const obj = new2(Parent, 'echo', 26)
obj.sayName() //'echo'
console.log(obj instanceof Parent) // true
console.log(obj.hasOwnProperty('name')) // true
console.log(obj.hasOwnProperty('age')) // true
console.log(obj.hasOwnProperty('sayName')) // false
7 实现 instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
const instanceof2 = function (left, right) {
if (!left && !right) {
return
}
const rightPrototype = right.prototype
console.log(`rightPrototype: ${rightPrototype}`);
while (left = Object.getPrototypeOf(left)) {
console.log(left);
if (left === rightPrototype) {
return true
}
}
return false
}
// 使用
// let obj = { a: 1 }
let obj = [1, 2, 3, 4,]
console.log(typeof (obj));
console.log(instanceof2(obj, ))
8 实现 call、apply、bind
8.1 手写 call
call 函数实现的原理是借用方法,关键在于隐式改变this
的指向。
Function.prototype.call2 = function (context, ...args) {
context = context ? Object(context) : window
const caller = Symbol('特殊属性caller')
context[caller] = this
const res = context[caller](...args)
delete context[caller]
return res
}
var name = '小王', age = 17
var obj = {
name: '小张',
objAge: this.age,
myFun: function () {
console.log(this.name + '年龄' + this.age)
}
}
var db = {
name: '德玛',
age: 99
}
obj.myFun.call2(db)
8.2 手写apply
function isArrayLike(o) {
return (o && // o 不是null、undefined等
typeof o === 'object' && // o 是对象
isFinite(o.length) && // o.length 是有限数值
o.length >= 0 && // o.length 为非负数
o.length === Math.floor(o.length) && // o.length 是整数
o.length < 4294967296 // o.length < 2^32
)
}
Function.prototype.apply2 = function (context, ...args) {
context = context ? Object(context) : window
const applyer = Symbol('applyer')
context[applyer] = this
let res
if (args) {
if (!Array.isArray(args) && !isArrayLike(args)) {
throw new Error('第二个参数不是数组并且不是类数组对象')
} else {
console.log(...Array.from(args));
res = context[applyer](...Array.from(args))
console.log(res);
}
} else {
res = context[applyer]()
}
delete context[applyer]
return res
}
// 使用
var name = '小王', age = 17
var obj = {
name: '小张',
objAge: this.age,
myFun: function (from, to) {
console.log(from)
console.log(this.name + '年龄' + this.age, ' 来自 ' + from + ' 去往 ' + to)
}
}
var db = {
name: '德玛',
age: 99
}
obj.myFun.apply2(db, ['成都', '上海'])
注:call() 方法的作用和 apply() 方法类似,区别就是 call() 方法接受的是参数列表,而 apply() 方法接受的是一个参数数组。
8.3 手写 bind
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现的关键思路:
- 拷贝保存原函数,新函数和原函数原型链接。
- 生成新的函数,在新函数里调用原函数。
Function.prototype.bind2 = function (context, ...other) {
const findFn = (...params) => {
context = new.target ? this : Object(context)
return this.call(context, ...other, ...params)
}
if (this.prototype) {
findFn.prototype = this.prototype
}
return findFn
}
// 使用
var name = '小王', age = 17
var obj = {
name: '小张',
objAge: this.age,
myFun: function (from, to) {
console.log(from)
console.log(this.name + '年龄' + this.age, ' 来自 ' + from + ' 去往 ' + to)
}
}
var db = {
name: '德玛',
age: 99
}
obj.myFun.bind2(db,'成都', '上海')() // 德玛年龄99 来自 成都 去往 上海
9 封装数据类型函数
const type = (function () {
const type = Object.create(null);
const typeAry = ['String', 'Number', 'Object', 'Array', 'Null', 'Undefined', 'Boolean']
for (let i = 0; i < typeAry.length; i++) {
const element = typeAry[i];
type[`is${element}`] = function (args) {
return Object.prototype.toString.call(args) === `[object ${element}]`
}
}
return type
}())
console.log(type.isArray(['1','2','3']))
console.log(type.isBoolean('1'))
10 自记忆函数
记忆化是一种构建函数的处理过程,能够记住上次计算结果
const memory = function (fn) {
const cache = {}
return function (...args) {
const key = JSON.stringify(args)
// console.log(`key: ${key}; cache: ${cache}; args: ${args}`);
if (cache[key]) {
// console.log(`cache[key]: ${cache[key]}`);
return cache[key]
}
// console.log(`cache[key]: ${cache[key]}`);
return cache[key] = fn.apply(fn, args)
}
}
// 使用
var add = function (a, b, c) {
return a + b + c
}
var memoryAdd = memory(add)
for (var i = 0; i < 5; i++) {
console.log(memoryAdd(1, 2, 3))
}
11 是否存在循环引用
function cycle(target) {
var map = new WeakMap()
function _cycle(obj) {
if (!map.has(obj)) {
map.set(obj, obj)
}
let keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const element = keys[i];
if (typeof obj[keys[i]] === 'object') {
if (map.has(obj[keys[i]])) {
obj[keys[i]] = '$'
continue
} else {
map.set(obj[keys[i]], obj[keys[i]])
}
_cycle(obj[keys[i]])
}
}
}
_cycle(target)
return target
}
12 拷贝数据
拷贝数据一直是业务开发中绕不开的技巧
- 通过深度优先思维拷贝数据(DFS)
深度优先是通过纵向的维度去思考问题,在处理过程中也考虑到对象环的问题。
解决对象环的核心思路是先存再拷贝。一开始先通过一个容器用来储存原来的对象再进行拷贝,在每一次拷贝之前去查找容器里是否已存在该对象。这样就切断了原来的对象和拷贝对象的联系。
let DFSdeepClone = (obj, visitedArr = []) => {
let _obj = {}
if (typeof obj === 'object' && obj !== null) {
let index = visitedArr.indexOf(obj)
_obj = Array.isArray(obj) ? [] : {}
if (~index) {
_obj = visitedArr[index]
} else {
visitedArr.push(obj) // 先将原来保存起来再拷贝
for (let item in obj) {
_obj[item] = DFSdeepClone(obj[item], visitedArr)
}
}
} else if (typeof obj === 'function') {
_obj = eval('(' + obj.toString() + ')') // 复制函数对象
} else {
_obj = obj
}
return _obj
}
- 通过广度优先思维拷贝数据(BFS)
广度优先是通过横向的维度去思考问题,通过创造源队列和拷贝数组队列之间的关系实现拷贝。
let BFSClone = (obj) => {
let res = Array.isArray(obj)? [] : {}
let originObj = [obj] //
let copyObj = [res] // []
let visitedCopyArr = []
while (originObj.length > 0) {
let item = originObj.shift() // obj
let _obj = copyObj.shift()
visitedCopyArr.push(item) // [obj]
if (typeof item === 'object' && item !== null) {
console.log(item);
for (let key in item) {
const val = item[key] // [1,2]
if (val.constructor === Object || val.constructor === Array) {
const index = visitedCopyArr.indexOf(val)
if (~index) {
_obj[key] = val
} else {
visitedCopyArr.push(val)
_obj[key] = val.constructor === Array ? [] : {}
originObj.push(val)
copyObj.push(_obj[key])
}
} else if (val.constructor === Function) {
_obj[key] = eval(`(${val.toString()})`)
} else {
_obj[key] = val
}
}
visitedCopyArr.push(_obj)
} else if (typeof item === 'function') {
_obj = eval(`(${item.toString()})`)
} else {
_obj = item
}
console.log(_obj);
}
return res
}
// 使用
const a = [[1, 2], { aa: 11, bb: 22 }]
console.log(BFSClone(a));