原稿:语雀 · 实用函数 · 原目录:手撕代码 › 实用函数
防抖&节流
⏱秒速理解防抖节流 有一盏电灯💡,受三个开关控制,对应正常(无防抖无节流)、带防抖、带节流,一个熊孩子去不停拨动开关(像你小时候一样🐻),则
|
防抖 | 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。通俗讲就是短时间多次触发事件,只执行最后一次回调。 |
适用场景 |
|
<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. 深拷贝的完整版本
注意正则对象处理:
- 使用 RegExp.prototype.source 和 RegExp.prototype.flags 获取原始正则的模式字符串和标志
- 此外还要拷贝 lastIndex 属性
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);

数组扁平化
参考链接: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 | 下载操作已完成 |
下面是使用 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);
}