实现防抖和节流
(1)防抖:可以使事件被触发不立即执行,n秒之后再进行处理,如果n秒内事件再次被触发,则重新计时
使用场景:一些点击提交请求上,防止重复请求
例如,当我们在搜索框中输入关键词时,输入框会不断触发 oninput 事件,如果每次输入都去请求服务器获取数据,会造成不必要的请求浪费。此时就可以使用防抖技术,将一定时间内的多次触发合并为一次操作,只请求一次服务器数据,减少了请求次数和服务器负载。
分析:
- 1.需要一个定时器
- 2.将定时器设置成指定间隔时间后执行
- 3.中途如果再次触发,则清空重新计时
js
// 输入:防抖函数fn;等待的秒数
// 输出:函数的执行
function debounce(fn,wait){
// 1.需要一个定时器
let timer = null
return function(){
let _this = this
let args = arguments
// 3.中途如果再次触发,则清空重新计时
if(timer){
clearTimeout(timer)
timer = null
}
// 2.将定时器设置成指定间隔时间后执行
timer = setTimeout(()=>{
fn.apply(_this, args)
})
}
}
js
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用防抖优化搜索框输入
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
function search() {
console.log('searching...');
// 发送请求获取搜索结果
}
searchInput.addEventListener('input', debounce(search, 500));
searchBtn.addEventListener('click', search);
(2)节流:指的是规定的一个时间,触发一次之后,如果在规定的时间内重复触发了,只有第一次生效
场景:使用在scroll函数事件的监听
例如,当我们拖动网页上的滚动条时,会不断触发 onscroll 事件,如果每次触发都去计算滚动距离,会造成浏览器性能下降。此时就可以使用节流技术,将一定时间内的多次触发限制为一次操作,只计算一次滚动距离,提高了浏览器性能和用户体验。
分析:
- 1.获取执行时间的时间点
- 2.获取当前时间点
- 3.两次重复操作的时间间隔与节流延时的关系
js
// 输入:节流函数fn;节流延时
// 输出:函数的执行
function throttle(fn, delay){
// 1.获取执行时间的时间点
let currentTime = Date.now()
return function(){
// 2.获取当前时间点
let nowTime = Date.now()
let _this = this
let args = arguments
// 3.两次重复操作的时间间隔与节流延时的关系
if(nowTime - currentTime >= delay){
currentTime = Date.now()
return fn.apply(_this, args)
}
}
}
javascript
function throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
// 使用节流优化滚动事件
window.addEventListener('scroll', throttle(function() {
console.log('scrolling...');
// 计算滚动距离
}, 500));
手写call、apply、bind
每个Function对象都存在apply()、call()、bind() 方法,其作用都是可以在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域。
三者异同:
-
- 三者都可以改变this的指向,第一个参数都是this,如果指向是null或者undefined则指向window
-
- apply的参数是数组,call是列表,而bind可以多次传入
-
- apply和call是改变this的指向之后直接运行函数,而bind则是返回绑定之后的函数
(1)手写call:
js
// 输入:上下文,执行函数的参数
// 输出:执行结果
Function.prototype.myCall = function(context){
// 1.判断执行对象是否为函数
if(typeof this !== 'function'){
console.error('this is not a function')
}
// 2.获取执行函数的参数
let args = [...arguments].slice(1)
let result = null
// 3.传入值判断,是否有值,如果没有,默认为全局,即window
if(!context){
context = window
}
// 4.执行对象挂载在上下文之上
context.fn = this
// 5.在上下文中调用执行对象并且传入执行参数
result = context.fn(...args)
// 6.将上下文复原,删除新增临时属性
delete context.fn
// 7.返回结果
return result
}
js
var name = 'waq';
var obj = {
name: 'goodLuck'
}
function sayName(){
console.log(this.name);
}
//1、改变this指向函数
Function.prototype.myCall = function (context){
// context 为可选参数,如果不传的话默认上下文是 window
context = context || window;
// 因为 call 可以传入多个参数作为调用函数的参数,所以将参数单独抽取出来
var args = [...arguments].slice(1); //第一个参数为context
// 给 context 创建一个 fn 属性,并将值设置为需要调用的函数
context.fn = this; //因为call的调用方式形如:sayName.call(obj),因此此时call方法的this指向为sayName,因此context.fn = this即为context.fn = sayName
var result = context.fn(...args);
// 删除对象上的函数,释放内存空间。返回结果
delete context.fn;
return result;
}
//2、验证
//没有改变this指向时
sayName(); //waq
//改变this指向后
sayName.myCall(obj); //goodLuck
(2)手写apply:apply 和 call 实现类似,不同的是参数处理,apply 传入的是数组。
js
// 输入:上下文,执行函数的参数
// 输出:执行结果
Function.prototype.myApply = function(context){
// 1.判断执行对象是否为函数
if(typeof this !== 'function'){
console.error('this is not a function')
}
// 2.获取执行函数的参数
let args = arguments[1]
let result = null
// 3.传入值判断,是否有值,如果没有,默认为全局,即window
if(!context){
context = window
}
// 4.执行对象挂载在上下文之上
context.fn = this
// 5.在上下文中调用执行对象并且传入执行参数
if(args){
result = context.fn(...args)
}else{
result = context.fn()
}
// 6.将上下文复原,删除新增临时属性
delete context.fn
// 7.返回结果
return result
}
js
Function.prototype.myApply = function (context){
context = context || window;
context.fn = this;
var result;
if (arguments[1]){
result = context.fn(...arguments[1]);
}else{
result = context.fn();
}
delete context.fn;
return result;
}
(3)手写bind:bind 会创建一个新函数,不会立即执行。bind 后面传入的这个参数列表可以分多次传入,call 和 apply 则必须一次性传入所有参数。
js
Function.prototype.myBind = function(context){
// 1.判断执行对象是否为函数
if(typeof this !== 'function'){
console.log('this is not a function')
}
// 2.获取参数
let args = [...arguments].slice(1)
let fn = this
return function Fn(){
// 根据调用方,确定最终返回值
return fn.apply(
// 判断this是否为Fn的实例对象
this instanceof Fn ? this : context,
args.concat(...arguments)
)
}
}
js
Function.prototype.myBind = function (context){
context = context || window;
//返回一个绑定this的函数,这里我们需要保存this,具体保存的this如同call
let self = this;
let args = [...arguments].slice(1);
//返回一个函数
return function () {
let newArgs = [...arguments];
return self.apply(context, args.concat(newArgs));
}
}
函数柯里化
柯里化:是把接受多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数、并且返回结果的新函数
不会立即求值,而是到了需要的时候再去求值
js
function curry(fn, ...args){
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args)
}
复用参数:
js
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
// 即使是相同的正则表达式,也需要重新传递一次
console.log(check(/\d+/g, 'test1')); // true
console.log(check(/\d+/g, 'testtest')); // false
console.log(check(/[a-z]+/g, 'test')); // true
// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt)
}
}
// 正则表达式通过闭包保存了起来
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
console.log(hasNumber('test1')); // true
console.log(hasNumber('testtest')); // false
console.log(hasLetter('21212')); // false
链式处理,使用promise封装一个ajax
js
function fetchData(url){
let promise = new Promise(function(resolve, reject){
let xhr = new XMLHttpRequest()
// 新建一个http请求
xhr.open('GET', url, true)
// 监听状态的改变流转
xhr.onreadystatechange = function(){
if(this.readyState === 4){
if(this.status === 200){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
}
xhr.onerror = function(){...}
xhr.responseType = 'xxx'
xhr.send()
})
return promise
}
// 调用
fetchData('xxxx').then(res=>{....})
手写浅拷贝和深拷贝
(1)浅拷贝的实现
js
// 快速方式,将obj2的属性给到obj1
Object.assign(obj1, obj2)
// 浅拷贝数组
arr.slice()
// 手写实现
function shallowCopy(object){
if(!object || (typeof object !== 'object') ){
return
}
let result = Array.isArray(object) ? [] : {}
for(let key in object){
if(object.hasOwnProperty(key)){
result[key] = object[key]
}
}
return result
}
(2)深拷贝
js
// 快速方式 或者第三方库,lodashde的cloneDeep()
JSON.parse(JSON.stringify(obj))
// 手写深拷贝
function deepCopy(object){
if(!object || (typeof object !== 'object') ){
return
}
let result = Array.isArray(object) ? [] : {}
for(let key in object){
if(object.hasOwnProperty(key)){
result[key] = typeof object[key] === 'object' ? deepCopy(object[key]) : object[key]
}
}
return result
}
数组操作题
(1)数组拍平
js
let arr = [1,[2,[3,4,5]]]
function flatten(arr){
let result = []
for(let i = 0;i<arr.length;i++){
if(Array.isArray(arr[i])){
result = result.concat(flatten(arr[i]))
}else{
result.push(arr[i])
}
}
return result
}
// 使用
flatten(arr)
(2)数组乱序输出
js
// 1.取出数组的一个元素 => 0 和 第一个随机
// 2.以此第二个元素 => 1 和 另一个随机索引进行交换
// 3.依次遍历
let arr1 = [1,2,3,4,5,6]
function disOrder(arr){
for (let i = 0; i < arr.length; i++) {
const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
}
disOrder(arr1)
(3)类数组到数组的转化
js
Array.from(a_arr)
Array.prototype.slice.call(a_arr)
Array.prototype.splice.call(a_arr,0)
Array.prototype.concat.call([],a_arr)
(4)转换类型:对象=>树
js
function arrayToTree(list, parentId) {
const arr = []
list.forEach(item => {
if (item.parentId === parentId) {
// 找到了匹配的节点
// 当前节点的id 和 当前节点的子节点的pid是相等的
const children = arrayToTree(list, item.id)
// 找到的节点的子节点
item.children = children
arr.push(item)
}
})
return arr
}
// 使用示例
const array = [
{ id: 1, name: 'A', parentId: null },
{ id: 2, name: 'B', parentId: 1 },
{ id: 3, name: 'C', parentId: 1 },
{ id: 4, name: 'D', parentId: 2 },
{ id: 5, name: 'E', parentId: 2 },
{ id: 6, name: 'F', parentId: 3 },
{ id: 7, name: 'G', parentId: null }
];
const tree = arrayToTree(array, null);
console.log(tree);
(5)数组去重
- 利用Array.from() Set是es6新增的数据结构,似于数组,但它的一大特性就是所有元素都是唯一的,没有重复的值,我们一般称为集合
Array.from()就是将一个类数组对象或者可遍历对象转换成一个真正的数组,也是ES6的新增方法
- 利用includes
- 利用map
- 利用单层for循环
- 利用Array.filter和map