前端手写

240 阅读8分钟

数据类型判断

typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。

function typeOf(obj) {
    let temp = Object.prototype.toString.call(obj).split(' ')[1]
    return temp.slice(0, temp.length-1).toLowerCase()
}
console.log(typeOf([]))  //array
console.log(typeOf({}))  //object
console.log(typeOf(new Date)) //date

 手写 instanceof 方法

function myInstanceof(left, right) { 
    let proto = Object.getPrototypeOf(left), // 获取对象的原型
    prototype = right.prototype;// 获取构造函数的 prototype 对象 
    // 判断构造函数的 prototype 对象是否在对象的原型链上
        while (true) { 
        if (!proto) return false;
        if (proto === prototype) return true;
        proto = Object.getPrototypeOf(proto);
        }
}


es6手写promise

//初始结构 this指向 then函数  执行异常  异步(promise.then中的异步和promise加了setTimeout的异步以及promise本身resolve、reject的异步) 回调保存 链式
class Commitment {
    static PENDING = '待定'; FULFILLED = '成功'; REJECT = '拒绝'
    constructor (func) {
      //初始状态为待定状态
      this.status = Commitment.PENDING
      //调用resolve和reject传过来的值
      this.result = null
      this.resolveCallbacks = []
      this.rejectCallbacks = []
      //promise的两个参数  (因为在外部调用,所以将构造函数中的this指向它)
      //处理错误
      try{
         func(this.resolve.bind(this), this.reject.bind(this))
      } catch (error) {
         //把错误传递给reject
         this.reject(error)
        }
    }
     //定义这两个函数
     resolve (result) {
        setTimeout(()=>{
          if(this.status == Commitment.PENDING) {
          this.status = Commitment.FULFILLED
          //将传过来的参数赋值给this.result
          this.result = result
          //待定状态的处理
          this.resolveCallbacks.forEach((callback)=>{
          callback(result)
          })
       }
        })
     }
     reject (result) {
        setTimeout(()=> {
         if(this.status == Commitment.PENDING) {
          this.status = Commitment.REJECT
          this.result = result
          //待定状态的处理
          this.rejectCallbacks.forEach((callback)=>{
          callback(result)
       }
        })
     }
     //链式功能
     return new Commitment((resolve, reject)=>{
       //then方法 (接收两个函数, 要判断执行哪个)
      then (onFULFILLED, onREJECT) {
           onFULFILLED = typeof onFULFILLED == 'function' ? onFULFILLED : () =>{}
           onREJECT = typeof onREJECT == 'function' ? onREJECT : () =>{}
           // 处理在promise中加了setTimeout的时候的待定状态
           if(this.status == Commitment.PENDING) {
            this.resolveCallbacks.push(onFULFILLED)
            this.rejectCallbacks.push(onREJECT)
           }
           if(this.status == Commitment.FULFILLED) {
           //处理commitment.then中两个函数的异步操作(加个setTimeout())
           setTimeout(() => {
             //成功时执行resolve,并将前面result保留的数据传递过去
             onFULFILLED(this.result)
           })
         }
         if(this.status == Commitment.REJECT) {
         setTimeout(() =>{
              //成功时执行resolve,并将前面result保留的数据传递过去
              onREJECT(this.result)
           })
         }
      }
     }) 
}
let commitment = new Commitment((resolve, reject)=>{
    //在这里面处理异常
    // throw new Error('出错了')
    // 这里加setTimeout  这个时候还没有执行setTimeout中的resolve和reject就执行then方法了
    // 所以要在上面then中处理状态为待定状态的操作
    setTimeout(()=>{
    //这里的resolve和reject是异步的,要加上setTimeout
       resolve()
       console.log('666')
    })
})
commitment.then(
    //处理这里不传入函数
    result => {console.log(result)}
    reject => {console.log(rejecr)}
)

防抖

//n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
<input type= 'submit' id='input'>
var btn = document.getElementById('input')
btn.addEventListener('click', debounce(submit,wait), false)

function submit () {
   console.log('提交了')
}
//清空时间
function debounce (fn,wait) {
   let timer = null
   //点击事件会给submit传参
   return function () {
      if (timer) clearTimeout(timer)
      timer = setTimeout(()=> {
      //想要在定义的事件函数中获取到参数,所以要在这里传过去 这里的this指向btn
      fn.apply(this,arguments)
      }, wait)
   }  
}

节流

//函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
//判断时间 
//时间戳
function throttle (fn,delay) {
   let begin = 0
   //点击事件会给submit传参
   return function () {
   var cur = Date.now()
     if (cur-begin> delay) {
      fn.apply(this,arguments)
      begin = cur
     }
   }  
}
//定时器
function throttle(fn, delay){
     // 设置一个触发开关
     let canUse = true
     return function(){
     //如果为true,就触发技能,否则就不能触发
        if(!canUse) return
        canUse = false
        setTimeout(()=>{
          fn.apply(this, arguments);
          canUse = true;
        }, delay)
     }
 }

手写call函数

  • 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文对象的一个属性。
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性。
  • 返回结果
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
     return console.error('类型错误')
  }
  //判断context是否传入,没有传入则设置为window
  context = context || window
  //获取参数
  let args = [...arguments].slice(1), result = null
  context.fn = this
  result = context.fn(...args);
  delete context.fn
  return result
}

手写apply函数

  • 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  • 将函数作为上下文对象的一个属性。
  • 判断参数值是否传入
  • 使用上下文对象来调用这个方法,并保存返回结果。
  • 删除刚才新增的属性
  • 返回结果
Function.prototype.mgApply = function (context) {
  if (typeof this !== 'function') {
   return consoel.error('类型错误')
  }
  context = context || window
  let result = null
  context.fn = this
  if (arguments[1]) {
  //arguments可以通过下标拿
      result = context.fn(...arguments[1])
   } else {
      result = context.fn();
   }
   delete context.fn
   return result
}

手写bind函数

  • 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  • 保存当前函数的引用,获取其余传入参数值。
  • 创建一个函数返回
  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
     return console.error('类型错误')
     }
  //拿参数 和call一样
  let args = [...arguments].slice(1)],fn = this
   return function Fn() {
   // 如果被new调用,this应该是fn的实例
      return fn.apply(this instanceof Fn ? this : context, args)
  }
}

浅拷贝

(1)Object.assign()

该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。

let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);
console.log(target); // {a: 1, b: 2, c: 3}

(2)扩展运算符

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

(3)数组方法实现数组浅拷贝

1)Array.prototype.slice

  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
let arr = [1,2,3,4]
console.log(arr.slice())//[1,2,3,4]

2)Array.prototype.concat

  • 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
let arr = [1,2,3,4]; 
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

深拷贝

(1)JSON.stringify()

  • 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。
let obj1 = { a: 0, b: { c: 0 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}

(2)函数库lodash的_.cloneDeep方法

var _ = require('lodash'); 
var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] };
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

(3)手写实现深拷贝函数

function deepClone(object) {
 if (!object || typeof object !== 'object') return
 let newObject = Array.isArray(object) ? [] : {}
 for (let key in object) {
 //这里要判断是不是这一层的属性,有可能下一层也有这个属性,所以要在这里判断一下
   if(object.hasownProperty(key)) {
     newObject[key] = typeof object[key] == 'object' ?  deepClone(object[key]) : object[key]
   }
 }
 return newObject
}

实现数组的扁平化

(1)递归实现

let arr = [1,[2,[3,4,5]]]
function flat(arr) {
  let result = []
  for(var i =0;i<arr.length;i++) {
    if(Array.isArray(arr[i])){
      result = result.concat(flatten(arr[i]))
    }else {
     result.push(arr[i])
    }
  }
  return result
}
flat(arr)

(2)ES6 中的 flat

let arr = [1, [2, [3, 4]]];
function flat (arr) {
  return arr.flat(Infinity)
}
console.log(flat(arr))

(3)some实现

let arr = [1, [2, [3, 4]]];
function flat(arr) {
 while(arr.some((item)=>{Array.isArray(item)})) {
   arr = [].concat(...arr)
 }
 return arr
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]

 实现数组的flat方法

function flat (arr, depth) {
 if(!Array.isArray(arr) || depth<=0) return arr
 return arr.reduce((prev, next) => {
    if(Array.isArray(next)) {
     return prev.concat(flat(next, depth - 1))
    }else {
    //注意这里是concat不是push
     return prev.concat(next)
    }
 },[])
}

 实现数组的push方法

//返回长度
let arr= []
Array.prototype.push = function (){
  for(var i = 0;i<arguments.length;i++) {
    this[this.length] = arguments[i]
  }
  return this.length
}

 实现数组的map方法

Array.prototype._map = function(fn) { 
if (typeof fn !== "function") {
  throw Error('参数必须是一个函数'); 
 }
 const res = [];
 for (let i = 0, len = this.length; i < len; i++) {
 res.push(fn(this[i]));
 }
 return res;
 }

 实现数组的filter方法

Array.prototype.filter = function(fn) {
 if(typeof fn !== 'function') return new Erroe('参数必须是一个函数')
 let res = []
 for(var i = 0;i<this.length;i++) {
 //filter返回成功的那个元素所以要执行返回true后再push
    fn(this[i]) && res.push(this[i])
 }
 return res
}

 实现数组的reduce方法

Array.prototype.reduce = function(fn, init) {
    var total =  init || arr[0]   // 有初始值使用初始值
    // 有初始值的话从0遍历, 否则从1遍历
    for (var i = init ? 0 : 1; i < this.length; i++) {
    //reduce中函数的参数接受4个值
        total = fn(total, this[i], i , this)
    } 
    return total
}
var arr = [1,2,3]
console.log(arr.reduce((prev, item) => prev + item, 10))

 实现数组的every方法

let arr= [1,2,3]
Array.prototype.every = function (fn) {
if(typeof fn!== 'function') return new Error('参数必须是函数')
for(var i =0;i<this.length;i++) {
  if (!fn(this[i])) { return false}
}
return true
}
let my = arr.every((item) => {
 return item>1
})
console.log(my)

 实现数组的some方法

let arr= [1,2,3]
Array.prototype.some = function (fn) {
if(typeof fn!== 'function') return new Error('参数必须是函数')
for(var i =0;i<this.length;i++) {
//some是返回true,而find是返回item,findIndex是返回i
  if (fn(this[i])) { return true}
}
//some是返回false,而find和findIndex是返回undefined
  return false

}
let my = arr.some((item) => {
 return item>1
})
console.log(my)

 实现数组的forEach方法

forEach里面如果修改基本数据类型要通过arr[index]的方式,因为保存的值,可以修改引用数据类型 forEach如果没有第二个参数默认this指向widow,如果有则指向这个对象

参考 深入理解关于forEach、map等循环方法无法修改当前遍历值 - 掘金 (juejin.cn)

let arr = ['a', 'b', 'c', 'd', 'e', 'f', 100]
Array.prototype.myForeach = function (fn) {
    //    如果不是函数 抛异常
    if (!Object.prototype.toString.call(fn) == '[object Function]') {
        throw new Error(`${fn} is no a function`)
    }
    let param = this
    for (var i = 0; i < param.length; i++) {
        fn(param[i], i, param)
     }
    }
      arr.myForeach((item, index, param) => {
    console.log(item, index, param)
    return (arr[index])
     })
     console.log(arr)

 实现数组去重

ES6方法(使用数据结构集合)

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
console.log([...new Set(array)])

ES5方法:使用map存储不重复的数字

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
function uniqueArr(arr) {
  let result = [],map = {}
  for(var i =0;i<arr.length;i++) {
    if(!map[arr[i]]) {
       map[arr[i]] =1
    }
  }
    console.log(map)
  for(var j in map) {
   if (map[j]) {
    result.push(j*1)
    }
  }
    return result
}
console.log(uniqueArr(array))

大数相加

function sumBigNumber(a, b) {
  let res = '';
  let temp = 0;
  a = a.split('');
  b = b.split('');
  //[] || [] 为true
  while (a.length || b.length || temp) {
    //   [].pop()为undefined ~NaN为-1 ~-1位0  对计算没有影响
    temp += ~~a.pop() + ~~b.pop();
    res = (temp % 10) + res;
    // 判断是否有进位
    temp  = temp > 9
  }
  return res.replace(/^0+/, '');
}
sumBigNumber('546546544665464645','5646465446546')

函数柯里化add(1)(2)(3)

1.直接写

function add(a){
  return function(b){
    return function (c){
      return a+b+c
       }
    }
  }
console.log(add(1)(2)(3))

2.封装一个公用的

function add(a,b,c){
  return a+b+c
  }
function curry(fn) {
//获取fn参个数
let len = fn.length
 return function temp(){
  let args = Array.prototype.slice.call(arguments)
  if (args.length >= len) {
    return fn(...args)
  } else {
    return function () {
    return temp(...args,...arguments)
    }
  }
 }
 }
 let r = curry(add)
 console.log(r(9)(2)(3))

将js对象转化为树形结构

// 转换前:
source = [{
            id: 1,
            pid: 0,
            name: 'body' 
          }, {
             id: 2,
             pid: 1,
             name: 'title'
             }, { 
             id: 3,
             pid: 2,
             name: 'div'
          }]
// 转换为:
tree = [{ 
            id: 1,
            pid: 0,
            name: 'body',
            children: [{
                id: 2,
                pid: 1,
                name: 'title',
                children: [{
                    id: 3,
                    pid: 1,
                    name: 'div'
                }]
             }
        }]
代码实现:
function jsonToTree(data) {
  // 初始化结果数组,并判断输入数据的格式
  let result = []
  if(!Array.isArray(data)) {
    return result
  }
  // 使用map,将当前对象的id与当前对象对应存储起来
  let map = {};

  data.forEach(item => {
    map[item.id] = item;
  });
  data.forEach(item => {
  //根据元数据的pid得到父元素
    let parent = map[item.pid];
    if(parent) {
      (parent.children = []).push(item);
    } else {
      result.push(item);
      console.log(result)
    }
  });
  return result;
}
console.log(jsonToTree([{
            id: 1,
            pid: 0,
            name: 'body'
          }, {
            id: 2,
            pid: 1,
            name: 'title'
          }, {
            id: 3,
            pid: 2,
            name: 'div'
          }]
),'00')

用Promise实现图片的异步加载

function imageAsync(url) {
 return new Promise((resolve,reject) => {
  let img = new Image()
  img.src = url
  img.onload = () => {
   console.log('图片加载成功')
   resolve()
  }
  img.onerror = () =>{
  console.log('图片加载失败')
  reject()
  }
 })
}
imageAsync(url).then(
()=>{console.log('加载成功')},
()=>{console.log('加载失败')}
)

### 实现双向数据绑定

Object.defineProperty方式
js
let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
  get() {
    console.log('获取数据了')
  },
  set(newVal) {
    console.log('数据更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})