请实现一个 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

判断是否是电话号码

function isPhone(tel) {    var regx = /^1[34578]\d{9}$/;    return regx.test(tel);}

解析 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;}

数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
办法一:利用Set
const res1 = Array.from(new Set(arr));
办法二:两层for循环+splice
const unique1 = arr => {  let len = arr.length;  for (let i = 0; i < len; i++) {    for (let j = i + 1; j < len; j++) {      if (arr[i] === arr[j]) {        arr.splice(j, 1);        // 每删除一个树,j--保障j的值通过自加后不变。同时,len--,缩小循环次数晋升性能        len--;        j--;      }    }  }  return arr;}
办法三:利用indexOf
const unique2 = arr => {  const res = [];  for (let i = 0; i < arr.length; i++) {    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);  }  return res;}

当然也能够用include、filter,思路大同小异。

办法四:利用include
const unique3 = arr => {  const res = [];  for (let i = 0; i < arr.length; i++) {    if (!res.includes(arr[i])) res.push(arr[i]);  }  return res;}
办法五:利用filter
const unique4 = arr => {  return arr.filter((item, index) => {    return arr.indexOf(item) === index;  });}
办法六:利用Map
const unique5 = arr => {  const map = new Map();  const res = [];  for (let i = 0; i < arr.length; i++) {    if (!map.has(arr[i])) {      map.set(arr[i], true)      res.push(arr[i]);    }  }  return res;}

Function.prototype.bind

Function.prototype.bind = function(context, ...args) {  if (typeof this !== 'function') {    throw new Error("Type Error");  }  // 保留this的值  var self = this;  return function F() {    // 思考new的状况    if(this instanceof F) {      return new self(...args, ...arguments)    }    return self.apply(context, [...args, ...arguments])  }}

深拷贝

递归的残缺版本(思考到了Symbol属性):

const cloneDeep1 = (target, hash = new WeakMap()) => {  // 对于传入参数解决  if (typeof target !== 'object' || target === null) {    return target;  }  // 哈希表中存在间接返回  if (hash.has(target)) return hash.get(target);  const cloneTarget = Array.isArray(target) ? [] : {};  hash.set(target, cloneTarget);  // 针对Symbol属性  const symKeys = Object.getOwnPropertySymbols(target);  if (symKeys.length) {    symKeys.forEach(symKey => {      if (typeof target[symKey] === 'object' && target[symKey] !== null) {        cloneTarget[symKey] = cloneDeep1(target[symKey]);      } else {        cloneTarget[symKey] = target[symKey];      }    })  }  for (const i in target) {    if (Object.prototype.hasOwnProperty.call(target, i)) {      cloneTarget[i] =        typeof target[i] === 'object' && target[i] !== null        ? cloneDeep1(target[i], hash)        : target[i];    }  }  return cloneTarget;}

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

实现节流函数(throttle)

防抖函数原理:规定在一个单位工夫内,只能触发一次函数。如果这个单位工夫内触发屡次函数,只有一次失效。

// 手写简化版

// 节流函数const throttle = (fn, delay = 500) => {  let flag = true;  return (...args) => {    if (!flag) return;    flag = false;    setTimeout(() => {      fn.apply(this, args);      flag = true;    }, delay);  };};

实用场景:

  • 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
  • 缩放场景:监控浏览器resize
  • 动画场景:防止短时间内屡次触发动画引起性能问题

手写 bind 函数

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
  2. 保留以后函数的援用,获取其余传入参数值。
  3. 创立一个函数返回
  4. 函数外部应用 apply 来绑定函数调用,须要判断函数作为构造函数的状况,这个时候须要传入以后函数的 this 给 apply 调用,其余状况都传入指定的上下文对象。
// bind 函数实现Function.prototype.myBind = function(context) {  // 判断调用对象是否为函数  if (typeof this !== "function") {    throw new TypeError("Error");  }  // 获取参数  var args = [...arguments].slice(1),      fn = this;  return function Fn() {    // 依据调用形式,传入不同绑定值    return fn.apply(      this instanceof Fn ? this : context,      args.concat(...arguments)    );  };};

实现数组的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;}

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

滚动加载

原理就是监听页面滚动事件,剖析clientHeightscrollTopscrollHeight三者的属性关系。

window.addEventListener('scroll', function() {  const clientHeight = document.documentElement.clientHeight;  const scrollTop = document.documentElement.scrollTop;  const scrollHeight = document.documentElement.scrollHeight;  if (clientHeight + scrollTop >= scrollHeight) {    // 检测到滚动至页面底部,进行后续操作    // ...  }}, false);

判断对象是否存在循环援用

循环援用对象原本没有什么问题,然而序列化的时候就会产生问题,比方调用JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.

上面办法能够用来判断一个对象中是否已存在循环援用:

const isCycleObject = (obj,parent) => {    const parentArr = parent || [obj];    for(let i in obj) {        if(typeof obj[i] === 'object') {            let flag = false;            parentArr.forEach((pObj) => {                if(pObj === obj[i]){                    flag = true;                }            })            if(flag) return true;            flag = isCycleObject(obj[i],[...parentArr,obj[i]]);            if(flag) return true;        }    }    return false;}const a = 1;const b = {a};const c = {b};const o = {d:{a:3},c}o.c.b.aa = a;console.log(isCycleObject(o)

查找有序二维数组的目标值:

var findNumberIn2DArray = function(matrix, target) {    if (matrix == null || matrix.length == 0) {        return false;    }    let row = 0;    let column = matrix[0].length - 1;    while (row < matrix.length && column >= 0) {        if (matrix[row][column] == target) {            return true;        } else if (matrix[row][column] > target) {            column--;        } else {            row++;        }    }    return false;};

二维数组斜向打印:

function printMatrix(arr){  let m = arr.length, n = arr[0].length    let res = []  // 左上角,从0 到 n - 1 列进行打印  for (let k = 0; k < n; k++) {    for (let i = 0, j = k; i < m && j >= 0; i++, j--) {      res.push(arr[i][j]);    }  }  // 右下角,从1 到 n - 1 行进行打印  for (let k = 1; k < m; k++) {    for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {      res.push(arr[i][j]);    }  }  return res}

实现日期格式化函数

输出:

dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
const dateFormat = (dateInput, format)=>{    var day = dateInput.getDate()     var month = dateInput.getMonth() + 1      var year = dateInput.getFullYear()       format = format.replace(/yyyy/, year)    format = format.replace(/MM/,month)    format = format.replace(/dd/,day)    return format}

字符串呈现的不反复最长长度

用一个滑动窗口装没有反复的字符,枚举字符记录最大值即可。用 map 保护字符的索引,遇到雷同的字符,把左边界挪动过来即可。移动的过程中记录最大长度:

var lengthOfLongestSubstring = function (s) {    let map = new Map();    let i = -1    let res = 0    let n = s.length    for (let j = 0; j < n; j++) {        if (map.has(s[j])) {            i = Math.max(i, map.get(s[j]))        }        res = Math.max(res, j - i)        map.set(s[j], j)    }    return res};

手写 Promise

const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";function MyPromise(fn) {  // 保留初始化状态  var self = this;  // 初始化状态  this.state = PENDING;  // 用于保留 resolve 或者 rejected 传入的值  this.value = null;  // 用于保留 resolve 的回调函数  this.resolvedCallbacks = [];  // 用于保留 reject 的回调函数  this.rejectedCallbacks = [];  // 状态转变为 resolved 办法  function resolve(value) {    // 判断传入元素是否为 Promise 值,如果是,则状态扭转必须期待前一个状态扭转后再进行扭转    if (value instanceof MyPromise) {      return value.then(resolve, reject);    }    // 保障代码的执行程序为本轮事件循环的开端    setTimeout(() => {      // 只有状态为 pending 时能力转变,      if (self.state === PENDING) {        // 批改状态        self.state = RESOLVED;        // 设置传入的值        self.value = value;        // 执行回调函数        self.resolvedCallbacks.forEach(callback => {          callback(value);        });      }    }, 0);  }  // 状态转变为 rejected 办法  function reject(value) {    // 保障代码的执行程序为本轮事件循环的开端    setTimeout(() => {      // 只有状态为 pending 时能力转变      if (self.state === PENDING) {        // 批改状态        self.state = REJECTED;        // 设置传入的值        self.value = value;        // 执行回调函数        self.rejectedCallbacks.forEach(callback => {          callback(value);        });      }    }, 0);  }  // 将两个办法传入函数执行  try {    fn(resolve, reject);  } catch (e) {    // 遇到谬误时,捕捉谬误,执行 reject 函数    reject(e);  }}MyPromise.prototype.then = function(onResolved, onRejected) {  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数  onResolved =    typeof onResolved === "function"      ? onResolved      : function(value) {          return value;        };  onRejected =    typeof onRejected === "function"      ? onRejected      : function(error) {          throw error;        };  // 如果是期待状态,则将函数退出对应列表中  if (this.state === PENDING) {    this.resolvedCallbacks.push(onResolved);    this.rejectedCallbacks.push(onRejected);  }  // 如果状态曾经凝固,则间接执行对应状态的函数  if (this.state === RESOLVED) {    onResolved(this.value);  }  if (this.state === REJECTED) {    onRejected(this.value);  }};

将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 => {    let parent = map[item.pid];    if(parent) {      (parent.children || (parent.children = [])).push(item);    } else {      result.push(item);    }  });  return result;}

手写节流函数

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

Array.prototype.map()

Array.prototype.map = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + ' is not a function');  }  const res = [];  // 同理  const O = Object(this);  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    if (i in O) {      // 调用回调函数并传入新数组      res[i] = callback.call(thisArg, O[i], i, this);    }  }  return res;}

实现apply办法

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

// 模仿 applyFunction.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;};

深克隆(deepclone)

简略版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  1. 他无奈实现对函数 、RegExp等非凡对象的克隆
  2. 会摈弃对象的constructor,所有的构造函数会指向Object
  3. 对象有循环援用,会报错

面试版:

/** * deep clone * @param  {[type]} parent object 须要进行克隆的对象 * @return {[type]}        深克隆后的对象 */const clone = parent => {  // 判断类型  const isType = (obj, type) => {    if (typeof obj !== "object") return false;    const typeString = Object.prototype.toString.call(obj);    let flag;    switch (type) {      case "Array":        flag = typeString === "[object Array]";        break;      case "Date":        flag = typeString === "[object Date]";        break;      case "RegExp":        flag = typeString === "[object RegExp]";        break;      default:        flag = false;    }    return flag;  };  // 解决正则  const getRegExp = re => {    var flags = "";    if (re.global) flags += "g";    if (re.ignoreCase) flags += "i";    if (re.multiline) flags += "m";    return flags;  };  // 保护两个贮存循环援用的数组  const parents = [];  const children = [];  const _clone = parent => {    if (parent === null) return null;    if (typeof parent !== "object") return parent;    let child, proto;    if (isType(parent, "Array")) {      // 对数组做非凡解决      child = [];    } else if (isType(parent, "RegExp")) {      // 对正则对象做非凡解决      child = new RegExp(parent.source, getRegExp(parent));      if (parent.lastIndex) child.lastIndex = parent.lastIndex;    } else if (isType(parent, "Date")) {      // 对Date对象做非凡解决      child = new Date(parent.getTime());    } else {      // 解决对象原型      proto = Object.getPrototypeOf(parent);      // 利用Object.create切断原型链      child = Object.create(proto);    }    // 解决循环援用    const index = parents.indexOf(parent);    if (index != -1) {      // 如果父数组存在本对象,阐明之前曾经被援用过,间接返回此对象      return children[index];    }    parents.push(parent);    children.push(child);    for (let i in parent) {      // 递归      child[i] = _clone(parent[i]);    }    return child;  };  return _clone(parent);};

局限性:

  1. 一些非凡状况没有解决: 例如Buffer对象、Promise、Set、Map
  2. 另外对于确保没有循环援用的对象,咱们能够省去对循环援用的非凡解决,因为这很耗费工夫
原理详解实现深克隆