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));

13 Promise 系列

posted @ 2021-06-15 18:04  lqqgis  阅读(86)  评论(0编辑  收藏  举报