这里是一些常用的功能、工具类代码写法,长期完善。
1.防抖
触发高频事件后n秒内函数只会执行一次(执行最后一次),如果n秒内高频事件再次被触发,则重新计算时间。
将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
实现方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法
缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟
//防抖debounce代码:
function debounce(fn, delay = 500) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
// 闭包返回的函数使用普通函数而非箭头函数,是为了其中this指向赋给传入的fn,否则this将指向外层
return function () {
// 每当用户输入的时候把前一个 setTimeout clear 掉
clearTimeout(timeout);
// 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
// 定时器中钩子使用箭头函数便于里面this指向外层function
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle));
2.节流
高频事件触发,但在n秒内只会执行一次(执行第一次),所以节流会稀释函数的执行频率。
使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。
实现方式:每次触发事件时,如果当前有等待执行的延时函数,则直接return。
特点:在一定时间内只存在一个定时器,执行完毕才能开启下次计时。
这里的this和arguments可以用es6箭头函数代替。
//节流throttle代码:
function throttle(fn,delay) {
let timer = null; // 通过闭包保存一个标记
return function () {
// 在函数开头判断是否已有定时,若已经开启则直接返回
if (timer) return;
// 将外部传入的函数的执行放在setTimeout中
timer = setTimeout(() => {
// 最后在setTimeout执行完毕后再把标记设置null,表示可以执行下一次循环了。
// 在一定时间内只存在一个定时器,执行完毕才能开启下次计时
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
//用时间戳实现节流
function throttle(handler, delay) {
let lastTime = Date.now();
return function () {
let nowTime = Date.now();
if (nowTime - lastTime > delay) {
handler.apply(this, arguments);
lastTime = nowTime;
}
}
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
3.实现一个once函数,传入函数只执行一次。
// 实际也是一个防抖,利用闭包原理
function once(fn){
var flag = true
return function(){
if(flag){
fn.apply(null,arguments)
flag = false
}
}
}
4.将原生ajax封装为promise
这里考察原生ajax知识点和promise用法。
var myAjax = (url,type="GET",data={})=>{
let params = (d) => {
let arr = []
for(let prop in d){
arr.push(prop + "=" + d[prop])
}
return arr.join("&")
}
return new Promise((resolve,reject)=>{
let xhr = new XMLHttpRequest()
let isGet = type.toLowerCase()=="get"
let sendData = isGet?null:data
let _url = isGet?`${url}?${params(data)}`:url
xhr.open(type,_url,true) //第三个参数为可选,true为异步,false为同步模式。默认true
xhr.send(sendData)
xhr.onreadystatechange = () =>{
if(xhr.readyState==4){
if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
resolve(JSON.parse(xhr.responseText))
}else{
reject(xhr.status+"请求失败")
}
}
}
})
}
5.js监听对象属性改变
① 在ES5中可以使用Object.difineProperty
来实现已有属性的监听。
var Book = {name:"钢铁是怎样炼成的"}
// 劫持
objProxy(Book,"name")
// 闭包封装
function objProxy(obj,key){
var val = obj[key]
Object.defineProperty(obj, key, {
set: function (newVal) {
val = newVal
console.log('你取了一个书名叫做' + newVal)
},
get: function () {
return '《' + val + '》'
}
})
}
// 测试
console.log(Book); // {}
console.log(Book.name); // "《钢铁是怎样炼成的》"
Book.name = "三国演义"; //你取了一个书名叫做三国演义
console.log(Book.name); // "《三国演义》"
② 在ES6中使用Proxy进行
var Book = {name:"钢铁是怎样炼成的"}
var proxyBook = new Proxy(Book, {
set: function (target, prop, newVal) {
target[prop] = newVal
console.log('你取了一个书名叫做' + newVal)
},
get: function (target, prop) {
return '《' + target[prop] + '》'
}
})
//测试
console.log(proxyBook); //Proxy {name: "钢铁是怎样炼成的"}
console.log(proxyBook.name); //"《钢铁是怎样炼成的》"
proxyBook.name = "水许传"; //你取了一个书名叫做水许传
proxyBook.price = 68; //你取了一个书名叫做68
console.log(proxyBook); //Proxy {name: "水浒传", price: 68}
6.用setTimeout实现setInterval
使用setInterval
在处理某些重复事件(如果定时器代码在代码再次添加到任务队列之前还没执行完成,就会导致定时器代码连续运行好几次而之间没有间隔)时会有两个问题:①某些间隔会被跳过;②多个定时器代码执行时间可能会比预期小。
如下使用setTimeout
取代setInterval
// 递归思路
function myInterval(){
//do something
setTimeout(myInterval,200)
}
setTimeout(myInterval,200)
// 或者
// arguments.callee代表当前函数,多用于递归调用
setTimeout(function(){
//do something
setTimeout(arguments.callee,200)
},200)
7.sleep实现
循环阻塞:传统思路实现sleep效果,需要将程序阻塞。而阻塞一般使用while方式。这种方式性能较差,且容易造成死循环。
function sleep(delay){
var nextTime = Date.now() + delay
while(Date.now() < nextTime);
return true
}
// 测试
sleep(5000);
console.log(1)
async/await方法:这种方法需要与promise和定时器搭配。但这种方法必须封装到async函数中,若将async放到上下文中,同样是异步执行。
function sleep(delay){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(true)
},delay)
})
}
(async function(){
await sleep(5000)
console.log(2)
})()
8.写一个函数,第一秒打印1,第二秒打印2
使用let块级作用域+延时器
for(let i=1;i<5;i++){
setTimeout(()=>{
console.log(i)
},1000*i)
}
使用闭包+延时器
for(var i=1;i<5;i++){
(function(i){
setTimeout(()=>{
console.log(i)
},1000*i)
})(i)
}
使用闭包+setInterval
function fn(){
var i=1
var timer = setInterval(()=>{
console.log(i)
i++
i>5 && clearInterval(timer)
},1000)
}
9.在数组原型链上实现数组去重的方法
Array.prototype.distinct = function(){
var newArr = []
for(var i=0;i<this.length;i++){
if(newArr.indexOf(this[i]) < 0){
newArr.push(this[i])
}
}
return newArr
}
// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'distinct', { enumerable: false } )
10.下划线命名与驼峰命名互转
// 下划线转驼峰
function _toHump(key){
// 去除首尾的下划线
// var _key = key.replace(/(^_*)|(_*$)/g,"")
var splitKey = key.split("_")
var HumpKey = splitKey.map((item,index)=>{
if(index>0 && item!=""){
item = item[0].toUpperCase()+item.slice(1)
}
return item
})
return HumpKey.join("")
}
// 驼峰转下划线
function hump2_(key){
var splitKey = key.split("")
var HumpKey = splitKey.map((item,index)=>{
// 判断是否为大写字母
if(index>0 && item>="A" && item <= "Z"){
item = "_" + item
}
return item
})
return HumpKey.join("")
}
11.页面加载进度实现
思路:利用document.onreadystatechange
事件判断页面加载状态。页面加载会触发两次此事件,分别为document.readyState="interactive"
(可交互:文档已被解析但资源文件、样式文件和框架未加载)和document.readyState="complete"
(文档和所有子资源已完成加载,onload
事件即将触发)。complete
状态可以将进度赋值100%。中间可以用定时器动态得改变进度得百分比。为避免进度已经100%但文档还未加载完成的情况,可以设定一个等待的值如80%时清除定时器。
!function(){
var n = 0
var timer = setInterval(function(){
if(n==100 || n==80){
clearInterval(timer)
}
n+=10
console.log(n)
},50)
document.onreadystatechange = function(){
if(document.readyState=="complete"){
n = 100
console.log(n)
}
}
}()
12.手写parseInt方法
思路:使用隐式转换。如+"10"===10
,10+""==="10"
function _parseInt(str) {
let str_type = typeof str;
// 如果类型不是 string 或 number 类型返回NaN
if (str_type !== 'string' && str_type !== 'number') {
return NaN
}
// 如果是数字转为字符串
str = str_type == 'number' ? str+"" : str
// 字符串处理
str = str.trim().split(".")[0] //取小数点前面的字符
var strArr = str.split('') //转为字符串数组
if (!strArr.length) {
// 如果为空则返回 NaN
return NaN
}
var strArrNum = strArr.map(function(str) {
return +str //隐式转换为数字
})
var num = strArrNum.reduce(function(x, y) {
return x * 10 + y //将数字数组转为整个数字
})
return num
}
_parseInt('123') //123
_parseInt('123.9') //123
_parseInt(123) //123
_parseInt(123.9) //123
_parseInt() //NaN
_parseInt("") //NaN
_parseInt("QWQW") //NaN
13.原生实现bind
Function.prototype.mybind = function (context) {
// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);
var args = Array.from(arguments).slice(1)
context.fn = this
return function() {
return context.fn(...args)
}
}
// 手写call
Function.prototype.myCall = function(context){
// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);
// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
// 函数中的this指向调用它的对象
context.fn = this
const res = context.fn(...[...arguments].slice(1))
//删除这个挂载的函数
delete context.fn
// 将执行结果返回
return res
}
14.找出数组重复项
思路1:遍历数组每一项,找出其之后的选项中是否有此项
function findRepeat(arr){
var tempArr = []
arr.forEach((item,index)=>{
if(tempArr.indexOf(item)<0 && arr.slice(index+1).indexOf(item)>=0){
tempArr.push(item)
}
})
return tempArr
}
思路2:利用sort方法,先使用sort方法将数组排序,再来判断找出重复元素
function res(arr){
var temp=[];
arr.sort().sort(function(a,b){
if(temp.indexOf(a)===-1 && a===b){
temp.push(a)
}
})
return temp;
}
思路2:利用ES6的数组filter方法,通过判断第一个重复项引索与当前引索不同找出重复项,然后再将重复项去重
Array.from(new Set(arr.filter((item,index)=>arr.indexOf(item)!==index)))
15.递归实现扁平数组->树结构
将arr1转为arr2
var arr1 = [
{id:1,pid:0},
{id:2,pid:1},
{id:3,pid:1},
{id:4,pid:3}
]
var arr2 = [
{
id:1,
pid:0,
children:[
{
id:2,
pid:1,
children:[]
},
{
id:3,
pid:1,
children:[
{
id:4,
pid:3,
children:[]
}
]
},
]
}
]
思路:递归将children作为参数进行运算
function fn(arr){
let res = arr.filter(obj=>obj.pid===0)
let doChildren = function(childArr){
childArr.forEach(item=>{
item.children = arr.filter(obj=>obj.pid===item.id)
if(item.children.length){
doChildren(item.children)
}
})
}
doChildren(res)
return res
}
// 最简方案
function formatToTree(ary, pid) {
return ary
.filter((item) =>
// 如果没有父id(第一次递归的时候)将所有父级查询出来
// 这里认为 item.parentId === 1 就是最顶层 需要根据业务调整
pid === undefined ? item.pid === 0 : item.pid === pid
)
.map((item) => {
// 通过父节点ID查询所有子节点
item.children = formatToTree(ary, item.id);
return item;
});
}
16.数组扁平化
数组扁平化是指将一个多维数组变为一维数组。
思路1递归:reduce
遍历数组每一项,若值为数组则递归遍历,否则concat
。
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
思路2:toString
将多维数组变为字符串然后再用split
分割还原为数组
arr.toString().split(',')
思路3:join
也可以将多维数组变为字符串然后再用split
分割还原为数组
arr.join().split(',')
思路3:普通递归法,这里略
思路4:扩展运算符将多维数组拆开为单个元素,然后利用concat传多个值
[].concat(1,2,3) //[1,2,3]
[].concat(...["1","2",["3","4"]]) //[1,2,3,4]
17.两个数组判断相等
两个数组是没办法直接判断是否相等的,arr1==arr2
结果必然是false
,因为数组是引用类型,内存地址不同。
若要判断数组相等与否,要考虑数组元素的顺序(数组是有序的,如果不需要考虑顺序,可以不用排序),数组元素的类型(如果是嵌套数组还要使用递归)。
这里我们不考虑数组元素为不同引用的对象问题,这里默认如果其中两个对象引用地址不同则直接为不等。
数组的扩展方法最好是放到数组原型上,方便直接继承。
Array.prototype.equals = function (arr) {
if (!arr) return false;
if (this.length !== arr.length) return false;
// 这里使用普通for循环便于配合return
for (let i = 0; i < this.length; i++){
let thisItem = this[i], arrItem = arr[i];
// 同为数组,递归判断
if (Array.isArray(thisItem) && Array.isArray(arrItem)){
if (!thisItem.equals(arrItem)) return false;
}
// 直接判断是否相等
else if (thisItem !== arrItem) return false;
}
return true
}
// 使用
let aaa = [1,2,[1,2]]
aaa.equals([1,2,[1,2]]) // true
// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'equals', { enumerable: false } )
本文固定连接:https://code.zuifengyun.com/2019/03/1676.html,转载须征得作者授权。