原稿:语雀 · 实用函数 · 原目录:手撕代码 › 实用函数

防抖&节流

⏱秒速理解防抖节流

有一盏电灯💡,受三个开关控制,对应正常(无防抖无节流)、带防抖、带节流,一个熊孩子去不停拨动开关(像你小时候一样🐻),则

  • 正常开关:他拨动得多快,电灯就能给你闪多快,欠打
  • 防抖开关:任凭他拨多快,电灯状态就是不变,最后熊孩子无聊了,松手后几秒钟,电灯状态变了一下
  • 节流开关:小孩以1s/次的速度拨动开关,但是节流限制时间是5s,那么他每拨动5下电灯状态才变一下
- 防抖装饰器

防抖

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。通俗讲就是短时间多次触发事件,只执行最后一次回调。

适用场景

  • 提交按钮:防止连续点击重复提交,只执行最后一次提交
  • 后端验证:在input框中输入时,如果每个字符都送去验证会耗费性能,如果设置为每过一定时间(如300ms),监听input事件的处理函数才被再次执行就能优化,减少不必要交互
<div>
    <input type="text" id="ipt">
</div>
<script>
let ipt = document.getElementById("ipt");
let dbFun = debounce(
  (value) => {
    console.log(value);
  },
  3000,
  false
);
ipt.addEventListener("keyup", function (event) {
  dbFun(event.target.value);
});

function debounce(func, wait, immediate) {
  let timeout = null;

  return function (...args) {
    // 如果已有定时器,则清除
    if (timeout) clearTimeout(timeout);
    // 立即执行
    if (immediate) {
      const callNow = !timeout;
      // 立即执行要求执行后wait时间内不会被触发
      timeout = setTimeout(() => {
        timeout = null;
      }, wait);
      if (callNow) func.apply(this, args);
    } else {
      // 非立即执行的条件
      timeout = setTimeout(() => {
        func.apply(this, args);
      }, wait);
    }
  };
}
</script>
  • 节流装饰器

节流

规定单位时间内只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效。

适用场景

对于可以持续触发的事件的处理,如 resize、scroll、mousemove 等等,有时并不希望在事件持续触发的过程中那么频繁地去执行函数,因为 JS 的执行会和重绘重排争夺主线程,可能导致动画掉帧

function throttle(func, wait) {
  let timeout = null;
  return function (...args) {
    // 执行func的条件:timeout为null
    if (!timeout) {
      // 在节流限制中,timeout处于计时状态
      timeout = setTimeout(() => {
        func.apply(this, args);
        // 节流限制解除,timeout处于null
        timeout = null;
      }, wait);
    }
  };
}

参考链接:


判断原型/对象属性

in 在对象上访问到指定属性时返回true,无论该属性是在实例上还是在原型上

hasOwnProperty() 只会对实例属性返回true

// 判断属性来自原型
function propertyFromProto(property, obj) {
  return property in obj && obj.hasOwnProperty(property);
}
// 判断属性来自实例
function propertyFromObj(property, obj) {
  return property in obj && ! obj.hasOwnProperty(property);
}

// 测试
function foo() {
  this.name = "foo";
  this.sayHi = function () {
    console.log("Say Hi");
  };
}

foo.prototype.sayGoodBye = function () {
  console.log("Say Good Bye");
};

let myPro = new foo();

console.log(propertyFromProto('name', myPro));      //true
console.log(propertyFromProto('sayGoodBye', myPro)); //false
console.log(propertyFromProto('toString', myPro));  //false

判断 Number 类型

因为 typeof NaN == “number”,因此需要额外采用 isNaN(num) 排除 NaN

return typeof num === "number" && !isNaN(num)

判断对象类型

该方法基于Object.prototype.toString(),通过call()调用打印出完整对象类型,常见的内置对象都重写了继承的toString(),因此要想打印类型,需要使用call()调用Object原型对象上的原始方法

// 对象类型判断
const isType = (obj, type) => {
  if (typeof obj !== "object") return false;
  const typeString = Object.prototype.toString.call(obj);
  let flag;
  switch (type) {
    case "Array":
      flag = typeString === "[object Array]";
      break;
    case "Date":
      flag = typeString === "[object Date]";
      break;
    case "RegExp":
      flag = typeString === "[object RegExp]";
      break;
    default:
      flag = false;
  }
  return flag;
};

使用时if (isType(parent, “RegExp”))即可判断


深拷贝

参考链接:JavaScript中如何实现深度克隆

根据JS内存管理和运行机制的内容,JS中原始类型数据的拷贝就是在栈中多存一份数据副本,但是对象的拷贝实际上只是在栈上多存了一个指向堆中实例的引用,新旧引用实际上指向同一个对象。而深拷贝就是不仅新建一个引用,同时在堆上新开辟一块内存空间,存储一个和原始对象一样的对象,并让新引用指向该对象。 2. 深拷贝的简单版本

  • JSON.stringify()可以将JS对象序列化成JSON字符串
  • JSON.parse()可以将JSON字符串反序列化成JS对象
const newObj = JSON.parse(JSON.stringify(oldObj));

但是这个版本无法深拷贝函数、正则对象;所有新对象constructor都会是Object;循环引用会报错 2. 深拷贝的完整版本

注意正则对象处理:

function clone(parent) {
  // 存储循环引用
  let parents = [],
    children = [];

  function _clone(parent) {
    if (parent === null) return null;
    // 原始值,直接返回
    if (typeof parent != "object" || typeof parent != "function") return parent;

    let child, proto;
    // 确定具体对象类型
    let parentType = Object.prototype.toString.call(parent);
    if (parentType == "[object Array]") {
      // Array
      child = [];
    } else if (parentType == "[object RegExp]") {
      // RegExp
      child = new RegExp(parent.source, parent.flags);
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (parentType == "[object Date]") {
      // Date
      child = new Date(parent.getDate());
    } else {
      // 其他对象
      // 获取parent原型配合create()而不是获取constructor配合new,避免创建冗余属性
      proto = Object.getPrototypeOf(parent);
      child = Object.create(proto);
    }

    // 是否有循环引用
    const index = parents.indexOf(parent);
    if (index != -1) {
      return children[index];
    }
    parents.push(parent);
image.png

数组扁平化

参考链接:5种方式实现数组扁平化

  • 递归:基于 reduce()
function flatten(arr) {
  return arr.reduce((prev, curr) => {
    return prev.concat(Array.isArray(curr) ? flatten(curr) : curr);
  }, []);
}
// 测试
let numbers = [1, [2, [3, 4], 5], 4, 3, 2, 1];
console.log(flatten(numbers));
  • 递归:基于 map()
const flatten = (arr) => {
  return [].concat(...arr.map(item => (Array.isArray(item) ? flatten(item) : item)));
};
  • 非递归:基于队列,当前元素 curr 是一个数组时,将队首的数组扩展
const flatten3 = function(arr){
  let queue = [...arr];
  let ret = [];
  while(queue.length){
    let curr = queue.shift();
    if(Array.isArray(curr)){
      queue.unshift(...curr);
    }else{
      ret.push(curr);
    }
  }
  return ret;
}

数组去重

  • ES5

使用indexOf()和辅助栈,收集栈内未存在元素

function unique(arr) {
  let ret = [];
  for (let i = 0; i < arr.length; i++) {
    if (ret.indexOf(arr[i]) == -1) ret.push(arr[i]);
  }
  return ret;
}
  • ES6

利用set自带的成员不重复特性

function unique(arr) {
    return [... new Set(arr)];
    // return Array.from(new Set(arr))
}

URL 解析为JSON对象

function parseURL(url) {
  const ret = {};
  if (url.indexOf('?') != -1) {
    let query = url.split('?')[1].split('&');
    for(let item of query){
      item = item.split('=');
      ret[item[0]] = item[1];
    }
  }
  return ret;
}

// 测试
let url = "http://i.cnblogs.com/EditPosts.aspx?opt=1&date=20190812"
console.log(parseURL(url));

sleep()

  • promise实现
function sleep(delay) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
}

function foo() {
  console.log("wake up");
}

sleep(3000).then(foo);
  • 异步函数实现
async function sleep(delay) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
}

async function foo() {
  await sleep(3000);
  console.log("wake up");
}

foo();

柯里化

  • 通用柯里化

下面这种通用柯里化适合给定参数个数时使用

function curry(func) {
  // 返回一个包装器
  return function curried(...args) {
    // 参数足够,直接返回func调用
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      // 参数不够,返回偏函数
      return function(...args2){
        return curried.apply(this, args.concat(args2));
      }
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

let curried_sum = curry(sum);
console.log(curried_sum(1)(2)(3));
  • sum(1)(2)(3).valueOf()

这个面试题需要修改下通用实现,因为 sum 的参数个数此时不确定,且要求使用 valueOf() 输出。实际上更简单了,只要不调用 valueOf() ,都返回一个包装函数

function sum(...args) {
  return args.reduce((a, b) => a + b);
}

function curry(func) {
  let sum = 0;
  const curried = function (...args) {
    sum += func.apply(this, args);
    return curried;
  };

  curried.valueOf = function () {
    console.log(sum);
    // 打印完成后清空sum
    sum = 0;
  };
  return curried;
}

发布订阅模式

参考链接:JS的发布订阅模式

观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知

class EventEmitter {
  constructor() {
    this.handler = [];
  }

  on(type, handler) {
    if (!(type in this.handler)) this.handler[type] = [];
    this.handler[type].push(handler);
  }

  emit(type, ...args) {
    if (!(type in this.handler)) throw new Error("invalid event type");
    this.handler[type].forEach((handler) => {
      handler(...args);
    });
  }

  off(type, handler) {
    if (!(type in this.handler)) throw new Error("invalid event type");
    if (!handler) delete this.handler[type];
    else {
      let index = this.handler[type].indexOf(handler);
      if (index == -1) throw new Error("invalid handler");
      this.handler[type].splice(index, 1);
      if (!this.handler[type].length) delete this.handler[type];
    }
  }

  once(type, handler) {
    const wrap = () => {
      handler(...arguments);
      this.off(type, wrap);
    };
    this.on(type, wrap);
  }
}

// 测试用例
//  触发的事件(订阅者的处理程序)
let logmsg1 = function (content) {
  console.log("logging1: " + content);
};

let logmsg2 = function (content) {
  console.log("logging2: " + content);
};

let eventEmitter = new EventEmitter();
let sub1 = eventEmitter.on("click", logmsg1);
let sub2 = eventEmitter.once("mouseover", logmsg2);
eventEmitter.emit("click", "It's a fake click");
// logging1: It's a fake click
eventEmitter.emit("click", "It's a fake click");
// logging1: It's a fake click
eventEmitter.emit("mouseover", "It's a fake mouseover,too");
// logging2: mouseover
eventEmitter.emit("mouseover", "It's a fake mouseover,too");
// Uncaught Error: invalid event type

eventBus

模拟 Vue 场景下的 eventBus

// eventBus.js
import Vue from 'vue';
export default new Vue();

// A.vue
<script>
import Bus from 'eventBus.js';
export default {
    methods: {
        sayHello() {
            Bus.$emit('sayHello', 'hello');
        },
        remove()    {
            Bus.$off('sayHello');
        }
    }
}
</script>

// B.vue
<script>
import Bus from 'eventBus.js';
export default {
    created() {
        Bus.$on('sayHello', target => {
            console.log(target);  // => 'hello'
        });
    }
}
</script>

原生 ajax

参考链接:

无论 get 或 post,实现原生 ajax 请求需要 2. 创建 XMLHttpRequest 实例 4. 调用 open(),初始化请求,可选的参数3表示开启异步 6. 调用 setRequestHeader(),设置请求头,通常在 POST 时使用 8. 添加对于 readyState 事件的监听。readyState 表示请求的状态,描述当前 xhr 对象的进展;status 表示响应的状态

0

UNSENT

代理被创建,但尚未调用 open() 方法

1

OPENED

open() 方法已经被调用

2

HEADERS_RECEIVED

send() 方法已经被调用,并且头部和状态已经可获得

3

LOADING

responseText 属性已经包含部分数据

4

DONE

下载操作已完成

2. 调用 send() 发送请求,开启异步后,该方法会立刻返回

下面是使用 Promise 封装后的 ajax 请求

class Ajax {
  get(url) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open("get", url);
      xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
          if (xhr.status >= 200 && xhr.status < 400) {
            resolve(xhr.responseText);
          } else {
            reject(xhr.responseText);
          }
        }
      };
      xhr.send();
    });
  }

  post(url, data) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open("post", url);
      xhr.setRequestHeader("Content-Type", "application/json");
      xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
          if (xhr.status >= 200 && xhr.status < 400) {
            resolve(xhr.responseText);
          } else {
            reject(xhr.responseText);
          }
        }
      };
      xhr.send(data);
    });
  }
}

固定区间随机数

//[m,n]
const random1 = function (min, max) {
  return Math.round(Math.random() * (max - min)) + min;
};
//[m,n)
const random2 = function (min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
};
//(m,n]
const random3 = function (min, max) {
  let rand = Math.random();
  if (Math.round(rand * (max - min)) == 0) return min + 1;
  else return Math.round(rand * (max - min)) + min;
};
//(m,n)
const random4 = function (min, max) {
  let rand = Math.random();
  if (Math.round(rand * (max - min)) == 0) return min + 1;
  if (Math.round(rand * (max - min)) == max-min) return max - 1;
  return Math.round(rand * (max - min)) + min;
};

JSONP

const jsonp = function (url, params, callback) {
    // 判断是否已有参数
    let queryString = url.indexOf("?") ? "?" : "&";
    // 拼接参数
    for (let item in params) {
      if (params.hasOwnProperty(item)) {
        queryString += `${item}=${params[item]}&`;
      }
    }

    // 为cb拼接随机token(可省略)
    let token = Math.random().toString().replace(".", "");
    let cbName = "jsonpCb" + token;
    queryString += `callback=${cbName}`;

    // 添加标签
    let script = document.createElement("script");
    script.src = url + queryString;

    // 包装回调执行逻辑
    window[cbName] = function (...args) {
      callback.apply(this, args);
      document.removeChild(script);
    };
    document.appendChild(script);
  };

获取所有 DOM 节点

const traveseDOM = function (parent) {
  ret.push(parent);
  if (parent.children) {
    let children = [...parent.children];
    for (let item of children) {
      traveseDOM(item);
    }
  }
};

let ret = [];
const html = document.documentElement;
traveseDOM(html);
console.log(ret);

随机生成 Hex 颜色

function hex() {
  // Math.random() * 0xffffff 生成一个[0,0xffffff)间的十进制整数
  // >> 0 位移0可以抛弃小数部分
  // toString(16) 重新将其转为16进制
  return "#" + ((Math.random() * 0xffffff) >> 0).toString(16);
}