Menu

js知识总结—常用功能代码实现

这里是一些常用的功能、工具类代码写法,长期完善。

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"===1010+""==="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,转载须征得作者授权。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持