JS笔试题

JavaScript 笔试部分

实现防抖函数(debounce)

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

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

debounce.gif

手写简化版:

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

适用场景:

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

生存环境请用 lodash.debounce

实现节流函数(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
  • 动画场景:避免短时间内多次触发动画引起性能问题

深克隆(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. 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间

实现 Event(event bus)

event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。

简单版:

class EventEmitter {
  constructor() {
    this._events = this._events || new Map(); // 储存事件/回调键值对
    this._maxListeners = this._maxListeners || 10; // 设立监听上限
  }
}
// 触发名为type的事件
EventEmitter.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的事件
EventEmitter.prototype.addListener = function (type, fn) {
  // 将type事件以及对应的fn函数放入this._events中储存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

面试版:

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

// 触发名为type的事件
EventEmitter.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的事件
EventEmitter.prototype.addListener = function (type, fn) {
  // 将type事件以及对应的fn函数放入this._events中储存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

// 触发名为type的事件
EventEmitter.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的事件
EventEmitter.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函数即可
  }
};

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

  // 如果是函数,说明只被监听了一次
  if (handler && typeof handler === "function") {
    this._events.delete(type, fn);
  } else {
    let position;
    // 如果handler是数组,说明被监听多次要找到对应的函数
    for (let i = 0; i < handler.length; i++) {
      if (handler[i] === fn) {
        position = i;
      } else {
        position = -1;
      }
    }
    // 如果找到匹配的函数,从数组中清除
    if (position !== -1) {
      // 找到数组对应的位置,直接清除此回调
      handler.splice(position, 1);
      // 如果清除后只有一个函数,那么取消数组,以函数形式保存
      if (handler.length === 1) {
        this._events.set(type, handler[0]);
      }
    } else {
      return this;
    }
  }
};

实现 instanceOf

// 模拟 instanceof
function instance_of(L, R) {
  //L 表示左表达式,R 表示右表达式
  var O = R.prototype; // 取 R 的显示原型
  L = L.__proto__; // 取 L 的隐式原型
  while (true) {
    if (L === null) return false;
    if (O === L)
      // 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}

模拟 new

new 操作符做了这些事:

  • 它创建了一个全新的对象
  • 它会被执行[[Prototype]](也就是 proto)链接
  • 它使 this 指向新创建的对象
  • 通过 new 创建的每个对象将最终被[[Prototype]]链接到这个函数的 prototype 对象上
  • 如果函数没有返回对象类型 Object(包含 Function, Array, Date, RegExg, Error),那么 new 表达式中的函数调用将返 回该对象引用
// objectFactory(name, 'cxk', '18')
function objectFactory() {
  const obj = new Object();
  const Constructor = [].shift.call(arguments);

  obj.__proto__ = Constructor.prototype;

  const ret = Constructor.apply(obj, arguments);

  return typeof ret === "object" ? ret : obj;
}

实现一个 call

call 做了什么:

  • 将函数设为对象的属性
  • 执行&删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模拟 call bar.mycall(null);
// 实现一个call方法:
Function.prototype.myCall = function (context) {
  // 此处没有考虑context非object情况
  context.fn = this;
  let args = [];
  for (let i = 1, len = arguments.length; i < len; i++) {
    args.push(arguments[i]);
  }
  context.fn(...args);
  let result = context.fn(...args);
  delete context.fn;
  return result;
};

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

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

模拟 Object.create

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

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

实现类的继承

类的继承在几年前是重点内容,有 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

实现 JSON.parse

var json = '{"name":"cxk", "age":25}';
var obj = eval("(" + json + ")");

此方法属于黑魔法,极易容易被 xss 攻击,还有一种 new Function 大同小异。
简单的教程看这个半小时实现一个 JSON 解析器

实现 Promise

具体过程可以看这篇史上最易读懂的 Promise/A+ 完全实现

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 确实会像预期的那样只调用一个onFulfilled / onRejected
        // 所以增加了一个flag来防止resolveOrReject被多次调用
        var thenAlreadyCalledOrThrow = false;
        if (typeof then === "function") {
          // 是thenable 那么尝试展开
          // 并且在该thenable状态改变之前this对象的状态不变
          then.bind(value)(
            // onFulfilled
            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 (onFulfilled, onRejected) {
    // 返回值穿透以及错误穿透, 注意错误穿透用的是throw而不是return,否则的话
    // 这个then返回的promise状态将变成resolved即接下来的then中的onFulfilled
    // 会被调用, 然而我们想要调用的是onRejected
    if (typeof onFulfilled !== "function") {
      onFulfilled = 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"
                ? onFulfilled(this.data)
                : onRejected(this.data);
            resolve(value);
          } catch (e) {
            reject(e);
          }
        }.bind(this)
      );
    };

    // then 接受两个函数返回一个新的Promise
    // then 自身的执行永远异步与onFulfilled/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 promise 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 promise 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);
    }
  });
};
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容

  • 1.实现防抖函数(debounce) 防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计...
    张小明_to阅读 352评论 0 2
  • 前言 为了方便现在和以后前端学习和面试,在此收集和整理了Js相关的笔试面试题,供自己查阅的同时,希望也会对大家有所...
    蛙哇阅读 1,830评论 0 8
  • 1. JS创建变量的5种方式?varletconstfunctionexport/import 2. var,le...
    Angel_6c4e阅读 811评论 0 13
  • JavaScript笔试部分 点击关注本公众号「程序员面试官」获取文档最新更新,并可以领取配套于本指南的 《前端面...
    寻找海蓝阅读 963评论 0 24
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53