前端手写题-防抖、节流,深浅拷贝等

272 阅读9分钟


防抖

  • 声明定时器
  • 返回函数
  • 判断是否有定时器,如果有,销毁定时器
  • 创建定时器,执行函数
function debounce(fn,t){
    let timer    //声明定时器
    return function(){
        let context=this   //保存上下文
        let args=arguments  //保存参数
        if(timer){
        clearTimeout(timer)}  //如果有定时器,清除定时器
        timer=setTimeout(()=>{
            fn.apply(context,args)  //执行函数
            },t)
        }
  }

节流

  • 声明定时器变量为空
  • 返回函数
  • 判断如果没有定时器,声明定时器,执行函数
  • 销毁定时器
function throttle(fn,t){
    let timer=null
    return function(){
        let context=this
        let args=arguments
        if(!timer){
            timer=setTimeout(()=>{
                fn.apply(context,args)
                timer=null
            },t)
        }
    }
}

浅拷⻉


//数组的浅拷贝
let ar=[1,2,3]
let br=ar.concat()
let cr=ar.slice()
let dr=[...ar]

console.log(br,cr,dr)

//对象的浅拷贝
const obj={
    name:"zhangsan "
}
const newobj={...obj}
const newobj2=Object.assign(obj)
console.log(newobj,newobj2)
  • 先判断要拷贝的对象,是不是对象类型,不是的话,直接返回
  • 新建一个对象或者数组 用来结果返回
  • 然后遍历对象的可枚举属性
  • 将老对象赋值给新对象
  • 返回新对象

function shallowcopy(obj) {
    // 如果不是对象,直接返回
    if (typeof obj!== 'object' || obj === null) {
        return obj;
    }
    // 判断是数组还是对象,创建相应的新对象
    let newobj = Array.isArray(obj)? [] : {};
    // 遍历对象的所有可枚举属性
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 复制属性值
            newobj[key] = obj[key];
        }
    }
    return newobj;
}

深拷贝

const obj={
    name:"zhangsan ",
    fa:{
        age:18
    }
}
方法一
const deepobj=JSON.parse(JSON.stringify(obj))
方法二
const _ = require('lodash');
const deepobj2=_.cloneDeep(obj)
输出结果
console.log(deepobj,deepobj2)

在进行深拷贝时,循环引用是一个常见的问题。所谓循环引用,就是对象之间相互引用,形成了一个闭环,例如对象 A 引用了对象 B,而对象 B 又引用了对象 A。如果不处理循环引用,深拷贝过程会陷入无限循环,最终导致栈溢出错误。

可以利用 WeakMap 来记录已经拷贝过的对象,在拷贝过程中,若遇到已经拷贝过的对象,直接返回之前拷贝的结果,从而避免循环引用

使用了,weakmap,当函数不再使用的时候,可以垃圾回收,不会导致内存泄漏
使用闭包,实现数据的私有,避免影响全局变量
function  deepClone(target){
    const map=new WeakMap() 

    function _deepClone(target){

        if(typeof target!=='object'||target===null) { return target} // 不是引用类型的,直接返回

        if(map.has(target))
        {return map.get(target)}          //判断缓存里面是否有,有的话,直接返回就行。避免循环引用的问题

        let newTarget=Array.isArray(target)?[]:{}  // 判断是否为数组
         
        map.set(target,newTarget)  // 缓存里面没有
      
        for(let key in target){   // 循环拷贝
            if(target.hasOwnProperty(key)){
                newTarget[key]=_deepClone(target[key])
            }
        }
        return newTarget  // 返回拷贝后的对象 
 }
 return _deepClone(target)
}

new

  • 创建一个新对象
  • 让新对象的对象原型指向构造函数的原型对象
  • 执行构造函数,并把构造函数的this指向新创建出来的对象
  • 返回创建后的结果;

function myNew(constructor, ...args) {
    // 创建一个新对象
    const obj = {};
    // 让新对象的原型指向构造函数的原型
    obj.__proto__ = constructor.prototype;
    // 执行构造函数,并且把 this 绑定到新对象上
    const result = constructor.apply(obj, args);
    // 若构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
    return typeof result === 'object' && result!== null? result : obj;
}

instanceof

判断 是不是当前原型链上的属性

function ins(target,type){
target=target.__proto__
type=type.prototype
while(true){
    if(target===null){
        return false
    }
    if(target===type){
        return true
    }
    target=target.__proto__
}
}

flat

  • 将多维数组转化为一维数组
  • 创建一个结果数组
  • this指向的是调用的数组,item就是数组里面的每一项
  • 对每一项进行判断,如果还是数组,递归调用函数。将结果和之前的结果拼接起来
  • 不是数组的话,直接将元素push进
  • 返回结果数组
Array.prototype.falten= function (){
    let flat=[]
    for(let item of this){
        if(Array.isArray(item)){
            flat=flat.concat(item.falten())
        }else{
            flat.push(item)
        }
    }
    return flat
}

url解析

参考:htps://juejin.cn/post/7388057629773889562

image.png 完整的 URL 信息包括以下几点:

  • 协议(protocol):采用的协议方案;
  • 登录信息(username & password):(可选项)指定用户名和密码,用来从服务器获取资源时的认证信息;
  • 服务器地址(hostname):待访问的服务器地址。可以是域名形式也可以是 IP 地址;
  • 服务器端口号(port):;(可选项)指定服务器连接网路的端口号;
  • 带层次的文件路径(pathname):指定服务器上的文件路径来定位特指的资源;
  • 查询字符串(search):(可选项)查询字符串参数;
  • 片段标识符(hash):(可选项)用来标记已获取资源中的子资源;

数组转树 哈希解法

const arr = [
  { id: 1, parentId: null, name: 'Root' },
  { id: 2, parentId: 1, name: 'Child 1' },
  { id: 3, parentId: 1, name: 'Child 2' },
  { id: 4, parentId: 2, name: 'Grandchild 1' },
]

 function arraytotree(arr){
   const map=new Map()
   for(let item of arr){     //用空间换时间 id:数组的每一项
    map.set(item.id,item)
   }
   const roots=[]  //根节点数组
   for(let item of arr){
    if(item.parentId===null){  //说明是根节点
        roots.push(item)
    }else{
        const parent=map.get(item.parentId)  //找到当前节点的父节点
        if(!parent.children){        //父节点没有children 属性,给他加一个孩子属性
            parent.children=[]
        }
        parent.children.push(item)  //把子项放入父亲的子节点中
    }
   }
}

实现函数柯里化


function curry(fn) {
  return function curried(...args) {
    // 如果传入的参数个数大于等于原函数的参数个数,直接执行
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }
    // 否则返回一个新函数,等待接收剩余参数
    return function (...args2) {
      return curried.apply(this, args.concat(args2))
    }
  }
}

// 使用示例
function add(a, b, c) {
  return a + b + c
}

const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

手写红绿灯

模拟一个红绿灯变化,红灯 1 秒,绿灯 1 秒,黄灯 1 秒,然后循环

function red() {
  console.log('red')
}

function green() {
  console.log('green')
}

function yellow() {
  console.log('yellow')
}

function light(cb, wait) {
  return new Promise((resolve) => {
    setTimeout(() => {
      cb()
      resolve()
    }, wait)
  })
}

function start() {
  return Promise.resolve()
    .then(() => {
      return light(red, 1000)
    })
    .then(() => {
      return light(green, 1000)
    })
    .then(() => {
      return light(yellow, 1000)
    })
    .finally(() => {
      return start()
    })
}

start()

正则表达式

则表达式常用字符可分为元字符、量词、边界符、字符类、分组与引用以及选择符

元字符

  • .:匹配除换行符外的任意单个字符。
  • \d:匹配任意数字,等价于 [0-9]
  • \D:匹配任意非数字字符。
  • \w:匹配字母、数字、下划线,等价于 [a-zA-Z0-9_]
  • \W:匹配非字母、数字、下划线的字符。
  • \s:匹配空白字符,如空格、制表符等。
  • \S:匹配非空白字符。

量词

  • *:匹配前面元素零次或多次。
  • +:匹配前面元素一次或多次。
  • ?:匹配前面元素零次或一次。
  • {n}:匹配前面元素恰好 n 次。
  • {n,}:匹配前面元素至少 n 次。
  • {n,m}:匹配前面元素至少 n 次,至多 m 次。

边界符

  • ^:匹配字符串开头。
  • $:匹配字符串结尾。
  • \b:匹配单词边界。
  • \B:匹配非单词边界。

字符类

  • [ ]:匹配方括号内任意一个字符。
  • [^ ]:匹配不在方括号内的任意一个字符。

分组与引用

  • ( ):创建捕获组,可提取匹配部分,也能对一组字符应用量词。
  • \n:引用前面第 n 个捕获组。

选择符

  • |:表示 “或” 关系,匹配多个模式中的任意一个。

在 JavaScript 里,exec() 和 test() 均为正则表达式对象的方法,

exec() 方法

作用

exec() 方法用于在指定的字符串中执行一次正则表达式匹配操作,并且返回一个包含匹配结果的数组。若未找到匹配项,则返回 null

语法
regexObj.exec(str)
  • regexObj:正则表达式对象。
  • str:需要进行匹配操作的字符串。
返回值

若匹配成功,返回的数组包含以下内容:

  • index:匹配结果在字符串中的起始位置。
  • input:原始输入的字符串。
  • groups:命名捕获组的匹配结果(如果有的话)。
  • 数组的第一个元素是整个匹配的字符串,后续元素依次是各个捕获组匹配到的内容。
示例
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const str = 'Today is 2024-10-15.';
const result = regex.exec(str);

if (result) {
    console.log('整个匹配的字符串:', result[0]);
    console.log('第一个捕获组:', result[1]);
    console.log('第二个捕获组:', result[2]);
    console.log('第三个捕获组:', result[3]);
    console.log('匹配结果的起始位置:', result.index);
    console.log('原始输入的字符串:', result.input);
} else {
    console.log('未找到匹配项');
}

在上述代码中,正则表达式 (\d{4})-(\d{2})-(\d{2}) 用于匹配日期格式的字符串。exec() 方法返回的数组包含了整个匹配的字符串以及各个捕获组的匹配结果。

image.png

test() 方法

作用

test() 方法用于检查一个字符串是否与某个正则表达式相匹配,若匹配则返回 true,反之则返回 false

语法
regexObj.test(str)
  • regexObj:正则表达式对象。
  • str:需要进行匹配检查的字符串。
返回值
  • 若字符串与正则表达式匹配,返回 true
  • 若不匹配,返回 false
示例

javascript

const regex = /\d+/;
const str1 = 'abc123';
const str2 = 'abc';

console.log(regex.test(str1)); // 输出: true
console.log(regex.test(str2)); // 输出: false

在上述代码中,正则表达式 \d+ 用于匹配一个或多个数字。test() 方法检查字符串中是否包含数字,并返回相应的布尔值。

总结

  • exec() 方法主要用于获取详细的匹配结果,包括整个匹配的字符串和各个捕获组的内容。
  • test() 方法主要用于快速检查字符串是否与正则表达式匹配,返回布尔值。在仅需判断是否匹配而无需具体匹配内容时,使用 test() 方法会更高效。

url解析

// 定义一个包含查询参数的 URL 字符串
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';

// 定义一个用于解析 URL 查询参数的函数
function parseUrl(url) {
    // 使用正则表达式提取 URL 中问号(?)后面的查询参数字符串
    // /.+?(.+)$/ 匹配模式中,第一个捕获组(括号内的部分)就是我们要的查询参数字符串
    const paramStr = /.+?(.+)$/.exec(url)[1];
    // 将查询参数字符串按 & 符号分割成多个键值对组成的数组
    const paramArr = paramStr.split('&');
    // 初始化一个空对象,用于存储解析后的键值对
    const paramObj = {};

    // 遍历键值对数组
    paramArr.forEach((item) => {
        // 检查当前键值对字符串中是否包含等号(=)
        // 如果包含等号,说明是有值的键值对
        if (/=/.test(item)) {
            // 使用解构赋值将键值对字符串按等号分割成键和值
            let [key, value] = item.split('=');
            // 对值进行 URL 解码,将编码后的特殊字符转换为原始字符
            value = decodeURIComponent(value);
            // 检查值是否为纯数字字符串
            // 如果是纯数字字符串,则将其转换为数字类型;否则保持为字符串类型
            value = /^\d+$/.test(value)? Number(value) : value;

            // 检查 paramObj 对象中是否已经存在当前的键
            if (paramObj.hasOwnProperty(key)) {
                // 如果已经存在该键,将新的值添加到已有的值数组中
                // 使用 [].concat() 方法将已有值和新值合并成一个数组
                paramObj[key] = [].concat(paramObj[key], value);
            } else {
                // 如果对象中不存在该键,直接将键值对添加到对象中
                paramObj[key] = value;
            }
        } else {
            // 如果键值对字符串中不包含等号,说明该参数没有显式的值
            // 按照要求,将该参数的值设为 true
            paramObj[item] = true;
        }
    });

    // 遍历结束后,返回存储解析结果的对象
    return paramObj;
}

// 调用 parseUrl 函数对 URL 进行解析,将解析结果存储在 res 变量中
let res = parseUrl(url);
// 打印解析后的对象
console.log(res);
  • [].concat(paramObj[key], value2) 会把 paramObj['id'](即 123)和 value2(即 456)合并成一个数组 [123, 456]
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';

function parseUrl(url) {
    const paramStr = /.+?(.+)$/.exec(url)[1];
    const paramArr = paramStr.split('&');
    const paramObj = {};

    paramArr.forEach((item) => {
        if (/=/.test(item)) {
            let [key, value] = item.split('=');
            value = decodeURIComponent(value);
            value = /^\d+$/.test(value)? Number(value) : value;

            if (paramObj.hasOwnProperty(key)) {
                paramObj[key] = [].concat(paramObj[key], value);
            } else {
                paramObj[key] = value;
            }
        } else {
            paramObj[item] = true;
        }
    });

    return paramObj;
}

let res = parseUrl(url);
console.log(res);

对象转数组

image.png

image.png

// 方式一:将对象的键和值分别存储到数组中
const obj1 = { a: 1, b: 2, c: 3 };
const keys = Object.keys(obj1);
const values = Object.values(obj1);
console.log("方式一:键数组", keys);
console.log("方式一:值数组", values);


// 方式二:将对象转换为键值对数组
const obj2 = { a: 1, b: 2, c: 3 };
const entries = Object.entries(obj2);
console.log("方式二:键值对数组", entries);

// 方式三:自定义转换函数
function objectToArray(obj) {
    const result = [];
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            result.push([key, obj[key]]);
        }
    }
    return result;
}

const obj3 = { a: 1, b: 2, c: 3 };
const arr = objectToArray(obj3);
console.log("方式三:自定义转换结果", arr);  

数组转json

在 JavaScript 中,使用 JSON.stringify 方法能够把数组转换为 JSON 字符串。

javascript

// 定义一个数组
const array = [1, 2, 3, 4, 5];

// 将数组转换为 JSON 字符串
const jsonStr = JSON.stringify(array);

console.log(jsonStr);

手写 Promise

class Promise{        //类
 constructor (executor){
 this.status='pending'
 this.value=undefined     //成功的结果
 this.reason=undefined    //失败的原因
 
 //存放成功的回调 
 this.onResolvedCallbacks = []; 
 //存放失败的回调 
 this.onRejectedCallbacks = [];
 }
 
 //resolve 函数
 let resolve=(value)=>{
 if(this.status === 'pending')
 { this.value = value; 
   this.status = "resolved"; 
   this.onResolvedCallbacks.forEach(fn => fn()); } }// 执行所有成功的回调函数
   
    //reject 函数
 let reject=(reason)=>{
 if(this.status==='pending'){
 this.reason=reason
 this.status='pengding'
 this.onRejectedCallbacks.forEach(fn=>fn() // 执行所有失败的回调函数
 }
 
 //考虑到异常情况
 
 try{
 executor(resolve,reject)  //executor 函数立即执行
 }catch(e){
 reject(e)}  // 如果 executor 函数抛出异常,调用 reject 方法
    }
 }

手写 Promise.all

Promise.all=function(promises)//传入一个数组{
return new Promise((resolve,reject)=>{
let count=0    //计数器
let result=[]  //保存结果的数组
for(let i=0;i<promises.length){
promises[i].then(v=>{
count++
result[i]=v  //将每一个promise的结果存入数组中
if(count===promises.length){resolve (result)}  //判断是否都执行完毕,将结果resolve
},r=>{
reject(r)
}

v 和 r 都是then 回调函数里面的

手写 Promise.race

Promise.race=function(promises){
    return new Promise((resolve,reject)=>{
        for(let i=0;i<promises.length){
           promises[i].then(v=>{ resolve(v)}, //只要有一个promise成功/失败,就立马返回结果
            r=>{reject(r) })
}
})

手写 Promise 封装 ajax请求

function sendAjax(url){
return new Promise((resolve,reject)=>{
const xhr=new XMLHttpRequest()  //创建对象
xhr.open('get',url)  //初始化
xhr.send()   //发送请求
//处理响应结果
xhr.onreadystatechange=function(){
if(xhr.readyState===4){
  //判断成功
  if(xhr.status>=200&&xhr.status<300){
  //成功执行
  resolve(xhr.response)  //响应体
  }else{ 
  //失败执行
  reject(xhr.status)}  //状态码
}
}
})
}

手写 call

Function.prototype.mycall=function(context){  //context 要改变的this指向
    let args=[...arguments].slice(1)  //拿到传入的参数 sub.newCall(add,1,2) 
    context=context||window 
    context.fn=this 
    let res=context.fn(...args)
    return res
}

手写 apply 调用函数,传入的参数必须是数组

Function.prototype.myapply=function(context){

context=context||window 
context.fn=this        //改变this指向   this指向调用函数
let res=null          //函数调用的返回值
//如果在调用函数时,传入参数;执行函数 
if(arguments[1])   {  res=context.fn(...arguments[1]  return res } 
else { res=context.fn()   return res }
return res

}

手写 bind 只改变this指向,不调用函数

特殊的地点在于,如果是使用了new方式 调用一个新函数。 因为 new 操作符会创建一个新对象,并且这个新对象会成为函数执行时的 this,而不是 bind 时指定的上下文。

Function.prototype.mybind=function(context){
let args=[...arguments].slice(1)  //拿到第一个剩余参数
var fn=this  //拿到原始函数
return function  Fn(){   //返回一个新函数
args2=Array.prototype.slice.call(arguments)  //第二个参数
let argsall=args.concat(args2)
    ifObject.getPrototypeOf(this===Fn.prototype()    ){  //判断是不是new的
      return new fn(...argsall)
}
return fn.apply(...argsall)}    //返回执行函数
}

}

观察者模式

class Subject{
    constructor() {
        this.observers = [];
    }

    subscribe(observer) {
        this.observers.push(observer);
    }

    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }
    notify(data) {
        this.observers.forEach(observer => observer.update(data));
    }
}

class Observer {
    constructor(name) {
        this.name = name;
    }

    update(data) {
        console.log(`${this.name} received data:`, data);
    }
}

// 创建一个主题对象
const subject = new Subject();
// 创建观察者对象
const observer1 = new Observer('Observer 1');
// 订阅观察者
subject.subscribe(observer1);
// 发布通知
subject.notify({ message: 'Hello, observers!' });
// 取消订阅
subject.unsubscribe(observer1);

发布订阅模式

class EventBus {
    constructor() {
        // 用于存储事件及其对应的回调函数数组
        this.events = {};
    }

    // 订阅事件
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }

    // 发布事件
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => {
                callback(...args);
            });
        }
    }

    // 取消订阅事件
    off(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb!== callback);
        }
    }
}

// 使用示例
const eventBus = new EventBus();

// 订阅事件
const callback = (data) => {
    console.log(`Received data: ${data}`);
};
eventBus.on('myEvent', callback);

// 发布事件
eventBus.emit('myEvent', 'Hello, Event Bus!');

// 取消订阅
eventBus.off('myEvent', callback);

// 再次发布事件,此时订阅者不会收到消息
eventBus.emit('myEvent', 'This message won\'t be received.');