前端手撕算法

142 阅读7分钟

JS原理类

Ajax的请求过程(手写ajax)

使用readystatechange 事件监控 readyState 的值

let xhr = new XMLHttpRequest(); 
xhr.onreadystatechange = function() { 
  if (xhr.readyState == 4) { 
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { 
      alert(xhr.responseText); 
    } else { 
      alert("Request was unsuccessful: " + xhr.status); 
    } 
  } 
}; 
xhr.open("get", "example.php", true); 
xhr.send(null);

使用onload事件监控 readyState 的值

let xhr = new XMLHttpRequest(); 
xhr.onload = function() { 
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { 
    alert(xhr.responseText); 
  } else { 
    alert("Request was unsuccessful: " + xhr.status); 
  } 
}; 
xhr.open("get", "altevents.php", true); 
xhr.send(null);

call/bind/apply

规定this的指向:apply(), call(),bind()

Function.prototype._call = function(thisArg, ...args) {
   //1.首先接收对象
  //如果是undefined或者 null 指向window,否则使用 Object() 将上下文包装成对象
  const context = Object(thisArg) || window;
  //2.然后在对象里新建一个临时属性接收this
  // 通过 obj.fn() 执行时,this 会指向前面的 obj 对象,所以this是,this指向函数foo
  // 为context设置一个临时属性接收指向函数foo的this指针
  //属性可以用symbol,避免属性名冲突
  const key = Symbol()
  context[key] = this
  //3.执行对象内的函数
  const result = context[key](...args)
  //4.删除临时对象
  delete context[key]
  //5.返回执行结果
  return result
}
// 只需要把第二个参数改成数组形式就可以了。
Function.prototype._apply = function(thisArg, array = []) {
  const context = Object(thisArg) || window;
  //给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol()
  context[key] = this
  const result = context[key](...array)
  delete context[key]
  return result
}
Function.prototype._bind = function(ctx, ...args) {
  // 下面的this指向调用_bind的函数,保存给_self
  const _self = this
  // bind 要返回一个函数, 就不会立即执行了
  const newFn = function(...rest) {
    // 调用 call 修改 this 指向
    return _self.call(ctx, ...args, ...rest)
  }
  if (_self.prototype) {
    // 复制源函数的prototype给newFn 一些情况下函数没有prototype,比如箭头函数
    newFn.prototype = Object.create(_self.prototype);
  }
  return newFn
}

书写注意点: mycall:参数一个obj,一个args,contex是Object(obj) || window,Symbol不用new

myapply:传入数组,测试集要用数组

mybind,返回函数用call,因为参数是...args,...rest

Promise.all

/*
注意点:
1.函数返回的是一个Promise对象
2.最好判断一下传入的参数是否为数组
3.并不是push进result数组的,而是通过下标的方式进行存储,这是因为我们为了保证输出的顺序,因为Promise对象执行的时间可能不同,push的话会破坏顺序。
4.通过计数标志来判断是否所有的promise对象都执行完毕了,因为在then中表示该promise对象已经执行完毕。
*/

function PromiseAll(promiseArray) {    //返回一个Promise对象
     return new Promise((resolve, reject) => {
     
        if (!Array.isArray(promiseArray)) {                        //传入的参数是否为数组
            return reject(new Error('传入的参数不是数组!'))
        }

        const res = []
        let counter = 0                         //设置一个计数器
        for (let i = 0; i < promiseArray.length; i++) {
            Promise.resolve(promiseArray[i]).then(value => {
                counter++                  //使用计数器返回 必须使用counter
                res[i] = value
                if (counter === promiseArray.length) {
                    resolve(res)
                }
            }).catch(e => reject(e))
        }
    })
}

instanceof

function myInstanceof(obj, func) {
    if(!['function', 'object'].includes(typeof obj) || obj === null) {
    	// 基本数据类型直接返回false,因为不满足instanceof的左侧参数是对象或者说引用类型
        return false
    }
    let objProto = obj.__proto__, funcProto = func.prototype
    while(objProto !== funcProto) {
    	// obj.__proto__不等于func.prototype时,继续通过__proto__向上层查找
    	// 当找到原型链尽头Object.prototype.__proto__=null 时还未找到,就返回false
        objProto = objProto.__proto__
        if(objProto === null){
            return false
        }
    }
    // obj.__proto__ 等于 prototype = func.prototype 时,不会进入上面循环,返回true
    // 不等进入上面循环,找到相等时会跳出循环,走到这里返回true
    return true
}
//测试
function A(){}
let a=new A;
console.log(myInstanceof(a,A))//true
console.log(myInstanceof([],A))//false

防抖和节流

函数防抖和节流,都是控制事件触发频率的方法。

防抖(debounce):

防抖触发高频率事件时n秒后只会执行一次,如果n秒内再次触发,则会重新计算。

简单概括:每次触发时都会取消之前的延时调用

场合具体有:

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
function debounce(fn, delay) {
  let timer = null;

  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}


使用示例:
function handleInput() {
  console.log('输入已停止');
}

const input = document.querySelector('#input');

input.addEventListener('input', debounce(handleInput, 1000));

节流(thorttle):

高频事件触发,每次触发事件时设置一个延迟调用方法,并且取消之前延时调用的方法。

简单概括:每次触发事件时都会判断是否等待执行的延时函数

场合具体有:

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
function throttle(fn, delay) {
  let timer = null;

  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}


使用示例:
function handleResize() {
  console.log('窗口大小已更改');
}

window.addEventListener('resize', throttle(handleResize, 1000));

细节问题:为什么节流用timer=null而防抖用clearTimeout(timer)?

times = null 只是将定时器的指向改为null,并没有在内存中清除定时器,定时器还是会如期运行;如同在debounce函数中将times = null并不能达到防抖的目的,因为每个定时器都只是将内存地址指向了null,而每个定时器都将会执行一遍.

而clearTimeout(times)会将定时器从内存中清除掉.

另外关于定时器是否需要用完清除的问题.

具体还得看需求,如果是很少个数的定时器,可以不清除;如果数量很多或者数量不可控,则必须要做到手动清除,否则定时器将会非常占用电脑cpu.非常影响性能.

深拷贝与浅拷贝

new

// 首先创建一个新对象,这个新对象的__proto__属性指向构造函数的prototype属性
// 此时构造函数执行环境的this指向这个新对象
// 执行构造函数中的代码,一般是通过this给新对象添加新的成员属性或方法。
// 最后返回这个新对象。

// func是构造函数,...args是需要传给构造函数的参数
function myNew(func, ...args) {
  // 生成原型链
  var obj = Object.create(func.prototype);
  // 盗用构造函数继承
  func.call(obj, ...args);
  // 最后return这个对象
  return obj;
}


测试:
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const person = myNew(Person, 'John', 25);
console.log(person.name); // 'John'
console.log(person.age); // 25

简单数据结构类

数组排序

js中的sort()方法用于对数组元素进行排序,具体是如何实现的?查阅资料发现,V8 引擎 sort 函数只给出了两种排序 InsertionSort 和 QuickSort,数组长度小于等于 22 的用插入排序 InsertionSort,比22大的数组则使用快速排序 QuickSort。源码中这样写道:

// In-place QuickSort algorithm.

// For short (length <= 22) arrays, insertion sort is used for efficienc

Array.sort()的快排实现方式

function quickSort(arr = []) {
    if (arr.length <= 1) {
        return arr;
    }

    const pivot = arr[Math.floor(arr.length / 2)];
    const left = [];
    const right = [];

    for (let i = 0; i < arr.length; i++) {
        if (i === Math.floor(arr.length / 2)) {
            continue;
        }
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }

    return [...quickSort(left), pivot, ...quickSort(right)];
}

Array.sort()的插入排序实现方式

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        let temp = arr[i];
        let j = i - 1;

        while (j >= 0 && arr[j] > temp) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = temp;
    }
    return arr;
}

初始状态:[5, 2, 6, 1, 3, 9, 8, 7, 4]

第一步:[2, 5, 6, 1, 3, 9, 8, 7, 4]

第二步:[2, 5, 6, 1, 3, 9, 8, 7, 4]

第三步:[1, 2, 5, 6, 3, 9, 8, 7, 4]

第四步:[1, 2, 3, 5, 6, 9, 8, 7, 4]

第五步:[1, 2, 3, 5, 6, 9, 8, 7, 4]

第六步:[1, 2, 3, 5, 6, 8, 9, 7, 4]

第七步:[1, 2, 3, 5, 6, 7, 8, 9, 4]

第八步:[1, 2, 3, 5, 6, 7, 8, 9, 4]

第九步:[1, 2, 3, 4, 5, 6, 7, 8, 9]

数组去重

  1. 利用Set数据结构去重
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3]
  1. 利用Array.prototype.filter()方法去重
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = arr.filter((item, index, arr) => arr.indexOf(item) === index);
console.log(uniqueArr); // [1, 2, 3]
  1. 利用Array.prototype.reduce()方法去重
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = arr.reduce((prev, curr) => prev.includes(curr) ? prev : [...prev, curr], []);
console.log(uniqueArr); // [1, 2, 3]
  1. 利用Object对象去重
const arr = [1, 2, 2, 3, 3, 3];
const obj = {};
const uniqueArr = arr.filter(item => obj.hasOwnProperty(item) ? false : (obj[item] = true));
console.log(uniqueArr); // [1, 2, 3]

数组扁平化

  1. flat(depth)
let a = [1,[2,3,[4,[5]]]];  
a.flat(Infinity); // [1,2,3,4,5]  a是4维数组
  1. for循环
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
function flatten(arr) {
  var res = [];
  for (let i = 0, length = arr.length; i < length; i++) {
    if (Array.isArray(arr[i])) {
      res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
      //res.push(...flatten(arr[i])); //扩展运算符  
    } else {
      res.push(arr[i]);
    }
  }
  return res;
}
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
  1. while循环
var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
function flatten(arr) {
  while (arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr);
    //arr = Array.prototype.concat.apply([],arr);
  }
  return arr;
}
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
  1. reduce方法
var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
function flatten(arr) {
  return arr.reduce((res,next) =>{
    return res.concat(Array.isArray(next)? flatten(next) : next);
  },[]);
}
  1. stack方法
var arr1 = [1, 2, [3], [1, 2, 3, [4, [2, 3, 4]]]];
function flatten(input) {
  const stack = [...input]; //保证不会破坏原数组
  const result = [];
  while (stack.length) {
    const first = stack.shift();
    if (Array.isArray(first)) {
      stack.unshift(...first);
    } else {
      result.push(first);
    }
  }
  return result;
}
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

通用计数器组件

通用计数器组件是一种在前端开发中常见的组件,它提供了计数器的基本功能,包括增加、减少、获取当前值等操作,并且可以通过事件回调的方式来监听计数器值的变化。通用计数器组件可以被广泛应用于各种场景中,例如商品数量的选择、投票统计等等。由于其常用性和通用性,因此在前端开发中经常需要使用通用计数器组件。

// 定义一个构造函数 Counter
function Counter(initialValue = 0) {
  // 初始化计数器的值和回调函数列表
  this.value = initialValue;
  this.listeners = [];
}

// 定义 Counter 的原型对象上的 increment 方法
Counter.prototype.increment = function() {
  // 增加计数器的值
  this.value++;
  // 通知所有回调函数,计数器的值已经发生了变化
  this.notifyListeners();
};

// 定义 Counter 的原型对象上的 decrement 方法
Counter.prototype.decrement = function() {
  // 减少计数器的值
  this.value--;
  // 通知所有回调函数,计数器的值已经发生了变化
  this.notifyListeners();
};

// 定义 Counter 的原型对象上的 getValue 方法
Counter.prototype.getValue = function() {
  // 获取当前计数器的值
  return this.value;
};

// 定义 Counter 的原型对象上的 setValue 方法
Counter.prototype.setValue = function(newValue) {
  // 设置计数器的值为 newValue
  this.value = newValue;
  // 通知所有回调函数,计数器的值已经发生了变化
  this.notifyListeners();
};

// 定义 Counter 的原型对象上的 addListener 方法
Counter.prototype.addListener = function(callback) {
  // 将回调函数添加到回调函数列表中
  this.listeners.push(callback);
};

// 定义 Counter 的原型对象上的 removeListener 方法
Counter.prototype.removeListener = function(callback) {
  // 查找回调函数在回调函数列表中的索引
  const index = this.listeners.indexOf(callback);
  // 如果找到了回调函数,则将其从回调函数列表中移除
  if (index !== -1) {
    this.listeners.splice(index, 1);
  }
};

// 定义 Counter 的原型对象上的 notifyListeners 方法
Counter.prototype.notifyListeners = function() {
  // 遍历回调函数列表,依次调用每个回调函数并传入计数器的当前值
  for (const listener of this.listeners) {
    listener(this.value);
  }
};

函数柯里化、组合函数