0
点赞
收藏
分享

微信扫一扫

前端常见手写题

先峰老师 2022-05-02 阅读 63

前端常见手写题

文章目录

手写 new 操作符

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj上,并且执行该构造函数
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function myNew(fn,...args) {
  let obj = Object.create(fn.prototype);
  let res = fn.call(obj,...args);
  if(res && (typeof res === "object" || typeof res === "function")){
    return res;
  }
  return obj;
}

测试案例:

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

Person.prototype.say = function(){
  console.log(this.age);
}

let p1 = myNew(Person,"zs",18);
console.log(p1.name);//zs
console.log(p1);//Person { name: 'zs', age: 18 }
p1.say();//18

call/bind/apply

call:

实现call的思路是:将函数设置为对象的属性,然后执行该函数。

Function.prototype.myCall = function (context, ...args) {
    //context是传入的那个对象
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this; //this指向调用call的函数
  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
  return context[fn](...args);
};

代码测试:

let obj = {
  value:1
}

function bar(name,age){
  console.log(this.value);
  return {
    value:this.value,
    name:name,    
    age:age
  }
}
console.log(bar.myCall(obj,'zs',18));
/**
 * 1
 * { value: 1, name: 'zs', age: 18 }
 */

apply:

// apply原理一致  只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  // 执行函数并返回结果
  return context[fn](...args);
};

bind:

bind可以分为多次传入参数,bind是返回绑定this之后的函数

Function.prototype.mybind = function(thisArg,...argArray){
    //1.获取到真实需要调用的函数
    var fn = this;

    //2.绑定this
    //如果是null或undefined,就绑定window,如果不是,那就统一转换成Object类型(这里主要针对基本数据类型,但调用Object()对引用数据类型没有影响)
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg):window;

    //最后要将proxyFn函数返回,(因为:`bind`是返回绑定this之后的函数,不会立即执行)
    function proxyFn(...args){
        //3.将函数放到thisArg中进行调用,相当于将真实需要调用的函数变成传入的对象的属性
        thisArg.fn = fn;
        //特殊:将两个传入的参数进行合并,(因为:`bind`可以分为多次传入参数)
        var finalArgs = [...argArray,...args];
        var result = thisArg.fn(...finalArgs);

        delete thisArg.fn;//最后要将这个属性从传入的对象上删除

        //4.返回结果:将真实需要调用的函数的返回值返回
        return result;
    }

    return proxyFn;
}

实现 instanceof 操作符

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。

instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。

function myInstanceof(left,right) {
  //先用typeof判断基础数据类型,如果是,直接返回false
  if(typeof left !== 'object' || left === null) {
    return false;
  }
  //getPrototypeOf能够拿到参数的原型对象(显式原型)
  let proto = Object.getPrototypeOf(left);
  while(true){
    if(proto === null) {//如果找到了原型链的尽头
      return false;
    }
    if(proto === right.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
}

//验证
console.log(myInstanceof(new Number(123),Number));//true
console.log(myInstanceof(123,Number));//false

compose(组合函数)

参数从右往左执行

// 用法如下:
function fn1(x) {
  return x + 1;
}
function fn2(x) {
  return x + 2;
}
function fn3(x) {
  return x + 3;
}
function fn4(x) {
  return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);//compose函数返回值是一个函数
console.log(a(1)); // 1+4+3+2+1=11

实现compose函数:

function compose(...fns){
  if(!fns.length) return (arg) => arg;
  if(fns.length === 1) return fns[0];
  return arg => fns.reduceRight((prev,fn) => {
    return fn(prev)
  },arg)
}

柯里化函数

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数。这个过程就称之为柯里化

核心思想是把多参数传入的函数拆成单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。

举例:

function  add1(x, y, z) {
  return x + y +z;
}
console.log(add1(10,20,30));

//柯里化处理函数
function add2(x) {
  return function(y) {
    return function(z) {
      return x+y+z;
    }
  }
}

console.log(add2(10)(20)(30));

var add3 = x => y => z => {
  return x + y + z
}
console.log(add3(10)(20)(30))

代码实现:

function currying(fn, ...args) {
  //获取函数需要的参数长度
  const length = fn.length;

  let allArgs = [...args];

  //返回一个函数
  const res = (...newArgs) => {
    allArgs = [...allArgs, ...newArgs];

    //判断参数的长度是否已经满足函数所需参数的长度
    if (allArgs.length === length) {
      return fn(...allArgs);
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      return res;
    }
  };
  return res;
}

实现AJAX请求

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (xhr.readyState !== 4) return;
  // 当请求成功时
  if (xhr.status === 200 || xhr.status === 304) {
    console.log(xhr.responseText);
  } else {
    console.error(xhr.statusText);
  }
};
// 发送 Http 请求
xhr.send();

使用Promise封装AJAX请求

const getJSON = function (url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, false);
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    };
    xhr.send();
  });
};

实现数组去重

ES6方法:使用数据结构set

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]

空对象 let obj ={}利用对象属性不能重复的特性

hasOwnProperty:这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
  let map = {};//空对象 let obj ={}利用对象属性不能重复的特性
  let res = [];
  for(var i = 0; i < array.length; i++) {
    if(!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1;
      res.push(array[i]);
    }
  }
  return res;
}

Map去重

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

let unique = arr => {
  let map = new Map();
  let brr = [];
  arr.forEach(item => {
    if(!map.has(item)){
      map.set(item,true);
      brr.push(item);
    }
  })
  return brr;
}

console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]

使用 indexOf / includes

使用 indexOf 返回数组是否包含某个值,没有就返回 -1,有就返回下标

使用 includes 返回数组是否包含某个值,没有就返回false,有就返回true

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

let unique = arr => {
  let brr = [];
  arr.forEach(item => {
    if(brr.indexOf(item) === -1) brr.push(item);
    //if(!brr.includes(item)) brr.push(item);
  })
  return brr;
}

console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]

使用 filter

indexOf() 方法可返回数组中某个指定的元素位置。

该方法将从头到尾地检索数组,看它是否含有对应的元素。开始检索的位置在数组 start 处或数组的开头(没有指定 start 参数时)。如果找到一个 item,则返回 item 的第一次出现的位置。开始位置的索引为 0。如果在数组中没找到指定元素则返回 -1。

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

let unique = arr => {
  let brr = arr.filter((item,index) => {
    return arr.indexOf(item) === index;
  })
  return brr;
}

console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]

实现数组元素求和

arr=[1,2,3,4,5,6,7,8,9,10],求和

let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);


//方法二:递归实现
let arr = [1, 2, 3, 4, 5, 6] 

function add(arr) {
    if (arr.length == 1) return arr[0] 
    return arr[0] + add(arr.slice(1)) 
}
console.log(add(arr)) // 21

arr=[1,2,3,[[4,5],6],7,8,9],求和

var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);


数组排序 --> 常见排序算法

冒泡排序–时间复杂度 n^2

function bubbleSort(arr) {
  const len = arr.length;

  for(let i=0; i<len; i++){
    for(let j=0; j<len-1; j++){
      if(arr[j] > arr[j+1]){
        const temp = arr[j];
        arr[j] = arr[j+1];
        arr[j+1] = temp;
      }
    }
  }

  return arr;
}

console.log(bubbleSort([3,6,1,5,2]));

选择排序–时间复杂度 n^2

把第一个没有排序过的元素设置为最小值,
从前往后遍历每个没有排序过的元素,
如果元素 < 现在的最小值
将此元素设置成为新的最小值,
将最小值和第一个没有排序过的位置交换。

这样下来,每一趟排序都会找到一个最小值,将它放在队列的前面。

function selectSort(arr) {
  const len =arr.length;

  for(let i = 0; i< len - 1; i++) {
    
    //在每一轮循环里,将第一个未排序的数字当做最小值
    let indexMin = i;
    for(let j = i; j < len; j++) {
      if(arr[j] < arr[indexMin]) {
        indexMin = j;
      }
    }
    //上面的for循环结束后,此时已经得到了真正最小值的下标为indexMin
    //如果最小值不是每次排序的第一个数,那么就交换
    if(indexMin !== i) {
      const temp = arr[i];
      arr[i] = arr[indexMin];
      arr[indexMin] = temp;
    }
  }

  return arr;
}

console.log(selectSort([3,6,1,5,2]));

插入排序–时间复杂度 n^2

将第一个元素标记为已排序,遍历每个没有排序的元素,“提取”该元素,将它与前面已排序的元素进行比较,找到它应该放置的位置,并将它插入进去。

function insertSort(arr) {
  const len =arr.length;

  for(let i = 1; i < len; i++) {
    const temp = arr[i];
    let j = i;
    while(j>0) {
      //因为要插入j,所以要让前面的值都向后挪一位
      if(arr[j - 1] > temp) {
        arr[j] = arr[j-1];
      }else {
        break;
      }
      j--;
    }
    //此时下标为j的地方就是应该插入的地方
    arr[j] = temp;
  }

  return arr;
}

console.log(insertSort([3,6,1,5,2]));

快排–时间复杂度 nlogn~ n^2 之间

平均时间复杂度:O(n*log2n)

原理
和归并排序一致,它也使用了分治策略的思想,它也将数组分成一个个小数组,但与归并不同的是,
它实际上并没有将它们分隔开。
快排使用了分治策略的思想,所谓分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或
多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,
则原问题的解则为子问题解的合并。

快排的过程简单的说只有三步:
首先从序列中选取一个数作为基准数
将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边 (一次快排
partition )
然后分别对基准的左右两边重复以上的操作,直到数组完全排序

具体按以下步骤实现:

  • 创建两个指针分别指向数组的最左端以及最右端
  • 在数组中任意取出一个元素作为基准
  • 左指针开始向右移动,遇到比基准大的停止
  • 右指针开始向左移动,遇到比基准小的元素停止,交换左右指针所指向的元素
  • 重复3,4,直到左指针超过右指针,此时,比基准小的值就都会放在基准的左边,比基准大
  • 的值会出现在基准的右边
  • 然后分别对基准的左右两边重复以上的操作,直到数组完全排序
function quickSort(arr) {
  const len =arr.length;

  if(len <= 1){
    return arr;
  }
  var left = [],
      right = [],
      mid = arr[0];
  for(let i = 1;i < len; i++){
    if(arr[i] > mid){
      right.push(arr[i]);
    }else{
      left.push(arr[i]);
    }
  }
  // return quick(left).concat([mid],quick(right));
  return [...quick(left),mid,...quick(right)]

}

console.log(quickSort([3,6,1,5,2]));

归并排序–时间复杂度 nlog(n)

它采用了分治策略,将数组分成2个较小的数组,然后每个数组再分成两个更小的数组,直至每个数组里只包含一个元素,然后将小数组不断的合并成较大的数组,直至只剩下一个数组,就是排序完成后的数组序列。

实现步骤:

  • 将原始序列平分成两个小数组
  • 判断小数组⻓度是否为1,不为1则继续分裂
  • 原始数组被分称了⻓度为1的多个小数组,然后合并相邻小数组(有序合并)
  • 不断合并小数组,直到合并称一个数组,则为排序后的数组序列
function merge(left, right) {
  let res = [];
  let i = 0;
  let j = 0;
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      res.push(left[i]);
      i++;
    } else {
      res.push(right[j]);
      j++;
    }
  }
  if (i < left.length) {
    res.push(...left.slice(i));
  } else {
    res.push(...right.slice(j));
  }
  return res;
}

function mergeSort(arr) {
  if (arr.length < 2) {
    return arr;
  }
  const mid = Math.floor(arr.length / 2);

  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

console.log(mergeSort([3, 6, 2, 4, 1]));

实现数组扁平化

点击这里👉【JavaScript】实现数组扁平化


防抖

函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

// 函数防抖的实现
function debounce(fn, wait) {
  let timer = null;

  return function() {
    let context = this,
        args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

//使用
btn.addEventListener(
  "click",
  debounce(() => {
    console.log(111);
  }, 1000)
);

节流

频繁触发的时候,比如滚动或连续点击,在指定的间隔时间内,只会执行一次

应用场景:resize、scroll等

//连续点击的话,第一下点击会立即执行一次 然后每过 wait 秒执行一次
function throttle(fn, wait) {
  let date = Date.now()
  return function() {
    let now = Date.now()
    // 用当前时间 减去 上一次点击的时间 和 传进来的时间作对比
    if (now - date > wait) {
      fn.call(this, arguments)
      date = now
    }
  }
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(111);
  }, 1000)
);

浅拷贝

const shallowClone = (target) => {
    if (typeof target === 'object' && target !== null) {  //如果源对象是引用数据类型

      const cloneTarget = Array.isArray(target) ? []: {}; 

      for (let prop in target) {  //遍历源对象中的属性
        if (target.hasOwnProperty(prop)) { 
            cloneTarget[prop] = target[prop]; 
        }  
      }  
      return cloneTarget; 

    } else {  //如果是基本数据类型,直接返回
      return target;  
    }  
}

let obj = {a:2,b:{c:1}};

let obj1 = shallowClone(obj);

console.log(obj1);//{ a: 2, b: { c: 1 } }

obj1.b.c = 10;
obj1.a = 20;

console.log(obj);//{ a: 2, b: { c: 10 } }
console.log(obj1);//{ a: 20, b: { c: 10 } }

深拷贝

function deepClone(target, map = new Map()) {
  // 基本数据类型直接返回
  if (typeof target !== 'object') {
    return target
  }

  // 引用数据类型特殊处理
  // 判断数组还是对象
  const temp = Array.isArray(target) ? [] : {}

  if (map.get(target)) {
    // 已存在则直接返回
    return map.get(target)
  }
  // 不存在则第一次设置
  map.set(target, temp)

  for (const key in target) {
    // 递归
    temp[key] = deepClone(target[key], map)
  }
  return temp
}

//测试代码
const a = {
  name: 'zs',
  age: 18,
  friend: { 
    name: "pjy",
    address: {
      city: "China"
    }
  },
}
a.key = a // 测试环引用
const b = deepClone(a)

console.log(b)
/**
 * {
    name: 'zs',
    age: 18,
    friend: { name: 'pjy', address: { city: 'China' } },
    key: [Circular *1]
  }
 */
console.log(b === a) // false

寄生式组合继承

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name) {
  Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;

//测试代码
let child = new Children("111");
console.log(child.name);
child.say();
child.play();

ES6继承

class Animal {
  // 构造函数,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数.
  constructor(name,color) {
    this.name = name;
    this.color = color;
  }
  // toString 是原型对象上的属性
  toString() {
    console.log('name:' + this.name + ',color:' + this.color);
  }
}

class Cat extends Animal {
  constructor(action) {
    // 子类必须要在constructor中指定super 函数,否则在新建实例的时候会报错.
    // 如果没有置顶constructor,默认带super函数的constructor将会被添加、
    super('cat','white');
    this.action = action;
  }
  toString() {
      //调用父类的方法
    super.toString();
  }
}


let cat = new Cat("eating");
console.log(cat);
console.log(cat.action);
cat.toString();

Promise.all

Promise.all 可以把多个 Promise 实例打包成一个新的 Promise 实例。传进去一个值为多个 Promise 对象的数组,成功的时候返回一个结果的数组,返回值的顺序和传进去的顺序是一致对应得上的,如果失败的话就返回最先 reject 状态的值。

如果遇到需要同时发送多个请求并且按顺序返回结果的话,Promise.all就可以完美解决这个问题。

Promise.all 的规则是这样的:

  • 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
  • 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;
  • 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
Promise.all = function(promises){
  //返回结果为promise对象
  return new Promise((resolve,reject) => {
    let count = 0; //计数器:只有所以的promise返回成功,才成功
    let arr = []; //存放每个promise的结果

    //遍历
    for(let i=0;i<promises.length;i++){
      promises[i].then(v => {
        count++;
        //将当前promise对象成功的结果 存入到数组中
        //注意:不可以用 arr.push(v),因为这样无法保证返回的顺讯
        arr[i] = v;
        if(count === promises.length){
          resolve(arr);
        }
      },r => {
        reject(r);
      })
    }
  })
}

Promise.race

Promise.race = function(promises){
  //返回结果为promise对象
  return new Promise((resolve,reject) => {
    //遍历
    for(let i=0;i<promises.length;i++){
      promises[i].then(v => {
        resolve(v);
      },r => {
        reject(r);
      })
    }
  })
}
举报

相关推荐

0 条评论