关于前端:前端js手写题经常忘记录一下

5次阅读

共计 13214 个字符,预计需要花费 34 分钟才能阅读完成。

实现 forEach 办法

Array.prototype.myForEach = function(callback, context=window) {
  // this=>arr
  let self = this,  
      i = 0,
      len = self.length;

  for(;i<len;i++) {typeof callback == 'function' && callback.call(context,self[i], i)
   }
}

批改嵌套层级很深对象的 key

// 有一个嵌套档次很深的对象,key 都是 a_b 模式,须要改成 ab 的模式,留神不能用递归。const a = {
  a_y: {
    a_z: {y_x: 6},
    b_c: 1
  }
}
// {
//   ay: {
//     az: {
//       yx: 6
//     },
//     bc: 1
//   }
// }

办法 1:序列化 JSON.stringify + 正则匹配

const regularExpress = (obj) => {
  try {const str = JSON.stringify(obj).replace(/_/g, "");
    return JSON.parse(str);
  } catch (error) {return obj;}
};;

办法 2:递归

const recursion = (obj) => {const keys = Object.keys(obj);
  keys.forEach((key) => {const newKey = key.replace(/_/g, "");
    obj[newKey] = recursion(obj[key]);
    delete obj[key];
  });
  return obj;
};

数组中的数据依据 key 去重

给定一个任意数组,实现一个通用函数,让数组中的数据依据 key 排重:

const dedup = (data, getKey = () => {}) => {// todo}
let data = [{ id: 1, v: 1},
  {id: 2, v: 2},
  {id: 1, v: 1},
];

// 以 id 作为排重 key,执行函数失去后果
// data = [//   { id: 1, v: 1},
//   {id: 2, v: 2},
// ];

实现

const dedup = (data, getKey = () => {}) => {const dateMap = data.reduce((pre, cur) => {const key = getKey(cur)
        if (!pre[key]) {pre[key] = cur
        }
        return pre
    }, {})
    return Object.values(dateMap)
}

应用

let data = [{ id: 1, v: 1},
    {id: 2, v: 2},
    {id: 1, v: 1},
];
console.log(dedup(data, (item) => item.id))

// 以 id 作为排重 key,执行函数失去后果
// data = [//   { id: 1, v: 1},
//   {id: 2, v: 2},
// ];

前端手写面试题具体解答

实现节流函数(throttle)

节流函数原理: 指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的工夫才会执行回调函数。总结起来就是:事件,依照一段时间的距离来进行触发

像 dom 的拖拽,如果用消抖的话,就会呈现卡顿的感觉,因为只在进行的时候执行了一次,这个时候就应该用节流,在肯定工夫内屡次执行,会晦涩很多

手写简版

应用工夫戳的节流函数会在第一次触发事件时立刻执行,当前每过 wait 秒之后才执行一次,并且最初一次触发事件不会被执行

工夫戳形式:

// func 是用户传入须要防抖的函数
// wait 是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的工夫
  let lastTime = 0
  return function(...args) {
    // 以后工夫
    let now = +new Date()
    // 将以后工夫和上一次执行函数工夫比照
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(throttle(() => {console.log(1)
  }, 500),
  1
)

定时器形式:

应用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最初一次进行触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = null;
  returnfunction(){
    var context = this;
    var args = arguments;
    if(!timer){timer = setTimeout(function(){func.apply(context, args);
        timer = null;
      },delay);
    }
  }
}

实用场景:

  • DOM 元素的拖拽性能实现(mousemove
  • 搜寻联想(keyup
  • 计算鼠标挪动的间隔(mousemove
  • Canvas 模仿画板性能(mousemove
  • 监听滚动事件判断是否到页面底部主动加载更多
  • 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
  • 缩放场景:监控浏览器resize
  • 动画场景:防止短时间内屡次触发动画引起性能问题

总结

  • 函数防抖:将几次操作合并为一次操作进行。原理是保护一个计时器,规定在 delay 工夫后触发函数,然而在 delay 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
  • 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。

基于 Generator 函数实现 async/await 原理

外围:传递给我一个 Generator 函数,把函数中的内容基于 Iterator 迭代器的特点一步步的执行

function readFile(file) {
    return new Promise(resolve => {setTimeout(() => {resolve(file);
    }, 1000);
    })
};

function asyncFunc(generator) {const iterator = generator(); // 接下来要执行 next
  // data 为第一次执行之后的返回后果,用于传给第二次执行
  const next = (data) => {let { value, done} = iterator.next(data); // 第二次执行,并接管第一次的申请后果 data

    if (done) return; // 执行结束 (到第三次) 间接返回
    // 第一次执行 next 时,yield 返回的 promise 实例 赋值给了 value
    value.then(data => {next(data); // 当第一次 value 执行结束且胜利时,执行下一步(并把第一次的后果传递下一步)
    });
  }
  next();};

asyncFunc(function* () {
    // 生成器函数:控制代码一步步执行 
  let data = yield readFile('a.js'); // 等这一步骤执行执行胜利之后,再往下走,没执行完的时候,间接返回
  data = yield readFile(data + 'b.js');
  return data;
})

实现模板字符串解析性能

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄 18,性别 undefined
function render(template, data) {const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
  if (reg.test(template)) { // 判断模板里是否有模板字符串
    const name = reg.exec(template)[1]; // 查找以后模板里第一个模板字符串的字段
    template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
    return render(template, data); // 递归的渲染并返回渲染后的构造
  }
  return template; // 如果模板没有模板字符串间接返回
}

AJAX

const getJSON = function(url) {return new Promise((resolve, reject) => {const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', '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();})
}

实现双向数据绑定

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

实现数组的扁平化

(1)递归实现

一般的递归思路很容易了解,就是通过循环递归的形式,一项一项地去遍历,如果每一项还是一个数组,那么就持续往下遍历,利用递归程序的办法,来实现数组的每一项的连贯:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {let result = [];

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

(2)reduce 函数迭代

从下面一般的递归函数中能够看出,其实就是对数组的每一项进行解决,那么其实也能够用 reduce 来实现数组的拼接,从而简化第一种办法的代码,革新后的代码如下所示:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {return arr.reduce(function(prev, next){return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]

(3)扩大运算符实现

这个办法的实现,采纳了扩大运算符和 some 的办法,两者独特应用,达到数组扁平化的目标:

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

(4)split 和 toString

能够通过 split 和 toString 两个办法来独特实现数组扁平化,因为数组会默认带一个 toString 的办法,所以能够把数组间接转换成逗号分隔的字符串,而后再用 split 办法把字符串从新转换为数组,如上面的代码所示:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {return arr.toString().split(',');
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

通过这两个办法能够将多维数组间接转换成逗号连贯的字符串,而后再从新分隔成数组。

(5)ES6 中的 flat

咱们还能够间接调用 ES6 中的 flat 办法来实现数组扁平化。flat 办法的语法:arr.flat([depth])

其中 depth 是 flat 的参数,depth 是能够传递数组的开展深度(默认不填、数值是 1),即开展一层数组。如果层数不确定,参数能够传进 Infinity,代表不管多少层都要开展:

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

能够看出,一个嵌套了两层的数组,通过将 flat 办法的参数设置为 Infinity,达到了咱们预期的成果。其实同样也能够设置成 2,也能实现这样的成果。在编程过程中,如果数组的嵌套层数不确定,最好间接应用 Infinity,能够达到扁平化。(6)正则和 JSON 办法 在第 4 种办法中曾经应用 toString 办法,其中依然采纳了将 JSON.stringify 的办法先转换为字符串,而后通过正则表达式过滤掉字符串中的数组的方括号,最初再利用 JSON.parse 把它转换成数组:

let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {let str = JSON.stringify(arr);
  str = str.replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  return JSON.parse(str); 
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

实现 apply 办法

思路: 利用 this 的上下文个性。apply其实就是改一下参数的问题

Function.prototype.myApply = function(context = window, args) {
  // this-->func  context--> obj  args--> 传递过去的参数

  // 在 context 上加一个惟一值不影响 context 上的属性
  let key = Symbol('key')
  context[key] = this; // context 为调用的上下文,this 此处为函数,将这个函数作为 context 的办法
  // let args = [...arguments].slice(1)   // 第一个参数为 obj 所以删除, 伪数组转为数组

  let result = context[key](...args); // 这里和 call 传参不一样

  // 革除定义的 this 不删除会导致 context 属性越来越多
  delete context[key]; 

  // 返回后果
  return result;
}
// 应用
function f(a,b){console.log(a,b)
 console.log(this.name)
}
let obj={name:'张三'}
f.myApply(obj,[1,2])  //arguments[1]

数组去重办法汇总

首先: 我晓得多少种去重形式

1. 双层 for 循环

function distinct(arr) {for (let i=0, len=arr.length; i<len; i++) {for (let j=i+1; j<len; j++) {if (arr[i] == arr[j]) {arr.splice(j, 1);
                // splice 会扭转数组长度,所以要将数组长度 len 和下标 j 减一
                len--;
                j--;
            }
        }
    }
    return arr;
}

思维: 双重 for 循环是比拟蠢笨的办法,它实现的原理很简略:先定义一个蕴含原始数组第一个元素的数组,而后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不反复则增加到新数组中,最初返回新数组;因为它的工夫复杂度是O(n^2),如果数组长度很大,效率会很低

2. Array.filter() 加 indexOf/includes

function distinct(a, b) {let arr = a.concat(b);
    return arr.filter((item, index)=> {//return arr.indexOf(item) === index
        return arr.includes(item)
    })
}

思维: 利用 indexOf 检测元素在数组中第一次呈现的地位是否和元素当初的地位相等,如果不等则阐明该元素是反复元素

3. ES6 中的 Set 去重

function distinct(array) {return Array.from(new Set(array));
}

思维: ES6 提供了新的数据结构 Set,Set 构造的一个个性就是成员值都是惟一的,没有反复的值。

4. reduce 实现对象数组去反复

var resources = [{ name: "张三", age: "18"},
    {name: "张三", age: "19"},
    {name: "张三", age: "20"},
    {name: "李四", age: "19"},
    {name: "王五", age: "20"},
    {name: "赵六", age: "21"}
]
var temp = {};
resources = resources.reduce((prev, curv) => {
 // 如果长期对象中有这个名字,什么都不做
 if (temp[curv.name]) { }else {
    // 如果长期对象没有就把这个名字加进去,同时把以后的这个对象退出到 prev 中
    temp[curv.name] = true;
    prev.push(curv);
 }
 return prev
}, []);
console.log("后果", resources);

这种办法是利用高阶函数 reduce 进行去重,这里只须要留神 initialValue 得放一个空数组[],不然没法push

实现 add(1)(2)(3)

函数柯里化概念:柯里化(Currying)是把承受多个参数的函数转变为承受一个繁多参数的函数,并且返回承受余下的参数且返回后果的新函数的技术。

1)粗犷版

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

2)柯里化解决方案

  • 参数长度固定
var add = function (m) {var temp = function (n) {return add(m + n);
  }
  temp.toString = function () {return m;}
  return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43

对于 add(3)(4)(5),其执行过程如下:

  1. 先执行 add(3),此时 m =3,并且返回 temp 函数;
  2. 执行 temp(4),这个函数内执行 add(m+n),n 是此次传进来的数值 4,m 值还是上一步中的 3,所以 add(m+n)=add(3+4)=add(7),此时 m =7,并且返回 temp 函数
  3. 执行 temp(5),这个函数内执行 add(m+n),n 是此次传进来的数值 5,m 值还是上一步中的 7,所以 add(m+n)=add(7+5)=add(12),此时 m =12,并且返回 temp 函数
  4. 因为前面没有传入参数,等于返回的 temp 函数不被执行而是打印,理解 JS 的敌人都晓得对象的 toString 是批改对象转换字符串的办法,因而代码中 temp 函数的 toString 函数 return m 值,而 m 值是最初一步执行函数时的值 m =12,所以返回值是 12。
  5. 参数长度不固定
function add (...args) {
    // 求和
    return args.reduce((a, b) => a + b)
}
function currying (fn) {let args = []
    return function temp (...newArgs) {if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return temp
        } else {let val = fn.apply(this, args)
            args = [] // 保障再次调用时清空
            return val
        }
    }
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)())  //15
console.log(addCurry(1)(2)(3, 4, 5)())  //15
console.log(addCurry(1)(2, 3, 4, 5)())  //15

实现 some 办法

Array.prototype.mySome=function(callback, context = window){
             var len = this.length,
                 flag=false,
           i = 0;

             for(;i < len; i++){if(callback.apply(context, [this[i], i , this])){
                    flag=true;
                    break;
                } 
             }
             return flag;
        }

        // var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)
        // console.log(flag);

应用 setTimeout 实现 setInterval

setInterval 的作用是每隔一段指定工夫执行一个函数,然而这个执行不是真的到了工夫立刻执行,它真正的作用是每隔一段时间将事件退出事件队列中去,只有当以后的执行栈为空的时候,能力去从事件队列中取出事件执行。所以可能会呈现这样的状况,就是以后执行栈执行的工夫很长,导致事件队列里边积攒多个定时器退出的事件,当执行栈完结的时候,这些事件会顺次执行,因而就不能到距离一段时间执行的成果。

针对 setInterval 的这个毛病,咱们能够应用 setTimeout 递归调用来模仿 setInterval,这样咱们就确保了只有一个事件完结了,咱们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

实现思路是应用递归函数,一直地去执行 setTimeout 从而达到 setInterval 的成果

function mySetInterval(fn, timeout) {
  // 控制器,管制定时器是否继续执行
  var timer = {flag: true};
  // 设置递归函数,模仿定时器执行。function interval() {if (timer.flag) {fn();
      setTimeout(interval, timeout);
    }
  }
  // 启动定时器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

实现 new 的过程

new 操作符做了这些事:

  • 创立一个全新的对象
  • 这个对象的 __proto__ 要指向构造函数的原型 prototype
  • 执行构造函数,应用 call/apply 扭转 this 的指向
  • 返回值为 object 类型则作为 new 办法的返回值返回,否则返回上述全新对象
function myNew(fn, ...args) {
  // 基于原型链 创立一个新对象
  let newObj = Object.create(fn.prototype);
  // 增加属性到新对象上 并获取 obj 函数的后果
  let res = fn.apply(newObj, args); // 扭转 this 指向

  // 如果执行后果有返回值并且是一个对象, 返回执行的后果, 否则, 返回新创建的对象
  return typeof res === 'object' ? res: newObj;
}
// 用法
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {console.log(this.age);
};
let p1 = myNew(Person, "poety", 18);
console.log(p1.name);
console.log(p1);
p1.say();

实现 apply 办法

apply 原理与 call 很类似,不多赘述

// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
  context.fn = this;

  var result;
  if (!arr) {result = context.fn();
  } else {var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }

  delete context.fn;
  return result;
};

实现防抖函数(debounce)

防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则从新计时。

那么与节流函数的区别间接看这个动画实现即可。

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {clearTimeout(timer);
    timer = setTimeout(() => {fn.apply(this, args);
    }, delay);
  };
};

实用场景:

  • 按钮提交场景:避免屡次提交按钮,只执行最初提交的一次
  • 服务端验证场景:表单验证须要服务端配合,只执行一段间断的输出事件的最初一次,还有搜寻联想词性能相似

生存环境请用 lodash.debounce

实现 call 办法

call 做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模仿 call bar.mycall(null);
// 实现一个 call 办法:// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()
Function.prototype.myCall = function(context = window, ...args) {if (typeof this !== "function") {throw new Error('type error')
  }
  // this-->func  context--> obj  args--> 传递过去的参数

  // 在 context 上加一个惟一值不影响 context 上的属性
  let key = Symbol('key')
  context[key] = this; // context 为调用的上下文,this 此处为函数,将这个函数作为 context 的办法
  // let args = [...arguments].slice(1)   // 第一个参数为 obj 所以删除, 伪数组转为数组

  // 绑定参数 并执行函数
  let result = context[key](...args);
  // 革除定义的 this 不删除会导致 context 属性越来越多
  delete context[key];

  // 返回后果 
  return result;
};
// 用法:f.call(obj,arg1)
function f(a,b){console.log(a+b)
 console.log(this.name)
}
let obj={name:1}
f.myCall(obj,1,2) // 否则 this 指向 window

setTimeout 与 setInterval 实现

setTimeout 模仿实现 setInterval

题目形容: setInterval 用来实现循环定时调用 可能会存在肯定的问题 能用 setTimeout 解决吗

实现代码如下:

function mySetInterval(fn, t) {
  let timerId = null;
  function interval() {fn();
    timerId = setTimeout(interval, t); // 递归调用
  }
  timerId = setTimeout(interval, t); // 首次调用
  return {
    // 利用闭包的个性 保留 timerId
    cancel:() => {clearTimeout(timerId)
    }
  }
}
// 测试
var a = mySetInterval(()=>{console.log(111);
},1000)
var b = mySetInterval(() => {console.log(222)
}, 1000)

// 终止定时器
a.cancel()
b.cancel()

为什么要用 setTimeout 模仿实现 setIntervalsetInterval 的缺点是什么?

setInterval(fn(), N);

下面这句代码的意思其实是 fn() 将会在 N 秒之后被推入工作队列。在 setInterval 被推入工作队列时,如果在它后面有很多工作或者某个工作等待时间较长比方网络申请等,那么这个定时器的执行工夫和咱们预约它执行的工夫可能并不统一

// 最常见的呈现的就是,当咱们须要应用 ajax 轮询服务器是否有新数据时,必定会有一些人会应用 setInterval,然而无论网络情况如何,它都会去一遍又一遍的发送申请,最初的间隔时间可能和原定的工夫有很大的出入

// 做一个网络轮询,每一秒查问一次数据。let startTime = new Date().getTime();
let count = 0;

setInterval(() => {
    let i = 0;
    while (i++ < 10000000); // 假如的网络提早
    count++;
    console.log(
        "与原设定的距离时差了:",
        new Date().getTime() - (startTime + count * 1000),
        "毫秒"
    );
}, 1000)

// 输入:// 与原设定的距离时差了:567 毫秒
// 与原设定的距离时差了:552 毫秒
// 与原设定的距离时差了:563 毫秒
// 与原设定的距离时差了:554 毫秒(2 次)
// 与原设定的距离时差了:564 毫秒
// 与原设定的距离时差了:602 毫秒
// 与原设定的距离时差了:573 毫秒
// 与原设定的距离时差了:633 毫秒

再次强调,定时器指定的工夫距离,示意的是何时将定时器的代码增加到音讯队列,而不是何时执行代码。所以真正何时执行代码的工夫是不能保障的,取决于何时被主线程的事件循环取到,并执行。

setInterval(function, N)
// 即:每隔 N 秒把 function 事件推到音讯队列中

上图可见,setInterval 每隔 100ms 往队列中增加一个事件;100ms 后,增加 T1 定时器代码至队列中,主线程中还有工作在执行,所以期待,some event 执行完结后执行 T1定时器代码;又过了 100msT2 定时器被增加到队列中,主线程还在执行 T1 代码,所以期待;又过了 100ms,实践上又要往队列里推一个定时器代码,但因为此时 T2 还在队列中,所以 T3 不会被增加(T3 被跳过),后果就是此时被跳过;这里咱们能够看到,T1 定时器执行完结后马上执行了 T2 代码,所以并没有达到定时器的成果

setInterval 有两个毛病

  • 应用 setInterval 时,某些距离会被跳过
  • 可能多个定时器会间断执行

能够这么了解 :每个setTimeout 产生的工作会间接 push 到工作队列中;而 setInterval 在每次把工作 push 到工作队列前,都要进行一下判断 (看上次的工作是否仍在队列中)。因此咱们个别用setTimeout 模仿setInterval,来躲避掉下面的毛病

setInterval 模仿实现 setTimeout

const mySetTimeout = (fn, t) => {const timer = setInterval(() => {clearInterval(timer);
    fn();}, t);
};
// 测试
// mySetTimeout(()=>{//   console.log(1);
// },1000)

验证是否是身份证

function isCardNo(number) {var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
    return regx.test(number);
}

正文完
 0