前端手写题(一)

164 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、javascript基础

1. 手写防抖函数:闭包

防抖函数是在事件触发n秒后执行回调函数,如果在n秒内又触发事件,则重新计时。例如在输入框,查找内容或者请求,在300,500毫秒后返回结果

function debounce(fn,delay = 500) {
  // 初始化将timer设为null
  let timer = null;
  return function() {
    const self = this;
    const args = arguments;
    if (timer) {
      // timer存在,清除setTimeout
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(self,args)
      // 执行后将timer设为null
      timer = null;
    },delay)
  }
}

const input = document.createElement('input')
input.id = 'input1'
document.body.appendChild(input)
const input1 = document.getElementById('input1')
// 调用debounce,是调用的return函数
input1.addEventListener('keyup',debounce(function() {
  console.log(input1.value)
},500))

2. 手写节流函数: 闭包

节流函数是在某一时间段内只触发一次事件的回调函数,如果在这一时间段中触发了多次事件,只生效一次。例如在100毫秒之内拖拽div,100毫秒只执行一次事件

function throttle(fn,delay = 100) {
  let timer = null;
  return function() {
    const self = this;
    const args = arguments;
    if (timer) {
      return
    }
    timer = setTimeout(() => {
      fn.apply(self,args);
      timer = null
    },delay)
  }
}

const div = document.createElement('div')
div.id = 'div1'
document.body.appendChild(div)
const div1 = document.getElementById('div1')
div1.draggable = true;
div1.style.height = '100px';
div1.style.width = '200px';
div1.style.border = '1px solid red'
div1.addEventListener('drag',throttle(function(e) {
  console.log(e.offsetX,e.offsetY)
},100))

3. 手写bind函数

bind(this,arg1,arg2...)

  1. bind传入参数,第一个参数是this的指向,没有则为undefined或null,默认指向全局window
  2. bind剩下的参数可单个,多个传入
  3. bind返回一个函数
Function.prototype.myBind = function() {
  // 获取调用函数
  const fn = this;
  // 判断是否为函数
  if (typeof fn !== 'function') {
    return
  }
  // 获取传入的参数转为数组
  const arr = Array.prototype.slice.call(arguments);
  // 获取this,取出第一个参数this,剩下的是参数
  const self = arr.shift();

  return function() {
    return fn.apply(self,arr)
  }
}
// 校验
function f1(a,b) {
  console.log(this, 'this', a, b, 'a---b')
  return 'this is f1'
}
const f2 = f1.myBind({x:10},10,20)
console.log(f2())
const f3 = f1.myBind({x:10})
console.log(f3())
const f4 = f1.myBind()
console.log(f4())
console.log([1,2,3].myBind())

/**
 * 输出结果:与bind输出结果一样
 * {x: 10} 'this' 10 20 'a---b'
 * this is f1
 * {x: 10} 'this' undefined undefined 'a---b'
 * window 'this' undefined undefined 'a---b'
 */

4. 手写apply函数

apply(this,[arr])

  1. apply传入参数,第一个参数是this的指向,没有则为undefined或null,默认指向全局window
  2. apply第二个参数是一个数组
Function.prototype.myApply = function() {
  // 获取调用函数
  const fn = this;
  // 判断是否为函数
  if (typeof fn !== 'function') {
    return
  }
  // 保存this指向,不存在则指向window
  const self = arguments[0];
  // 判断第二个参数是否存在,apply(this,arr)
  let result;
  if (arguments[1]) {
    result = fn.call(self,...arguments[1])
  }
  else {
    result = fn.call(self)
  }
  
  return result;
}
// 校验
function f1(...args) {
  console.log(this, 'this', args, 'args---')
  return 'this is f1'
}
const arr = [10,14,25,5]
const f2 = f1.myApply({x:10},arr)
console.log(f2)
console.log(f1.myApply({x:10}))
console.log(f1.myApply())
console.log(arr.myApply())

/**
 * 输出结果:与apply输出结果一样
 * {x: 10} 'this' (4) [10, 14, 25, 5] 'args---'
 * this is f1
 * {x: 10} 'this' [] 'args---'
 * window 'this' [] 'args---'
 * apply.js:31 Uncaught TypeError: arr.myApply is not a function
 */

5. 手写call函数

call(this,arg1,arg2...)

  1. call传入参数,第一个参数是this的指向,没有则为undefined或null,默认指向全局window
  2. call剩下的参数可单个,多个传入
Function.prototype.myCall = function() {
  // 获取调用函数
  const fn = this;
  // 判断是否为函数
  if (typeof fn !== 'function') {
    return
  }
  // 获取传入的参数转为数组
  const arr = Array.prototype.slice.call(arguments);
  // 获取this,取出第一个参数this,剩下的是参数
  const self = arr.shift();
  
  return fn.apply(self,arr)
}

function f1(a,b) {
  console.log(this, 'this', a, b, 'a---b')
  return 'this is f1'
}
const f2 = f1.myCall({x:10},10,20)
console.log(f2)
console.log(f1.myCall({x:10}))
console.log(f1.myCall())
// console.log([1,2,3].myCall())

/**
 * 输出结果:与call输出结果一样
 * {x: 10} 'this' 10 20 'a---b'
 * this is f1
 * {x: 10} 'this' undefined undefined 'a---b'
 * window 'this' undefined undefined 'a---b'
 */

6. 手写深拷贝:递归

复制一个对象或数组,如果改变复制后的其中某一属性,原对象中的属性也会跟着改变,深拷贝出来的对象,则不会改变原对象

  1. 手写深拷贝
function deepClone(obj = {}) {
  // 判断是否是数组或者对象
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  // 判断obj是数组还是对象
  let result;
  if (Array.isArray(obj)) {
    result = [];
  }
  else {
    result = {};
  }
  // 遍历并递归
  for(let key in obj) {
    // 判断是否是自身属性
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key])
    }
  }

  return result
}

// 校验
const obj = {
  name: 'lili',
  school: {
    city: 'beijing'
  }
}
const obj1 = deepClone(obj);
obj1.school.city = 'shanghai';
console.log(obj1.school.city,obj.school.city)
/**
 * 输出结果:
 * shanghai beijing
 */
  1. 使用JSON.parse(JSON.stringify(data)) 它的原理就是利用JSON.stringify 将js转为JSON字符串,再使用JSON.parse来还原js对象。但是对象中的函数,undefined,symbol,使用JSON.stringify()处理后,都会消失
const obj = {
  name: 'lili',
  school: {
    city: 'beijing'
  }
}

const obj2 = JSON.parse(JSON.stringify(obj))

obj2.school.city = 'hangzhou';
console.log(obj2.school.city,obj.school.city)//hangzhou beijing
  1. 使用lodash的_.cloneDeep()方法
import _ from 'lodash';

const obj = {
  name: 'lili',
  school: {
    city: 'beijing'
  }
}

const obj3 = _.cloneDeep(obj)

obj3.school.city = 'nanjing';
console.log(obj3.school.city,obj.school.city)//nanjing beijing

7. 浅拷贝方法

  1. Object.assign(result,obj,obj1...)接受的第一个参数是目标对象,其余参数是源对象
  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为null 和 undefined 不能转化为对象,所以第一个参数不能为null或 undefined,会报错。
const obj = {
  name: 'lili',
  school: {
    city: 'beijing'
  }
}
const obj1 = {
  name: 'shallowCopy',
  age: 18
}
const obj2 = 'ab'

const res1 = Object.assign(obj,obj1,obj2)
console.log(res1,obj,obj1,obj2)
/**
 * {
 *  0: "a",
    1: "b",
    age: 18,
    name: "shallowCopy",
    school: {city: 'beijing'}
 * }
  {
    0: "a",
    1: "b",
    age: 18,
    name: "shallowCopy",
    school: {city: 'beijing'}
  }
  {
    name: "shallowCopy",
    school: {city: 'beijing'}
  }
  'ab'
 */
console.log(Object.assign(undefined,obj))// 报错
  1. 扩展运算符:...
const obj = {
  name: 'lili',
  school: {
    city: 'beijing'
  }
}
const obj2 = 'ab'
const res2 = {...obj,...obj2}
obj.name = 'shallowCopy'
obj.school.city = 'shanghai'
console.log(res2,obj)
/**
 * {
 *  0: "a",
    1: "b",
    name: "lili",
    school: {city: 'shanghai'}
 * }
  {
    name: "shallowCopy",
    school: {city: 'shanghai'}
  }
 */

8. 深度比较:递归

function isObject(obj) {
  return typeof obj === 'object' && obj !== null
}
function isEqual(obj1,obj2) {
  // 先判断是否是对象或者数组,不是的话直接进行比较
  if (!isObject(obj1) || !isObject(obj2)) {
    return obj1 === obj2
  }
  // 是对象或者数组,比较长度是否相同,不相同直接返回false
  const obj1keys = Object.keys(obj1);
  const obj2keys = Object.keys(obj2);
  if (obj1keys.length !== obj2keys.length) {
    return false;
  }
  // 比较是否全相等,for in 适用于对象和数组
  for (let key in obj1) {
    // 递归来比较是否完全相等
    const res = isEqual(obj1[key],obj2[key])
    // 某一次不相等就返回false,不用再比较
    if (!res) {
      return false;
    }
  }
  // 全相等返回true
  return true;

}

const obj1 = {
  a: 100,
  b: {
    x: 10,
    y: 20
  }
}

const obj2 = {
  a: 100,
  b: {
    x: 10,
    y: 20
  }
}

console.log(obj1 === obj2) // 直接比较是false
console.log(isEqual(obj1,obj2)) // true

9. 实现Ajax请求,使用Promise实现ajax

Ajax(asynchronous JavaScript and XML)是javascript的异步请求方式,从服务器xml中获取数据,不用刷新更新页面

function getAjax({url,method,data}) {
  const p = new Promise((reslove,reject) => {
    // 1. 创建xmlhttpRequest请求
    const xhr = new XMLHttpRequest();
    // 2. 初始化,创建http请求
    xhr.open(method, url);
    // 3. 监听函数
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          reslove(xhr.response)
        }
        else {
          reject(xhr.status)
        }
      }
    }
    // 4. 发送请求
    xhr.send(data);
  })

  return p
}

getAjax('GET','/api/categories').then(function (data) {
  console.log(data)
}).catch(function (status) {
  new Error(status)
})

10. 判断数据类型的方法

  1. typeof,可以判断出
  • 基本数据类型中的undefined,String,Boolean,Number,
  • 引用类型与Null为object;
  • 函数为function
  • typeof null === 'object'//true是因为在javascript初期版本,使用的32位系统存储,为了性能使用低位存储变量的类型信息:000——对象;010——浮点数;100——字符串;110——布尔;1——整数;null所有的机器码都是0,所以当成的对象处理;undefined是−2^30 整数来表示
console.log(getType(null)) // null
console.log(getType(undefined)) // undefined
console.log(getType('abc')) // string
console.log(getType(1)) // number
console.log(getType(true)) // boolean

console.log(getType({a: 'b'})) // object
console.log(getType([1,2])) // array
console.log(getType(new Function())) // function

console.log(getType(new Date())) // date
console.log(getType(new RegExp())) // regexp
console.log(getType(new Error())) // error
console.log(getType(document)) // htmldocument
console.log(getType(window)) // window
  1. instanceof,判断引用类型
  • instanceof用来判断A是否是B的实例
  • instanceof用来判断两个对象是否属于实例关系,不能判断属于那种类型
手写instanceof方法:
function myInstanceOf(left,right) {
  if (typeof left !== 'object' || left == null) {
    return false
  }
  let proto = left.__proto__; // 或者Object.getPrototypeOf(left)
  while (true) {
    if (proto === null) {
      return false;
    }
    if (proto === right.prototype) {
      return true;
    }
    
    proto = proto.__proto__;
  }
}

console.log(myInstanceOf([],Array)) // true
console.log(myInstanceOf([],Object)) // true
console.log(myInstanceOf('123',String)) // false
console.log([] instanceof Array) // true
console.log('123' instanceof String) // false
  1. constructor不能识别undefined,null
console.log([].constructor == Array);       //true
console.log({}.constructor == Object);      //true
console.log("string".constructor == String);  //true
console.log((123).constructor == Number);       //true
console.log(true.constructor == Boolean);       //true 
  1. Object.prototype.toString.call(),,可以判断所有类型
Object.prototype.toString.call(null)//"[object Null]"
Object.prototype.toString.call(undefined)//"[object Undefined]"
Object.prototype.toString.call('')//"[object String]"
Object.prototype.toString.call(false)//"[object Boolean]"
Object.prototype.toString.call(1)//"[object Number]"
Object.prototype.toString.call([])//"[object Array]"
Object.prototype.toString.call({})//"[object Object]"
手写判断类型:
function getType(value) {
  // 使用Object.prototype.toString.call判断出类型
  let type = Object.prototype.toString.call(value);
  // 得出是[object Null]等,会有[object ],所以从8开始,-1是到结尾
  // 截取类型,并转为小写
  let res = type.slice(8,-1).toLowerCase()
  return res;
}

console.log(getType(null)) // null
console.log(getType(undefined)) // undefined
console.log(getType('abc')) // string
console.log(getType(1)) // number
console.log(getType(true)) // boolean

console.log(getType({a: 'b'})) // object
console.log(getType([1,2])) // array
console.log(getType(new Function())) // function

console.log(getType(new Date())) // date
console.log(getType(new RegExp())) // regexp
console.log(getType(new Error())) // error
console.log(getType(document)) // htmldocument
console.log(getType(window)) // window

11. 手写new方法

new 是创建一个用户定义的对象类型的实例或者带有构造函数对象的实例

主要步骤:

  • 创建一个空对象{}
  • 设置原型,将对象的原型指向函数的 prototype 对象
  • 让函数的this指向对象,执行构造函数,传入参数的构造函数
  • 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
// fn 构造函数
// ...args 构造函数传入的参数
function myNew(fn, ...args) {
  if(typeof fn !== 'function'){
    console.error("type error")
    return
  }
  // 创建新的对象
  // 将对象的 __proto__ 属性指向构造函数的原型
  // let obj = {}
  // obj.__proto__ = fn.prototype
  // 相当于let obj = Object.create(myNew.prototype)
  // __proto__ 属性是一个有争议不建议使用的属性,我们使用 Object.create() 方法来代替。
  // Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
  let obj = Object.create(myNew.prototype)
  let res = fn.apply(obj, args)

  let isObject = res instanceof Object

  return isObject ? res : obj
}

function Person(name) {
  this.name = name;
  this.sayName = function () {
    console.log("name", this.name);
  };
}
const person = myNew(Person, "ayetongzhi");
console.log("person", person);
person.sayName();  // ayetongzhi

const person1 = new Person('ayetongzhi')
console.log("person1", person1);
person1.sayName();  // ayetongzhi

12. 手写object.create

new Object与平时的{}一样,都将属性添加到自身;Object.create()则是将属性添加到隐式原型__proto__([[prototype]])上;Object.create(null) 没有隐式原型;new Object(null)有隐式原型

Object.create(obj):创建一个新对象,使用现有的对象obj来提供新创建的对象的__proto__

  • 先创建一个临时的构造函数
  • 将传入的对象作为临时构造函数的原型
  • 返回临时构造函数的新实例
function myObject(obj) {
  function F(){};
  F.prototype = obj;
  return new F();
}

const obj = {
  a: 100,
  b: 200,
  sum() {
    return this.a + this.b
  }
}
const res = Object.create(obj)
console.log(res)
console.log(obj.isPrototypeOf(res)) // true, obj是否存在与res的原型链上

image.png

13. 手写promise,promise.then,promise.all.promis.race

1. 手写promise,promise.then
  • promise有三种状态,pending,fulfilled,rejected
// promise有三种状态,pending,rejected,fulfilled,有个状态值state
// 执行promise,传入函数,(resolve,reject)=>{}
// 返回一个值,可能是成功回调,也可能失败回调,resolveCallbacks,rejectCallbacks
// then(resolve(),reject())传入两个回调函数

class MyPromise {
  // promise状态,'pending','fulfilled','rejected'
  state = 'pending';
  // 成功后的值
  value = undefined;
  // 失败后的原因
  err = undefined;
  // pending状态下,存储成功的回调,fulfilled直接执行then
  resolveCallbacks = [];
  // pending状态下,存储失败的回调,rejected直接执行catch
  rejectCallbacks = [];

  constructor(fn) {
    const resolveHandler = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.resolveCallbacks.forEach(item => item(this.value)
        );
      }
    }
    const rejectHandler = (err) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.err = err;
        this.rejectCallbacks.forEach(fn => fn(this.err));
      }
    }

    try {
      fn(resolveHandler, rejectHandler);
    }
    catch(err) {
      rejectHandler(err);
    }
  }

  then(fn1, fn2) {
    // 判断是否是函数,catch调用第一个值传入的null
    typeof fn1 === 'function' ? fn1 : (v) => v;
    typeof fn2 === 'function' ? fn2 : (e) => e;

    // 当pending状态时,不知道接下来是成功还是失败,得存储到callbacks中
    if (this.state === 'pending') {
      const promise = new MyPromise((resolve, reject) => {
        this.resolveCallbacks.push(() => {
          try {
            const newValue = fn1(this.value);
            resolve(newValue);
          }
          catch(err) {
            reject(err);
          }
        })

        this.rejectCallbacks.push(() => {
          try {
            const newErr = fn2(this.err);
            reject(newErr);
          }
          catch(err) {
            reject(err);
          }
        })
      })
      return promise
    }

    // 如果是成功或失败,会立即执行
    if (this.state === 'fulfilled') {
      // then返回一个promise对象
      // 成功的话,执行resolve
      const promise = new MyPromise((resolve, reject) => {
        try {
          const newValue = fn1(this.value);
          resolve(newValue);
        }
        catch(err) {
          reject(err);
        }
      })
      return promise
    }

    if (this.state === 'rejected') {
      // then返回一个promise对象
      // 失败的话,执行reject
      const promise = new MyPromise((resolve, reject) => {
        try {
          const newErr = fn2(this.err);
          reject(newErr);
        }
        catch(err) {
          reject(err);
        }
      })
      return promise
    }

  }

  catch(fn) {
    return this.then(null, fn)
  }
}

const p = new MyPromise((resolve, reject) => {
  // resolve(100);
  setTimeout(() => {
    resolve(100);
  }, 500)
})
console.log(p)
const p1 = p.then(data1 => {
  console.log(data1)
  return data1 + 1
})
const p2 = p1.then(data2 => {
  console.log(data2)
  return data2 + 2
})
const p3 = p2.catch(err => {
  console.error(err)
})

image.png

2. promise.all

promise.all传入一个数组,返回一个promise

  • 传入参数,全部成功时才会成功,返回结果是成功组成的数组
  • 有一个失败,则失败,返回结果是失败的promise结果
function myPromiseAll(promiseArr) {
  const promise = new Promise((resolve,reject) => {
    // 先判断是不是数组
    if (!Array.isArray(promiseArr)) {
      throw new Error('传入的必须是数组')
    }

    let count = 0
    let arrNum = promiseArr.length
    let res = []
    for (let i = 0; i < arrNum; i++) {
      Promise.resolve(promiseArr[i]).then(value => {
        count++
        res[i] = value
        if (count === arrNum) {
          return resolve(res)
        }
      }).catch((err) => {
        return reject(err)
      })
    }
  })
  return promise
}

let pall1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
      resolve(1)
  }, 1000)
})
let pall2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
      resolve(2)
  }, 2000)
})
let pall3 = new Promise(function (resolve, reject) {
  setTimeout(function () {
      resolve(3)
  }, 3000)
})
myPromiseAll([pall3, pall1, pall2]).then(res => {
  console.log(res) // [3, 1, 2]
})
Promise.all([pall3, pall1, pall2]).then(res => {
  console.log(res) // [3, 1, 2]
})

3. promise.race
  • Promise.race传入数组,返回promise,
  • 只要有一个状态改变,最后结果立马就会改变
  • 比如有一个变为resolve,结果就是resolve,或者一个为reject,结果就是reject
function myPromiseRace (args) {
  const promise = new Promise((resolve, reject) => {
    let length = args.length
    for (let i = 0; i < length; i++) {
      Promise.resolve(args[i]).then(resolve, reject)
      // 相当于.then(res => {resolve(res)}) .catch(err => reject(err))
    }
  })
  return promise
}