前端面试与进阶指南
20W字囊括上百个前端面试题的项目开源了
实现防抖函数(debounce)
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:1
2
3
4
5
6
7
8
9// 防抖函数const debounce = (fn, delay) => {
  let timer = null;
  return(...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
生存环境请用lodash.debounce
实现节流函数(throttle)
防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。1
2
3
4
5
6
7
8
9
10
11
12
13// 手写简化版
 
    // 节流函数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)
简单版:1
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无法实现对函数 、RegExp等特殊对象的克隆
- 会抛弃对象的constructor,所有的构造函数会指向Object
- 对象有循环引用,会报错
面试版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75/**
 * deep clone
 * @param  {[type]} parent object 需要进行克隆的对象
 * @return {[type]}        深克隆后的对象
 */const clone = parent => {
  // 判断类型const isType = (obj, type) => {
    if (typeof obj !== "object") returnfalse;
    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) returnnull;
    if (typeof parent !== "object") return parent;
    let child, proto;
    if (isType(parent, "Array")) {
      // 对数组做特殊处理
      child = [];
    } elseif (isType(parent, "RegExp")) {
      // 对正则对象做特殊处理
      child = newRegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } elseif (isType(parent, "Date")) {
      // 对Date对象做特殊处理
      child = newDate(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);
};
局限性:
- 一些特殊情况没有处理: 例如Buffer对象、Promise、Set、Map
- 另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间
原理详解实现深克隆
实现Event(event bus)
event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。
简单版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26classEventEmeitter{
  constructor() {
    this._events = this._events || newMap(); // 储存事件/回调键值对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);
  }
  returntrue;
};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 将type事件以及对应的fn函数放入this._events中储存if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};
面试版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83classEventEmeitter{
  constructor() {
    this._events = this._events || newMap(); // 储存事件/回调键值对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);
  }
  returntrue;
};
// 监听名为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);
    }
  }
  returntrue;
};
// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  const handler = this._events.get(type); // 获取对应事件名称的函数清单if (!handler) {
    this._events.set(type, fn);
  } elseif (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 {
      returnthis;
    }
  }
};
实现具体过程和思路见实现event
实现instanceOf
| 1 | // 模拟 instanceof | 
模拟new
new操作符做了这些事:
- 它创建了一个全新的对象
- 它会被执行[[Prototype]](也就是proto)链接
- 它使this指向新创建的对象
- 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
- 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用
| 1 | // objectFactory(name, 'cxk', '18') | 
实现一个call
call做了什么:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定this到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
| 1 | // 模拟 call bar.mycall(null); | 
实现apply方法
apply原理与call很相似,不多赘述1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 模拟 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失效,但是传入的参数依然有效
| 1 | // mdn的实现 | 
模拟Object.create
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。1
2
3
4
5
6
7// 模拟 Object.create
function create(proto) {
  functionF() {}
  F.prototype = proto;
  return new F();
}
实现类的继承
类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function Parent(name) {
    this.parent = name
}
Parent.prototype.say = function() {
    console.log(`${this.parent}: 你打篮球的样子像kunkun`)
}
functionChild(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: 你打篮球的样子像kunkunvar child = new Child('cxk', 'father');
child.say() // father好,我是练习时长两年半的cxk
实现JSON.parse
| 1 | var json = '{"name":"cxk", "age":25}'; | 
此方法属于黑魔法,极易容易被xss攻击,还有一种new Function大同小异。
简单的教程看这个半小时实现一个 JSON 解析器
实现Promise
我很早之前实现过一版,而且注释很多,但是居然找不到了,这是在网络上找了一版带注释的,目测没有大问题,具体过程可以看这篇史上最易读懂的 Promise/A+ 完全实现
| 1 | var PromisePolyfill = (function () { | 
解析 URL Params 为对象
| 1 | let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled'; | 
模板引擎实现
| 1 | let template = '我是{{name}},年龄{{age}},性别{{sex}}'; | 
转化为驼峰命名
| 1 | var s1 = "get-element-by-id"// 转化为 getElementById | 
查找字符串中出现最多的字符和个数
例: abbcccddddd -> 字符最多的是d,出现了5次1
2
3
4
5
6
7
8
9
10
11
12
13
14let 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}次`);
字符串查找
请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
functionisContain(a, b) {
  for (let i in b) {
    if (a[0] === b[i]) {
      let tmp = true;
      for (let j in a) {
        if (a[j] !== b[~~i + ~~j]) {
          tmp = false;
        }
      }
      if (tmp) {
        return i;
      }
    }
  }
  return-1;
}
实现千位分隔符
| 1 | // 保留三位小数 | 
正则表达式(运用了正则的前向声明和反前向声明):1
2
3
4
5function parseToMoney(str){
    // 仅仅对位置进行匹配
    let re = /(?=(?!\b)(\d{3})+$)/g; 
   return str.replace(re,','); 
}
判断是否是电话号码
| 1 | function isPhone(tel) { | 
验证是否是邮箱
| 1 | function isEmail(email) { | 
验证是否是身份证
| 1 | function isCardNo(number) { |