写点什么

美团前端二面必会手写面试题汇总

  • 2022 年 10 月 10 日
    浙江
  • 本文字数:24372 字

    阅读完需:约 80 分钟

请实现一个 add 函数,满足以下功能

add(1);             // 1add(1)(2);      // 3add(1)(2)(3);// 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
复制代码


function add(...args) {  // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值  let fn = function(...newArgs) {   return add.apply(null, args.concat(newArgs))  }
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 fn.toString = function() { return args.reduce((total,curr)=> total + curr) }
return fn}
复制代码


考点:


  • 使用闭包, 同时要对 JavaScript 的作用域链(原型链)有深入的理解

  • 重写函数的 toSting()方法


// 测试,调用toString方法触发求值
add(1).toString(); // 1add(1)(2).toString(); // 3add(1)(2)(3).toString();// 6add(1)(2, 3).toString(); // 6add(1, 2)(3).toString(); // 6add(1, 2, 3).toString(); // 6
复制代码

数组去重方法汇总

首先:我知道多少种去重方式


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

实现观察者模式

观察者模式(基于发布订阅模式) 有观察者,也有被观察者


观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了 内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者


class Subject { // 被观察者 学生  constructor(name) {    this.state = 'happy'    this.observers = []; // 存储所有的观察者  }  // 收集所有的观察者  attach(o){ // Subject. prototype. attch    this.observers.push(o)  }  // 更新被观察者 状态的方法  setState(newState) {    this.state = newState; // 更新状态    // this 指被观察者 学生    this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态  }}
class Observer{ // 观察者 父母和老师 constructor(name) { this.name = name } update(student) { console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state) }}
let student = new Subject('学生');
let parent = new Observer('父母'); let teacher = new Observer('老师');
// 被观察者存储观察者的前提,需要先接纳观察者student. attach(parent); student. attach(teacher); student. setState('被欺负了');
复制代码

实现一个链表结构

链表结构



看图理解 next 层级



// 链表 从头尾删除、增加 性能比较好// 分为很多类 常用单向链表、双向链表
// js模拟链表结构:增删改查
// node节点class Node { constructor(element,next) { this.element = element this.next = next } }
class LinkedList { constructor() { this.head = null // 默认应该指向第一个节点 this.size = 0 // 通过这个长度可以遍历这个链表 } // 增加O(n) add(index,element) { if(arguments.length === 1) { // 向末尾添加 element = index // 当前元素等于传递的第一项 index = this.size // 索引指向最后一个元素 } if(index < 0 || index > this.size) { throw new Error('添加的索引不正常') } if(index === 0) { // 直接找到头部 把头部改掉 性能更好 let head = this.head this.head = new Node(element,head) } else { // 获取当前头指针 let current = this.head // 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值 for (let i = 0; i < index-1; i++) { // 找到它的前一个 current = current.next } // 让创建的元素指向上一个元素的下一个 // 看图理解next层级 current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next }
this.size++; } // 删除O(n) remove(index) { if(index < 0 || index >= this.size) { throw new Error('删除的索引不正常') } this.size-- if(index === 0) { let head = this.head this.head = this.head.next // 移动指针位置
return head // 返回删除的元素 }else { let current = this.head for (let i = 0; i < index-1; i++) { // index-1找到它的前一个 current = current.next } let returnVal = current.next // 返回删除的元素 // 找到待删除的指针的上一个 current.next.next // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可 current.next = current.next.next
return returnVal } } // 查找O(n) get(index) { if(index < 0 || index >= this.size) { throw new Error('查找的索引不正常') } let current = this.head for (let i = 0; i < index; i++) { current = current.next } return current }}

var ll = new LinkedList()
ll.add(0,100) // Node { ellement: 100, next: null }ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }ll.add(300)ll.remove(0)
console.log(ll.get(2),'get')console.log(ll.head)
module.exports = LinkedList
复制代码


参考前端手写面试题详细解答

怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)

第一版 时间复杂度为 O(n^2)


function getTenNum(testArray, n) {  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * testArray.length);    const cur = testArray[random];    if (result.includes(cur)) {      i--;      break;    }    result.push(cur);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码


第二版 标记法 / 自定义属性法 时间复杂度为 O(n)


function getTenNum(testArray, n) {  let hash = {};  let result = [];  let ranNum = n;  while (ranNum > 0) {    const ran = Math.floor(Math.random() * testArray.length);    if (!hash[ran]) {      hash[ran] = true;      result.push(ran);      ranNum--;    }  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码


第三版 交换法 时间复杂度为 O(n)


function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; i++) {    debugger;    const ran = Math.floor(Math.random() * (cloneArr.length - i));    result.push(cloneArr[ran]);    cloneArr[ran] = cloneArr[cloneArr.length - i - 1];  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码


值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见


最终版 边遍历边删除 时间复杂度为 O(n)


function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * cloneArr.length);    const cur = cloneArr[random];    result.push(cur);    cloneArr.splice(random, 1);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码

实现一下 hash 路由

基础的html代码:


<html>  <style>    html, body {      margin: 0;      height: 100%;    }    ul {      list-style: none;      margin: 0;      padding: 0;      display: flex;      justify-content: center;    }    .box {      width: 100%;      height: 100%;      background-color: red;    }  </style>  <body>  <ul>    <li>      <a href="#red">红色</a>    </li>    <li>      <a href="#green">绿色</a>    </li>    <li>      <a href="#purple">紫色</a>    </li>  </ul>  </body></html>
复制代码


简单实现:


<script>  const box = document.getElementsByClassName('box')[0];  const hash = location.hash  window.onhashchange = function (e) {    const color = hash.slice(1)    box.style.background = color  }</script>
复制代码


封装成一个 class:


<script>  const box = document.getElementsByClassName('box')[0];  const hash = location.hash  class HashRouter {    constructor (hashStr, cb) {      this.hashStr = hashStr      this.cb = cb      this.watchHash()      this.watch = this.watchHash.bind(this)      window.addEventListener('hashchange', this.watch)    }    watchHash () {      let hash = window.location.hash.slice(1)      this.hashStr = hash      this.cb(hash)    }  }  new HashRouter('red', (color) => {    box.style.background = color  })</script>
复制代码

实现 redux 中间件

简单实现


function createStore(reducer) {  let currentState  let listeners = []
function getState() { return currentState }
function dispatch(action) { currentState = reducer(currentState, action) listeners.map(listener => { listener() }) return action }
function subscribe(cb) { listeners.push(cb) return () => {} }
dispatch({type: 'ZZZZZZZZZZ'})
return { getState, dispatch, subscribe }}
// 应用实例如下:function reducer(state = 0, action) { switch (action.type) { case 'ADD': return state + 1 case 'MINUS': return state - 1 default: return state }}
const store = createStore(reducer)
console.log(store);store.subscribe(() => { console.log('change');})console.log(store.getState());console.log(store.dispatch({type: 'ADD'}));console.log(store.getState());
复制代码


2. 迷你版


export const createStore = (reducer,enhancer)=>{    if(enhancer) {        return enhancer(createStore)(reducer)    }    let currentState = {}    let currentListeners = []
const getState = ()=>currentState const subscribe = (listener)=>{ currentListeners.push(listener) } const dispatch = action=>{ currentState = reducer(currentState, action) currentListeners.forEach(v=>v()) return action } dispatch({type:'@@INIT'}) return {getState,subscribe,dispatch}}
//中间件实现export applyMiddleWare(...middlewares){ return createStore=>...args=>{ const store = createStore(...args) let dispatch = store.dispatch
const midApi = { getState:store.getState, dispatch:...args=>dispatch(...args) } const middlewaresChain = middlewares.map(middleware=>middleware(midApi)) dispatch = compose(...middlewaresChain)(store.dispatch) return { ...store, dispatch } }
// fn1(fn2(fn3())) 把函数嵌套依次调用export function compose(...funcs){ if(funcs.length===0){ return arg=>arg } if(funs.length===1){ return funs[0] } return funcs.reduce((ret,item)=>(...args)=>ret(item(...args)))}

//bindActionCreator实现
function bindActionCreator(creator,dispatch){ return ...args=>dispatch(creator(...args))}function bindActionCreators(creators,didpatch){ //let bound = {} //Object.keys(creators).forEach(v=>{ // let creator = creator[v] // bound[v] = bindActionCreator(creator,dispatch) //}) //return bound
return Object.keys(creators).reduce((ret,item)=>{ ret[item] = bindActionCreator(creators[item],dispatch) return ret },{})}
复制代码

实现一个 JS 函数柯里化

预先处理的思想,利用闭包的机制

  • 柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数

  • 函数柯里化的主要作用和特点就是参数复用提前返回延迟执行


  • 柯里化把多次传入的参数合并,柯里化是一个高阶函数

  • 每次都返回一个新函数

  • 每次入参都是一个


当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?


有两种思路:


  • 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数

  • 在调用柯里化工具函数时,手动指定所需的参数个数


将这两点结合一下,实现一个简单 curry 函数


通用版


// 写法1function curry(fn, args) {  var length = fn.length;  var args = args || [];  return function(){      newArgs = args.concat(Array.prototype.slice.call(arguments));      if (newArgs.length < length) {          return curry.call(this,fn,newArgs);      }else{          return fn.apply(this,newArgs);      }  }}
复制代码


// 写法2// 分批传入参数// redux 源码的compose也是用了类似柯里化的操作const curry = (fn, arr = []) => {// arr就是我们要收集每次调用时传入的参数  let len = fn.length; // 函数的长度,就是参数的个数
return function(...args) { let newArgs = [...arr, ...args] // 收集每次传入的参数
// 如果传入的参数个数等于我们指定的函数参数个数,就执行指定的真正函数 if(newArgs.length === len) { return fn(...newArgs) } else { // 递归收集参数 return curry(fn, newArgs) } }}
复制代码


// 测试function multiFn(a, b, c) {  return a * b * c;}
var multi = curry(multiFn);
multi(2)(3)(4);multi(2,3,4);multi(2)(3,4);multi(2,3)(4)
复制代码


ES6 写法


const curry = (fn, arr = []) => (...args) => (  arg => arg.length === fn.length    ? fn(...arg)    : curry(fn, arg))([...arr, ...args])
复制代码


// 测试let curryTest=curry((a,b,c,d)=>a+b+c+d)curryTest(1,2,3)(4) //返回10curryTest(1,2)(4)(3) //返回10curryTest(1,2)(3,4) //返回10
复制代码


// 柯里化求值// 指定的函数function sum(a,b,c,d,e) {  return a + b + c + d + e}
// 传入指定的函数,执行一次let newSum = curry(sum)
// 柯里化 每次入参都是一个参数newSum(1)(2)(3)(4)(5)
// 偏函数newSum(1)(2)(3,4,5)
复制代码


// 柯里化简单应用// 判断类型,参数多少个,就执行多少次收集function isType(type, val) {  return Object.prototype.toString.call(val) === `[object ${type}]`}
let newType = curry(isType)
// 相当于把函数参数一个个传了,把第一次先缓存起来let isString = newType('String')let isNumber = newType('Number')
isString('hello world')isNumber(999)
复制代码

实现千位分隔符

// 保留三位小数parseToMoney(1234.56); // return '1,234.56'parseToMoney(123456789); // return '123,456,789'parseToMoney(1087654.321); // return '1,087,654.321'
复制代码


function parseToMoney(num) {  num = parseFloat(num.toFixed(3));  let [integer, decimal] = String.prototype.split.call(num, '.');  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');  return integer + '.' + (decimal ? decimal : '');}
复制代码


正则表达式(运用了正则的前向声明和反前向声明):


function parseToMoney(str){    // 仅仅对位置进行匹配    let re = /(?=(?!\b)(\d{3})+$)/g;    return str.replace(re,','); }
复制代码

实现非负大整数相加

JavaScript 对数值有范围的限制,限制如下:


Number.MAX_VALUE // 1.7976931348623157e+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码


如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。


实现一个算法进行大数的相加:


function sumBigNumber(a, b) {  let res = '';  let temp = 0;
a = a.split(''); b = b.split('');
while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, '');}
复制代码


其主要的思路如下:


  • 首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化

  • 初始化 res,temp 来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算

  • 将两个数组的对应的位进行相加,两个数相加的结果可能大于 10,所以可能要仅为,对 10 进行取余操作,将结果保存在当前位

  • 判断当前位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会自动隐式转化为 1,以便于下一次相加

  • 重复上述操作,直至计算结束

手写常见排序

冒泡排序

冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。


function bubbleSort(list) {  var n = list.length;  if (!n) return [];
for (var i = 0; i < n; i++) { // 注意这里需要 n - i - 1 for (var j = 0; j < n - i - 1; j++) { if (list[j] > list[j + 1]) { var temp = list[j + 1]; list[j + 1] = list[j]; list[j] = temp; } } } return list;}
复制代码

快速排序

快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作


ffunction quickSort(arr) {  if (arr.length<=1){    return arr;  }  var baseIndex = Math.floor(arr.length/2);//向下取整,选取基准点  var base = arr.splice(baseIndex,1)[0];//取出基准点的值,  // splice 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。  // slice方法返回一个新的数组对象,不会更改原数组  //这里不能直接base=arr[baseIndex],因为base代表的每次都删除的那个数  var left=[];  var right=[];  for (var i = 0; i<arr.length; i++){    // 这里的length是变化的,因为splice会改变原数组。    if (arr[i] < base){      left.push(arr[i]);//比基准点小的放在左边数组,    }  }else{    right.push(arr[i]);//比基准点大的放在右边数组,  }  return quickSort(left).concat([base],quickSort(right));}
复制代码

选择排序

function selectSort(arr) {  // 缓存数组长度  const len = arr.length;  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引  let minIndex;  // i 是当前排序区间的起点  for (let i = 0; i < len - 1; i++) {    // 初始化 minIndex 为当前区间第一个元素    minIndex = i;    // i、j分别定义当前区间的上下界,i是左边界,j是右边界    for (let j = i; j < len; j++) {      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j      if (arr[j] < arr[minIndex]) {        minIndex = j;      }    }    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者    if (minIndex !== i) {      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];    }  }  return arr;}// console.log(selectSort([3, 6, 2, 4, 1]));
复制代码

插入排序

function insertSort(arr) {  for (let i = 1; i < arr.length; i++) {    let j = i;    let target = arr[j];    while (j > 0 && arr[j - 1] > target) {      arr[j] = arr[j - 1];      j--;    }    arr[j] = target;  }  return arr;}// console.log(insertSort([3, 6, 2, 4, 1]));
复制代码

实现 every 方法

Array.prototype.myEvery=function(callback, context = window){    var len=this.length,        flag=true,        i = 0;
for(;i < len; i++){ if(!callback.apply(context,[this[i], i , this])){ flag=false; break; } } return flag; }

// var obj = {num: 1} // var aa=arr.myEvery(function(v,index,arr){ // return v.num>=12; // },obj) // console.log(aa)
复制代码

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是 d,出现了 5 次


let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);
复制代码

实现节流函数(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 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

Object.is

Object.is解决的主要是这两个问题:


+0 === -0  // trueNaN === NaN // false
复制代码


const is= (x, y) => {  if (x === y) {    // +0和-0应该不相等    return x !== 0 || y !== 0 || 1/x === 1/y;  } else {    return x !== x && y !== y;  }}
复制代码

debounce(防抖)

触发高频时间后 n 秒内函数只会执行一次,如果 n 秒内高频时间再次触发,则重新计算时间。


const debounce = (fn, time) => {  let timeout = null;  return function() {    clearTimeout(timeout)    timeout = setTimeout(() => {      fn.apply(this, arguments);    }, time);  }};
复制代码


防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

实现 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)); // 12console.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。


  • 参数长度不固定


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)())  //15console.log(addCurry(1)(2)(3, 4, 5)())  //15console.log(addCurry(1)(2, 3, 4, 5)())  //15
复制代码

图片懒加载

可以给 img 标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充 src 属性,此时才会进行图片资源加载。


function lazyload() {  const imgs = document.getElementsByTagName('img');  const len = imgs.length;  // 视口的高度  const viewHeight = document.documentElement.clientHeight;  // 滚动条高度  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;  for (let i = 0; i < len; i++) {    const offsetHeight = imgs[i].offsetTop;    if (offsetHeight < viewHeight + scrollHeight) {      const src = imgs[i].dataset.src;      imgs[i].src = src;    }  }}
// 可以使用节流优化一下window.addEventListener('scroll', lazyload);
复制代码

转化为驼峰命名

var s1 = "get-element-by-id"
// 转化为 getElementById
复制代码


var f = function(s) {    return s.replace(/-\w/g, function(x) {        return x.slice(1).toUpperCase();    })}
复制代码

手写节流函数

函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。


// 函数节流的实现;function throttle(fn, delay) {  let curTime = Date.now();
return function() { let context = this, args = arguments, nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } };}
复制代码


// 题目let a = "9007199254740991";let b = "1234567899999999999";
function add(a ,b){ //...}
复制代码


实现代码如下:


function add(a ,b){   //取两个数字的最大长度   let maxLength = Math.max(a.length, b.length);   //用0去补齐长度   a = a.padStart(maxLength , 0);//"0009007199254740991"   b = b.padStart(maxLength , 0);//"1234567899999999999"   //定义加法过程中需要用到的变量   let t = 0;   let f = 0;   //"进位"   let sum = "";   for(let i=maxLength-1 ; i>=0 ; i--){      t = parseInt(a[i]) + parseInt(b[i]) + f;      f = Math.floor(t/10);      sum = t%10 + sum;   }   if(f!==0){      sum = '' + f + sum;   }   return sum;}
复制代码

原生实现

function ajax() {  let xhr = new XMLHttpRequest() //实例化,以调用方法  xhr.open('get', 'https://www.google.com')  //参数2,url。参数三:异步  xhr.onreadystatechange = () => {  //每当 readyState 属性改变时,就会调用该函数。    if (xhr.readyState === 4) {  //XMLHttpRequest 代理当前所处状态。      if (xhr.status >= 200 && xhr.status < 300) {  //200-300请求成功        let string = request.responseText        //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象        let object = JSON.parse(string)      }    }  }  request.send() //用于实际发出 HTTP 请求。不带参数为GET请求}
复制代码

Promise 实现

基于Promise封装Ajax


  • 返回一个新的Promise实例

  • 创建HMLHttpRequest异步对象

  • 调用open方法,打开url,与服务器建立链接(发送前的一些处理)

  • 监听Ajax状态信息

  • 如果xhr.readyState == 4(表示服务器响应完成,可以获取使用服务器的响应了)

  • xhr.status == 200,返回resolve状态

  • xhr.status == 404,返回reject状态

  • xhr.readyState !== 4,把请求主体的信息基于send发送给服务器


function ajax(url) {  return new Promise((resolve, reject) => {    let xhr = new XMLHttpRequest()    xhr.open('get', url)    xhr.onreadystatechange = () => {      if (xhr.readyState == 4) {        if (xhr.status >= 200 && xhr.status <= 300) {          resolve(JSON.parse(xhr.responseText))        } else {          reject('请求出错')        }      }    }    xhr.send()  //发送hppt请求  })}
let url = '/data.json'ajax(url).then(res => console.log(res)) .catch(reason => console.log(reason))
复制代码

实现一个简易的 MVVM

实现一个简易的MVVM我会分为这么几步来:


  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据

  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,删除方法removeSub,还有一个notify方法用来遍历更新它subs中的所有依赖,同时 Dep 类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。

  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.notify方法来通知每一个依赖它的观察者进行更新

  4. 完成这些之后,我们还需要一个compile方法来将 HTML 模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用 compile 方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。

  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)

  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了


coding :


需要注意的点:


  • childNodes会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)

  • firstElementChild表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile进行递归调用,否则用正则匹配

  • 这里面的正则真的不难,大家可以看一下


完整代码如下:


<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <meta http-equiv="X-UA-Compatible" content="ie=edge" />    <title>MVVM</title>  </head>  <body>    <div id="app">      <h3>姓名</h3>      <p>{{name}}</p>      <h3>年龄</h3>      <p>{{age}}</p>    </div>  </body></html><script>  document.addEventListener(    "DOMContentLoaded",    function () {      let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };      let vm = new Vue(opt);      setTimeout(() => {        opt.data.name = "jing";      }, 2000);    },    false  );  class Vue {    constructor(opt) {      this.opt = opt;      this.observer(opt.data);      let root = document.querySelector(opt.el);      this.compile(root);    }    observer(data) {      Object.keys(data).forEach((key) => {        let obv = new Dep();        data["_" + key] = data[key];
Object.defineProperty(data, key, { get() { Dep.target && obv.addSubNode(Dep.target); return data["_" + key]; }, set(newVal) { obv.update(newVal); data["_" + key] = newVal; }, }); }); } compile(node) { [].forEach.call(node.childNodes, (child) => { if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) { let key = RegExp.$1.trim(); child.innerHTML = child.innerHTML.replace( new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"), this.opt.data[key] ); Dep.target = child; this.opt.data[key]; Dep.target = null; } else if (child.firstElementChild) this.compile(child); }); } }
class Dep { constructor() { this.subNode = []; } addSubNode(node) { this.subNode.push(node); } update(newVal) { this.subNode.forEach((node) => { node.innerHTML = newVal; }); } }</script>
复制代码


简化版 2


function update(){  console.log('数据变化~~~ mock update view')}let obj = [1,2,3]// 变异方法 push shift unshfit reverse sort splice pop// Object.definePropertylet oldProto = Array.prototype;let proto = Object.create(oldProto); // 克隆了一分['push','shift'].forEach(item=>{  proto[item] = function(){    update();    oldProto[item].apply(this,arguments);  }})function observer(value){ // proxy reflect  if(Array.isArray(value)){    // AOP    return value.__proto__ = proto;    // 重写 这个数组里的push shift unshfit reverse sort splice pop  }  if(typeof value !== 'object'){    return value;  }  for(let key in value){    defineReactive(value,key,value[key]);  }}function defineReactive(obj,key,value){  observer(value); // 如果是对象 继续增加getter和setter  Object.defineProperty(obj,key,{    get(){        return value;    },    set(newValue){        if(newValue !== value){            observer(newValue);            value = newValue;            update();        }    }  })}observer(obj); // AOP// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控// obj.name.n = 100;obj.push(123);obj.push(456);console.log(obj);
复制代码


参考前端手写面试题详细解答

实现一个链表结构

链表结构



看图理解 next 层级



// 链表 从头尾删除、增加 性能比较好// 分为很多类 常用单向链表、双向链表
// js模拟链表结构:增删改查
// node节点class Node { constructor(element,next) { this.element = element this.next = next } }
class LinkedList { constructor() { this.head = null // 默认应该指向第一个节点 this.size = 0 // 通过这个长度可以遍历这个链表 } // 增加O(n) add(index,element) { if(arguments.length === 1) { // 向末尾添加 element = index // 当前元素等于传递的第一项 index = this.size // 索引指向最后一个元素 } if(index < 0 || index > this.size) { throw new Error('添加的索引不正常') } if(index === 0) { // 直接找到头部 把头部改掉 性能更好 let head = this.head this.head = new Node(element,head) } else { // 获取当前头指针 let current = this.head // 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值 for (let i = 0; i < index-1; i++) { // 找到它的前一个 current = current.next } // 让创建的元素指向上一个元素的下一个 // 看图理解next层级 current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next }
this.size++; } // 删除O(n) remove(index) { if(index < 0 || index >= this.size) { throw new Error('删除的索引不正常') } this.size-- if(index === 0) { let head = this.head this.head = this.head.next // 移动指针位置
return head // 返回删除的元素 }else { let current = this.head for (let i = 0; i < index-1; i++) { // index-1找到它的前一个 current = current.next } let returnVal = current.next // 返回删除的元素 // 找到待删除的指针的上一个 current.next.next // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可 current.next = current.next.next
return returnVal } } // 查找O(n) get(index) { if(index < 0 || index >= this.size) { throw new Error('查找的索引不正常') } let current = this.head for (let i = 0; i < index; i++) { current = current.next } return current }}

var ll = new LinkedList()
ll.add(0,100) // Node { ellement: 100, next: null }ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }ll.add(300)ll.remove(0)
console.log(ll.get(2),'get')console.log(ll.head)
module.exports = LinkedList
复制代码

实现 Object.create

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


// 模拟 Object.create
function create(proto) { function F() {} F.prototype = proto;
return new F();}
复制代码

实现防抖函数(debounce)

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


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


手写简化版:


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


适用场景:


  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次

  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似


生存环境请用 lodash.debounce

对象数组如何去重

根据每个对象的某一个具体属性来进行去重


const responseList = [  { id: 1, a: 1 },  { id: 2, a: 2 },  { id: 3, a: 3 },  { id: 1, a: 4 },];const result = responseList.reduce((acc, cur) => {    const ids = acc.map(item => item.id);    return ids.includes(cur.id) ? acc : [...acc, cur];}, []);console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
复制代码

实现字符串的 repeat 方法

输入字符串 s,以及其重复的次数,输出重复的结果,例如输入 abc,2,输出 abcabc。


function repeat(s, n) {    return (new Array(n + 1)).join(s);}
复制代码


递归:


function repeat(s, n) {    return (n > 0) ? s.concat(repeat(s, --n)) : "";}
复制代码

手写深度比较 isEqual

思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归


  • 递归退出条件:

  • 被比较的是两个值类型变量,直接用“===”判断

  • 被比较的两个变量之一为null,直接判断另一个元素是否也为null

  • 提前结束递推:

  • 两个变量keys数量不同

  • 传入的两个参数是同一个变量

  • 递推工作:深度比较每一个key


function isEqual(obj1, obj2){    //其中一个为值类型或null    if(!isObject(obj1) || !isObject(obj2)){        return obj1 === obj2;    }
//判断是否两个参数是同一个变量 if(obj1 === obj2){ return true; }
//判断keys数是否相等 const obj1Keys = Object.keys(obj1); const obj2Keys = Object.keys(obj2); if(obj1Keys.length !== obj2Keys.length){ return false; }
//深度比较每一个key for(let key in obj1){ if(!isEqual(obj1[key], obj2[key])){ return false; } }
return true;}
复制代码

实现一个 sleep 函数,比如 sleep(1000) 意味着等待 1000 毫秒

// 使用 promise来实现 sleepconst sleep = (time) => {  return new Promise(resolve => setTimeout(resolve, time))}
sleep(1000).then(() => { // 这里写你的骚操作})
复制代码

实现 Promise

var PromisePolyfill = (function () {  // 和reject不同的是resolve需要尝试展开thenable对象  function tryToResolve (value) {    if (this === value) {    // 主要是防止下面这种情况    // let y = new Promise(res => setTimeout(res(y)))      throw TypeError('Chaining cycle detected for promise!')    }
// 根据规范2.32以及2.33 对对象或者函数尝试展开 // 保证S6之前的 polyfill 也能和ES6的原生promise混用 if (value !== null && (typeof value === 'object' || typeof value === 'function')) { try { // 这里记录这次then的值同时要被try包裹 // 主要原因是 then 可能是一个getter, 也也就是说 // 1. value.then可能报错 // 2. value.then可能产生副作用(例如多次执行可能结果不同) var then = value.then
// 另一方面, 由于无法保证 then 确实会像预期的那样只调用一个onFullfilled / onRejected // 所以增加了一个flag来防止resolveOrReject被多次调用 var thenAlreadyCalledOrThrow = false if (typeof then === 'function') { // 是thenable 那么尝试展开 // 并且在该thenable状态改变之前this对象的状态不变 then.bind(value)( // onFullfilled function (value2) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true tryToResolve.bind(this, value2)() }.bind(this),
// onRejected function (reason2) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true resolveOrReject.bind(this, 'rejected', reason2)() }.bind(this) ) } else { // 拥有then 但是then不是一个函数 所以也不是thenable resolveOrReject.bind(this, 'resolved', value)() } } catch (e) { if (thenAlreadyCalledOrThrow) return thenAlreadyCalledOrThrow = true resolveOrReject.bind(this, 'rejected', e)() } } else { // 基本类型 直接返回 resolveOrReject.bind(this, 'resolved', value)() } }
function resolveOrReject (status, data) { if (this.status !== 'pending') return this.status = status this.data = data if (status === 'resolved') { for (var i = 0; i < this.resolveList.length; ++i) { this.resolveList[i]() } } else { for (i = 0; i < this.rejectList.length; ++i) { this.rejectList[i]() } } }
function Promise (executor) { if (!(this instanceof Promise)) { throw Error('Promise can not be called without new !') }
if (typeof executor !== 'function') { // 非标准 但与Chrome谷歌保持一致 throw TypeError('Promise resolver ' + executor + ' is not a function') }
this.status = 'pending' this.resolveList = [] this.rejectList = []
try { executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected')) } catch (e) { resolveOrReject.bind(this, 'rejected', e)() } }
Promise.prototype.then = function (onFullfilled, onRejected) { // 返回值穿透以及错误穿透, 注意错误穿透用的是throw而不是return,否则的话 // 这个then返回的promise状态将变成resolved即接下来的then中的onFullfilled // 会被调用, 然而我们想要调用的是onRejected if (typeof onFullfilled !== 'function') { onFullfilled = function (data) { return data } } if (typeof onRejected !== 'function') { onRejected = function (reason) { throw reason } }
var executor = function (resolve, reject) { setTimeout(function () { try { // 拿到对应的handle函数处理this.data // 并以此为依据解析这个新的Promise var value = this.status === 'resolved' ? onFullfilled(this.data) : onRejected(this.data) resolve(value) } catch (e) { reject(e) } }.bind(this)) }
// then 接受两个函数返回一个新的Promise // then 自身的执行永远异步与onFullfilled/onRejected的执行 if (this.status !== 'pending') { return new Promise(executor.bind(this)) } else { // pending return new Promise(function (resolve, reject) { this.resolveList.push(executor.bind(this, resolve, reject)) this.rejectList.push(executor.bind(this, resolve, reject)) }.bind(this)) } }
// for prmise A+ test Promise.deferred = Promise.defer = function () { var dfd = {} dfd.promise = new Promise(function (resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd }
// for prmise A+ test if (typeof module !== 'undefined') { module.exports = Promise }
return Promise})()
PromisePolyfill.all = function (promises) { return new Promise((resolve, reject) => { const result = [] let cnt = 0 for (let i = 0; i < promises.length; ++i) { promises[i].then(value => { cnt++ result[i] = value if (cnt === promises.length) resolve(result) }, reject) } })}
PromisePolyfill.race = function (promises) { return new Promise((resolve, reject) => { for (let i = 0; i < promises.length; ++i) { promises[i].then(resolve, reject) } })}
复制代码

实现 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);
复制代码

实现 bind

实现 bind 要做什么


  • 返回一个函数,绑定 this,传递预置参数

  • bind 返回的函数可以作为构造函数使用。故作为构造函数时应使得 this 失效,但是传入的参数依然有效


// mdn的实现if (!Function.prototype.bind) {  Function.prototype.bind = function(oThis) {    if (typeof this !== 'function') {      // closest thing possible to the ECMAScript 5      // internal IsCallable function      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');    }
var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function() {}, fBound = function() { // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用 return fToBind.apply(this instanceof fBound ? this : oThis, // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的 aArgs.concat(Array.prototype.slice.call(arguments))); };
// 维护原型关系 if (this.prototype) { // Function.prototype doesn't have a prototype property fNOP.prototype = this.prototype; } // 下行的代码使fBound.prototype是fNOP的实例,因此 // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例 fBound.prototype = new fNOP();
return fBound; };}
复制代码

实现一个迭代器生成函数

ES6 对迭代器的实现

JS 原生的集合类型数据结构,只有Array(数组)和Object(对象);而ES6中,又新增了MapSet。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套 统一的接口机制 ——迭代器(Iterator)。


ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的 next 方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。


在 ES6 中,针对ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for...of...遍历数组时:


const arr = [1, 2, 3]const len = arr.lengthfor(item of arr) {   console.log(`当前元素是${item}`)}
复制代码


之所以能够按顺序一次一次地拿到数组里的每一个成员,是因为我们借助数组的Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员,像这样:


const arr = [1, 2, 3]// 通过调用iterator,拿到迭代器对象const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行next,就能逐个访问集合的成员iterator.next()iterator.next()iterator.next()
复制代码


丢进控制台,我们可以看到next每次会按顺序帮我们访问一个集合成员:



for...of...做的事情,基本等价于下面这通操作:


// 通过调用iterator,拿到迭代器对象const iterator = arr[Symbol.iterator]()
// 初始化一个迭代结果let now = { done: false }
// 循环往外迭代成员while(!now.done) { now = iterator.next() if(!now.done) { console.log(`现在遍历到了${now.value}`) }}
复制代码


可以看出,for...of...其实就是iterator循环调用换了种写法。在 ES6 中我们之所以能够开心地用for...of...遍历各种各种的集合,全靠迭代器模式在背后给力。


ps:此处推荐阅读迭代协议 (opens new window),相信大家读过后会对迭代器在 ES6 中的实现有更深的理解。

实现类的继承

类的继承在几年前是重点内容,有 n 种继承方式各有优劣,es6 普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。


function Parent(name) {    this.parent = name}Parent.prototype.say = function() {    console.log(`${this.parent}: 你打篮球的样子像kunkun`)}function Child(name, parent) {    // 将父类的构造函数绑定在子类上    Parent.call(this, parent)    this.child = name}
/** 1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创建了父类原型的副本,与父类原型完全隔离*/Child.prototype = Object.create(Parent.prototype);Child.prototype.say = function() { console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);}
// 注意记得把子类的构造指向子类本身Child.prototype.constructor = Child;
var parent = new Parent('father');parent.say() // father: 你打篮球的样子像kunkun
var child = new Child('cxk', 'father');child.say() // father好,我是练习时长两年半的cxk
复制代码

instanceof

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。


const myInstanceof = (left, right) => {  // 基本数据类型都返回false  if (typeof left !== 'object' || left === null) return false;  let proto = Object.getPrototypeOf(left);  while (true) {    if (proto === null) return false;    if (proto === right.prototype) return true;    proto = Object.getPrototypeOf(proto);  }}
复制代码

实现双向数据绑定

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})
复制代码

实现 Array.of 方法

Array.of()方法用于将一组值,转换为数组


  • 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

  • Array.of()基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一


Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1
复制代码


实现


function ArrayOf(){  return [].slice.call(arguments);}
复制代码

验证是否是身份证

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


用户头像

还未添加个人签名 2022.07.31 加入

还未添加个人简介

评论

发布
暂无评论
美团前端二面必会手写面试题汇总_JavaScript_helloworld1024fd_InfoQ写作社区