乐趣区

关于前端:前端一面手写面试题总结

应用 Promise 封装 AJAX 申请

// promise 封装实现:function getJSON(url) {
  // 创立一个 promise 对象
  let promise = new Promise(function(resolve, reject) {let xhr = new XMLHttpRequest();
    // 新建一个 http 申请
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {if (this.readyState !== 4) return;
      // 当申请胜利或失败时,扭转 promise 的状态
      if (this.status === 200) {resolve(this.response);
      } else {reject(new Error(this.statusText));
      }
    };
    // 设置谬误监听函数
    xhr.onerror = function() {reject(new Error(this.statusText));
    };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置申请头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 申请
    xhr.send(null);
  });
  return promise;
}

实现一个 add 办法实现两个大数相加

// 题目
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;
}

实现双向数据绑定

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})

实现公布 - 订阅模式

class EventCenter{
  // 1. 定义事件容器,用来装事件数组
    let handlers = {}

  // 2. 增加事件办法,参数:事件名 事件办法
  addEventListener(type, handler) {
    // 创立新数组容器
    if (!this.handlers[type]) {this.handlers[type] = []}
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 触发事件,参数:事件名 事件参数
  dispatchEvent(type, params) {
    // 若没有注册该事件则抛出谬误
    if (!this.handlers[type]) {return new Error('该事件未注册')
    }
    // 触发事件
    this.handlers[type].forEach(handler => {handler(...params)
    })
  }

  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和公布
  removeEventListener(type, handler) {if (!this.handlers[type]) {return new Error('事件有效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {return new Error('无该绑定事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {delete this.handlers[type]
      }
    }
  }
}

手写节流函数

函数节流是指规定一个单位工夫,在这个单位工夫内,只能有一次触发事件的回调函数执行,如果在同一个单位工夫内某事件被触发屡次,只有一次能失效。节流能够应用在 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);
    }
  };
}

Promise 并行限度

就是实现有并行限度的 Promise 调度器问题

class Scheduler {constructor() {this.queue = [];
    this.maxCount = 2;
    this.runCounts = 0;
  }
  add(promiseCreator) {this.queue.push(promiseCreator);
  }
  taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();
    }
  }
  request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}
    this.runCounts++;

    this.queue.shift()().then(() => {
      this.runCounts--;
      this.request();});
  }
}

const timeout = time => new Promise(resolve => {setTimeout(resolve, time);
})

const scheduler = new Scheduler();

const addTask = (time,order) => {scheduler.add(() => timeout(time).then(()=>console.log(order)))
}


addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4

参考 前端进阶面试题具体解答

Promise.all

Promise.all是反对链式调用的,实质上就是返回了一个 Promise 实例,通过 resolvereject来扭转实例状态。

Promise.myAll = function(promiseArr) {return new Promise((resolve, reject) => {const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {promiseArr[i]
      .then(res => {ans[i] = res;
        index++;
        if (index === promiseArr.length) {resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

实现浅拷贝

浅拷贝是指,一个新的对象对原始对象的属性值进行准确地拷贝,如果拷贝的是根本数据类型,拷贝的就是根本数据类型的值,如果是援用数据类型,拷贝的就是内存地址。如果其中一个对象的援用内存地址产生扭转,另一个对象也会发生变化。

(1)Object.assign()

Object.assign()是 ES6 中对象的拷贝办法,承受的第一个参数是指标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该办法能够实现浅拷贝,也能够实现一维对象的深拷贝。

留神:

  • 如果指标对象和源对象有同名属性,或者多个源对象有同名属性,则前面的属性会笼罩后面的属性。
  • 如果该函数只有一个参数,当参数为对象时,间接返回该对象;当参数不是对象时,会先将参数转为对象而后返回。
  • 因为 nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,会报错。
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 cloneObj = {...obj};

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
  • slice()办法是 JavaScript 数组的一个办法,这个办法能够从已有数组中返回选定的元素:用法:array.slice(start, end),该办法不会扭转原始数组。
  • 该办法有两个参数,两个参数都可选,如果两个参数都不写,就能够实现一个数组的浅拷贝。
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false
2)Array.prototype.concat
  • concat() 办法用于合并两个或多个数组。此办法不会更改现有数组,而是返回一个新数组。
  • 该办法有两个参数,两个参数都可选,如果两个参数都不写,就能够实现一个数组的浅拷贝。
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

(4)手写实现浅拷贝

// 浅拷贝的实现;

function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 依据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
    }
  }

  return newObject;
}// 浅拷贝的实现;

function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 依据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
    }
  }

  return newObject;
}// 浅拷贝的实现;
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;
  // 依据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};
  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] = object[key];
    }
  }
  return newObject;
}

手写 new 操作符

在调用 new 的过程中会产生以上四件事件:

(1)首先创立了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象增加属性)

(4)判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回后果
  return flag ? result : newObject;
}
// 应用办法
objectFactory(构造函数, 初始化参数);

Object.assign

Object.assign()办法用于将所有可枚举属性的值从一个或多个源对象复制到指标对象。它将返回指标对象(请留神这个操作是浅拷贝)

Object.defineProperty(Object, 'assign', {value: function(target, ...args) {if (target == null) {return new TypeError('Cannot convert undefined or null to object');
    }

    // 指标对象须要对立是援用数据类型,若不是会主动转换
    const to = Object(target);

    for (let i = 0; i < args.length; i++) {
      // 每一个源对象
      const nextSource = args[i];
      if (nextSource !== null) {
        // 应用 for...in 和 hasOwnProperty 双重判断,确保只拿到自身的属性、办法(不蕴含继承的)for (const nextKey in nextSource) {if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  // 不可枚举
  enumerable: false,
  writable: true,
  configurable: true,
})

实现 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

字符串解析问题

var a = {
    b: 123,
    c: '456',
    e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'

实现函数使得将 str 字符串中的 {} 内的变量替换,如果属性不存在放弃原样(比方{a.d}

相似于模版字符串,但有一点出入,实际上原理大差不差

const fn1 = (str, obj) => {
    let res = '';
    // 标记位,标记后面是否有{
    let flag = false;
    let start;
    for (let i = 0; i < str.length; i++) {if (str[i] === '{') {
            flag = true;
            start = i + 1;
            continue;
        }
        if (!flag) res += str[i];
        else {if (str[i] === '}') {
                flag = false;
                res += match(str.slice(start, i), obj);
            }
        }
    }
    return res;
}
// 对象匹配操作
const match = (str, obj) => {const keys = str.split('.').slice(1);
    let index = 0;
    let o = obj;
    while (index < keys.length) {const key = keys[index];
        if (!o[key]) {return `{${str}}`;
        } else {o = o[key];
        }
        index++;
    }
    return o;
}

应用 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;
}

封装异步的 fetch,应用 async await 形式来应用

(async () => {
    class HttpRequestUtil {async get(url) {const res = await fetch(url);
            const data = await res.json();
            return data;
        }
        async post(url, data) {
            const res = await fetch(url, {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async put(url, data) {
            const res = await fetch(url, {
                method: 'PUT',
                headers: {'Content-Type': 'application/json'},
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async delete(url, data) {
            const res = await fetch(url, {
                method: 'DELETE',
                headers: {'Content-Type': 'application/json'},
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
    }
    const httpRequestUtil = new HttpRequestUtil();
    const res = await httpRequestUtil.get('http://golderbrother.cn/');
    console.log(res);
})();

实现 Event(event bus)

event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖伎俩之一,同时波及了订阅 - 公布设计模式,是十分重要的根底。

简略版:

class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听下限
  }
}


// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 从贮存事件键值对的 this._events 中获取对应事件回调函数
  handler = this._events.get(type);
  if (args.length > 0) {handler.apply(this, args);
  } else {handler.call(this);
  }
  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
  if (!this._events.get(type)) {this._events.set(type, fn);
  }
};

面试版:

class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听下限
  }
}

// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 从贮存事件键值对的 this._events 中获取对应事件回调函数
  handler = this._events.get(type);
  if (args.length > 0) {handler.apply(this, args);
  } else {handler.call(this);
  }
  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
  if (!this._events.get(type)) {this._events.set(type, fn);
  }
};

// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  handler = this._events.get(type);
  if (Array.isArray(handler)) {
    // 如果是一个数组阐明有多个监听者, 须要顺次此触发外面的函数
    for (let i = 0; i < handler.length; i++) {if (args.length > 0) {handler[i].apply(this, args);
      } else {handler[i].call(this);
      }
    }
  } else {
    // 单个函数的状况咱们间接触发即可
    if (args.length > 0) {handler.apply(this, args);
    } else {handler.call(this);
    }
  }

  return true;
};

// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
  if (!handler) {this._events.set(type, fn);
  } else if (handler && typeof handler === "function") {
    // 如果 handler 是函数阐明只有一个监听者
    this._events.set(type, [handler, fn]); // 多个监听者咱们须要用数组贮存
  } else {handler.push(fn); // 曾经有多个监听者, 那么间接往数组里 push 函数即可
  }
};

EventEmeitter.prototype.removeListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单

  // 如果是函数, 阐明只被监听了一次
  if (handler && typeof handler === "function") {this._events.delete(type, fn);
  } else {
    let postion;
    // 如果 handler 是数组, 阐明被监听屡次要找到对应的函数
    for (let i = 0; i < handler.length; i++) {if (handler[i] === fn) {postion = i;} else {postion = -1;}
    }
    // 如果找到匹配的函数, 从数组中革除
    if (postion !== -1) {
      // 找到数组对应的地位, 间接革除此回调
      handler.splice(postion, 1);
      // 如果革除后只有一个函数, 那么勾销数组, 以函数模式保留
      if (handler.length === 1) {this._events.set(type, handler[0]);
      }
    } else {return this;}
  }
};

实现具体过程和思路见实现 event

解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 后果
{ user: 'anonymous',
  id: [123, 456], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/

function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来
  const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {if (/=/.test(param)) { // 解决有 value 的参数
      let [key, val] = param.split('='); // 宰割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创立 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 解决没有 value 的参数
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}

验证是否是身份证

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

实现观察者模式

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

观察者须要放到被观察者中,被观察者的状态变动须要告诉观察者 我变动了 外部也是基于公布订阅模式,收集观察者,状态变动后要被动告诉观察者

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('被欺侮了');

实现数组的 filter 办法

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

实现千位分隔符

// 保留三位小数
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,','); 
}
退出移动版