# 前端面试手写代码

# 1. 为数字添加千位分隔符

function milliFormat(num) {
    return num && num.toString()
        .replace(/\d+/, function(s){
             return s.replace(/(\d)(?=(\d{3})+$)/g, '$1,')
         })
}

toLocalString方法也可以实现千位分隔符

  • **x(?=y) 匹配'x'仅仅当x后面跟着'y'**
  • **(?:x) 仅分组,不记录匹配项** -$1,$2就是按顺序对应小括号里面的正则 捕获到的内容。

# 2. 单例模式

class Single{
  constructor(...args) {
    console.log(...args)
  }
}

Single.getInstance = (function() {
  let instance 
  return function(...args) {
    if (!instance) {
      instance = new Single(...args)
    }
    return instance
  }
})()


// 测试
const a = Single.getInstance('yang', 18)
const b = Single.getInstance('mike', 'male')

a === b //true
1. getInstance是类的静态方法,不能使用new SingleObject()
2. 创建出来的实例全等

# 3. 观察者模式

// 目标对象
class Subject{
  constructor() {
    this.subs = []
  }
  addSub = (sub) => {
    this.subs.push(sub)
  }
  notify = (...args) => {
    this.subs.map(sub => {
      sub.update(...args)
    })
  }
}


// 观察者
class Observer{
  update(...args) {
    console.log(...args)
  }
}


// 测试
const subject = new Subject()
const observer = new Observer()
subject.addSub(observer)
subject.notify('a', 'b', 'c')
首先是目标的构造函数,他有个数组,用于添加观察者。
还有个广播方法notify,遍历观察者数组后调用观察者们的update方法。

# 4. 发布订阅模式

class EventEmitter{
  constructor() {
    this._eventpool = {}
  }
  on(event, cb) {
    this._eventpool[event] 
    ? this._eventpool[event].push(cb)
    : this._eventpool[event] = [cb]
  }
  emit(event, ...args) {
    if (this._eventpool[event]) {
      this._eventpool[event].map(cb => {
        cb(...args)
      })
    }
  }
  off(event) {
    delete this._eventpool[event]
  }
  once(event, cb) {
    this.on(event, (...args) => {
      cb(...args)
      this.off(event)
    })
  }
}


// 测试
const emitter = new EventEmitter()
const cb = (...args) => {
   console.log(...args)
}
emitter.on('a', cb)
emitter.emit('a', 'y', 'g', 'w')
emitter.emit('a', 'y', 'g', 'w')

emitter.once('b', cb)
emitter.emit('b', 'y', 'g', 'w')
emitter.emit('b', 'y', 'g', 'w')
基于一个主题/事件通道,订阅者subscriber通过自定义事件订阅主题,发布者publisher通过发布主题事件的方式发布。

观察者模式和发布订阅模式区别

观察者模式和发布订阅模式区别:

1. 在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。
2. 发布订阅模式相比观察者模式多了个主题/事件通道,订阅者和发布者不是直接关联的。
3. 观察者模式两个对象之间有很强的依赖关系;发布/订阅模式两个对象之间的耦合度低。

# 5. 函数柯里化

当我们没有重新定义 toString 与 valueOf 时,函数的隐式转换会调用默认的 toString 方法,它会将函数的定义内容作为字符串返回。

而当我们主动定义了 toString/vauleOf 方法时,那么隐式转换的返回结果则由我们自己控制了。其中 valueOf的优先级会比 toString 高一点。

柯里化好处:参数复用、延迟运行(返回函数,想什么时候运行什么时候运行)
function currying() {
  const [fn, ...args] = [...arguments]
  const cb = function() {
    if (!arguments.length) {
      return fn.apply(this, args)
    }
    args.push(...arguments)
    return cb
  }
  
  // 视情况添加
  cb.toString = () => {
    return fn.apply(this, args)
  }
  return cb
}

function add() {
  return [...arguments].reduce((sum, item) => sum + item, 0)
}

// 测试柯里化
const a = currying(add, 12, 24, 36)
a()
a(123)(123)(123)

# 6. 实现一个add方法,使结果满足如下预期

add(1)(2)(3) = 6; 
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
  const args = [...arguments]
  const cb = function() {
    args.push(...arguments)
    return cb
  }
  
  cb.toString = () => {
    return args.reduce((sum, item)=> sum + item)
  }
  return cb
}

# 7. 实现 call、apply 方法

Function.prototype.myCall = function() {
  let [context = window, ...args] = [...arguments]
  
  // 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
  const key = Symbol('key')
  context[key] = this

  const res = context[key](...args)
  
  delete context[key]
  return res
}


Function.prototype.myApply = function() {
  // 和call的不同点在于没有...,因为只有两个参数(context 和 数组)
  let [context = window, args = []] = [...arguments] 
  
  // 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
  const key = Symbol('key')
  context[key] = this

  let res = context[key](...args);
  
  delete context[key]
  return res
}

# 8. 实现 bind 方法

Function.prototype.myBind = function() {
  const [context, ...args] = [...arguments]
  const fn = this
  
  const newFn = function() {
    const newArgs = args.concat(...arguments)

    if (this instanceof newFn) {
      return fn.apply(this, newArgs)
    }
    return fn.apply(context, newArgs)
  }

  newFn.prototype = Object.create(fn.prototype)
  return newFn
}

测试:

const obj = {
  name: 'yang',
  getName() {
    console.log(this.name)
  }
}
obj.getName()
const obj2 = {
  name: 'mike'
}
const getName2 = obj.getName.bind(obj2)
getName2()

测试构造函数:

function Test(name) {
  this.name = name
}

const newTest = Test.myBind (obj)

const obj = {
  name: 'yang'
}

const instance = new Test('haha')
const instance2 = new newTest('hehe')

console.log(instance, instance2)

# 9. 实现 instanceof 方法

instanceof运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object返回true,就表示obj对象是Object的实例。 instanceof的原理是检查右边构造函数的prototype属性,是否在左边对象的原型链上。

// 思路:右边变量的原型存在于左边变量的原型链中。
function myInstanceof(left, right) {
  let leftVal = left.__proto__
  let rightVal = right.prototype
  
  while(true) {
    if (leftVal === null) return false
    if (leftVal === rightVal) return true
    leftVal = leftVal.__proto__
  }
}

# 10. new 的本质

1. 创建一个新对象且将其隐式原型指向构造函数原型
2. 执行构造函数
3. 返回该对象
function myNew() {
  const [fn, ...args] = [...arguments]
  const obj = {
    __proto__: fn.prototype
  }
  fn.call(obj, ...args)
  return obj
}

# 11. Object.create 的基本实现原理

1. 创建一个空的构造函数
2. 将传入的对象作为其原型
3. 用该构造函数创建新的对象,并返回
function myCreate(proto) {
  function F(){}
  F.prototype = proto
  return new F()
}

生成实例对象的常用方法是,使用new命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?

JavaScript 提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};

// 实例对象
var B = Object.create(A);

Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true

上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

# 12. 实现防抖和节流

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。

区别:
(1) 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,
    而函数防抖只是在最后一次事件后才触发一次函数。
(2) 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,
    而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

应用: 进行窗口的resize、scroll,输入框内容校验或请求ajax时
function debounce(fn, time) {
  let timer
  
  return function() {
    const _this = this
    const args = [...arguments]
    if(timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(_this, args)
    }, time)
  }
}
function throttle(fn, time) {
  let timer 
  
  return function() {
    const _this = this
    const args = [...arguments]
    if (!timer) {
      setTimeout(() => {
        timer = null // 注意,一定要先置为null,再执行fn
        fn.apply(_this, args)
      }, time)
    }
  }
}
// 测试
function count() {
 console.log('xxxxx')
}
window.onscroll = debounce(count, 500)
window.onscroll = throttle(count, 500)

# 13. 用 for 和 reduce 实现 map 和 filter

// for 实现 map

Array.prototype.myMap = function() {
  const arr = this
  const [fn, thisArg] = [...arguments]
  const res = []
  
  for (let i = 0; i < arr.length; i++) {
    res.push(fn.call(thisArg, arr[i], i, arr))
  }
  return res
}
// for 实现 filter

Array.prototype.myFilter = function() {
  const arr = this
  const [fn, thisArg] = [...arguments]
  const res = []
  
  for (let i = 0; i < arr.length; i++) {
    // 与上面的唯一不同
    if (fn.call(thisArg, arr[i], i, arr)) {
      res.push(arr[i])
    }
  }
  return res
}
// reduce 实现 map
Array.prototype.myMap = function() {
  const arr = this
  const [fn, thisArg] = [...arguments]
  
  return arr.reduce((acc, item, index) => {
    acc.push(fn.call(thisArg, item, index, arr))
    return acc
  }, [])
}
// reduce 实现 filter
Array.prototype.myFilter = function() {
  const arr = this
  const [fn, thisArg] = [...arguments]
  
  return arr.reduce((acc, item, index) => {
    if (fn.call(thisArg, item, index, arr)) {
      acc.push(item)
    }
    return acc
  }, [])
}

# 14. 使用 for 循环打印1-10,每个数字间隔 100ms

// 使用 let
for (let i = 1; i < 11; i++) {
  setTimeout(() => {
    console.log(i)
  }, 500 * i)
}

/**
 * 使用闭包
 * 整个 for 循环内部是一个闭包,注意将i传进去
 */ 
for (var i = 1; i < 11; i++) {
  (function(i){
    setTimeout(()=> {
      console.log(i)
    }, 500 * i)
  })(i)
}

// 错误做法
for (var i = 0; i < 11; i++) {
  setTimeout(() => {
    console.log(i)
  }, 500 * i)
}

# 15. 使用 setTimeout 模拟 setInterval

function mySetInterval(fn, delay) {
  let timer;
  const interval = () => {
    fn();
    timer = setTimeout(interval, delay);
  };
  setTimeout(interval, delay);
  return {
    cancel: () => {
      clearTimeout(timer);
    },
  };
}

// 测试
const { cancel } = mySetInterval(() => console.log(888), 1000);
setTimeout(() => {
  cancel();
}, 4000);

setInterval有两个缺点:

  • 使用setInterval时,某些间隔会被跳过
  • 可能多个定时器会连续执行

setInterval 的回调函数并不是到时后立即执行,而是等系统计算资源空闲下来后才会执行。而下一次触发时间则是在 setInterval 回调函数执行完毕之后才开始计时,所以如果 setInterval 内执行的计算过于耗时,或者有其他耗时任务在执行,setInterval 的计时会越来越不准,延迟很厉害。

var startTime = new Date().getTime();
var count = 0;

//耗时任务
setInterval(function(){
    var i = 0;
    while(i++ < 100000000);
}, 0);

setInterval(function(){
    count++;
    console.log(new Date().getTime() - (startTime + count * 1000));
}, 1000);

代码里输出了setInterval触发时间和应该正确触发时间的延迟毫秒数

176
340
495
652
807
961
1114
1268
1425
1579
1734
1888
2048
2201
2357
2521
2679
2834
2996
......

setTimeout模拟setInterval好处:

  • 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
  • 保证定时器间隔(解决缺点二)

参考资料:

  1. 解决 setInterval 计时器不准的问题 (opens new window)

# 16. ES5 实现继承

function Parent() {
}
function Child() {
  Parent.call(this)
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

这种方法称为寄生组合式继承。

  • 寄生式继承的思路:在创建对象的函数中直接吸收其他对象的功能,然后对其进行扩展并返回。
  • 构造函数的目的是为了复制属性,Parent.call(this, name)肯定不能少
  • Child.prototype =new Parent()的目的是为了获取到父类原型对象(prototype)上的方法,基于这个目的,有没有别的方法可以做到 在不需要实例化父类构造函数的情况下,也能得到父类原型对象上的方法呢? 当然可以,我们可以采用寄生式继承来得到父类原型对象上的方法。
  • 那么使用Object.create的原型链是什么样的呢?其实很简单: SubType.prototype.__ proto __ = SuperType.protype 也就是说,子类的原型相当于是父类原型的一个实例,这不就是实现了两者的链接了吗?

其他方式的不足

function Parent() {
  this.name = 'parent'
}
Parent.prototype.say = function() {}

function Child() {
  Parent.call(this)
  this.type = 'child'
}
console.log(new Child().say()) // 报错

构造函数中的继承:

  • 只用Parent.call(this)的缺点是,原型链上的属性并没被继承
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child'
}
Child.prototype = new Parent()


const s1 = new Child()
const s2 = new Child()

console.log(s1.play, s2.play) //  [1, 2, 3]  [1, 2, 3] 
s1.play.push(4)
console.log(s1.play, s2.play)  // [1, 2, 3, 4] [1, 2, 3, 4]

单纯的原型链继承:

  • 使用Child.prototype = new Parent()的缺点是,子类实例的引用类型是公用的,修改一个实例会引起另一个实例的改变。
function Parent(){
  console.log('____')
  this.play = [1, 2, 3]
}
function Child() {
  Parent.call(this)
}

Child.prototype = new Parent()
const s1 = new Child()
const s2 = new Child()
console.log(s1.play, s2.play)
s1.play.push(4)
console.log(s1.play, s2.play)

简单的组合方式是,上述两种的相加。缺点是父级构造函数执行了两次

function Parent() {
  
}
function Child() {
  Parent.call(this)
}

Child.prototype = Parent.prototype

const s = new Child()
console.log(s.constructor) // Parent

Child.prototype = Parent.prototype的缺点是:

  • 无法区分一个实例是哪个原型对象创建的。若强行使用过Child.prototype.constructor=Child,会使得父类对象的contructor属性也是Child,显然错误。
  • 并且,当我们想要在子对象原型中扩展一些属性以便之后继续继承的话,父对象的原型也会被改写,因为这里的原型对象实例始终只有一个,这也是这种继承方式的缺点。

# 17. 实现 compose

/**
 * 实现以下功能:compose([a, b, c])('参数') => a(b(c('参数')))
 */
function compose(funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

// 另一种
function compose(funcs) {
  let index = funcs.length - 1
  
  return function() {
    let res = funcs[index].call(null, ...arguments)
    while (--index >= 0) {
      res = funcs[index].call(null, res)
    }
    return res
  }
}


// 测试
const a = x => x * 2
const b = x => x + 5
const c = x => x * 3
compose([a, b, c])(123)

# 18. 实现深拷贝

1. 使用`JSON.stringify`和`JSON.parse`实现深拷贝:`JSON.stringify`把对象转成字符
   串,再用`JSON.parse`把字符串转成新的对象;

   缺陷:它会抛弃对象的`constructor`,深拷贝之后,不管这个对象原来的构造函数是什么,
   在深拷贝之后都会变成`Object`;这种方法能正确处理的对象只有 `Number`, `String`,
   `Boolean`, `Array`, 扁平对象,也就是说,只有可以转成`JSON`格式的对象才可以这样
   用,像`function`没办法转成`JSON`;

2. `slice`是否为深拷贝
   `slice()`和`concat()`都并非深拷贝,而是只拷贝第一层。
function deepCopy(obj, cache = new WeakMap()) {
  if (!(obj instanceof Object)) return obj

  // 防止循环引用
  if (cache.get(obj)) return cache.get(obj)

  // 支持函数
  if (obj instanceof Function) {
    // return new Function('return '+obj.toString())()
    // 错误,对于`say(){}`这样的函数会报错

    return eval(obj)
  }

  // 支持日期
  if (obj instanceof Date) return new Date(obj)

  // 支持正则对象
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)

  // 数组是 key 为数字素银的特殊对象
  const res = Array.isArray(obj) ? [] : {}

  // 缓存 copy 的对象,用于处理循环引用的情况
  cache.set(obj, res)

  Object.keys(obj).forEach((key) => {
    if (obj[key] instanceof Object) {
      res[key] = deepCopy(obj[key], cache)
    } else {
      res[key] = obj[key]
    }
  });
  return res
}

// 测试
const source = {
  name: 'Jack',
  meta: {
    age: 12,
    birth: new Date('1997-10-10'),
    ary: [1, 2, { a: 1 }],
    say() {
      console.log('Hello');
    }
  }
}
source.source = source
const newObj = deepCopy(source)

console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false

参考资料:

  1. 深拷贝, 简书 (opens new window)
  2. 如何写出一个惊艳面试官的深拷贝? (opens new window)

# 20. 数组扁平化

function recursionFlat(arr) {
  const res = [];
  arr.map((item) => {
    if (Array.isArray(item)) {
      res.push(...recursionFlat(item));
    } else {
      res.push(item);
    }
  });
  return res;
}

function reduceFlat(arr) {
  return arr.reduce(
    (acc, item) => res.concat(Array.isArray(item) ? reduceFlat(item) : item),
    []
  );
}

const source = [1, 2, [3, 4, [5, 6]], "7"];
console.log(recursionFlat(arr));
console.log(reduceFlat(arr));

# 21. 对象扁平化

function objectFlat(obj = {}) {
  const res = {};

  _objectFlat(obj, res);
  return res;
}

function _objectFlat(item, res, preKey = "") {
  Object.entries(item).forEach(([key, val]) => {
    const newKey = preKey ? `${preKey}.${key}` : key;
    if (val && typeof val === "object") {
      _objectFlat(val, res, newKey);
    } else {
      res[newKey] = val;
    }
  });
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
console.log(objectFlat(source));

# 22. 图片懒加载

// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
  const position = el.getBoundingClientRect()
  const windowHeight = document.documentElement.clientHeight
  // 顶部边缘可见
  const topVisible = position.top > 0 && position.top < windowHeight;
  // 底部边缘可见
  const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
  return topVisible || bottomVisible;
}

function imageLazyLoad() {
  const images = document.querySelectorAll('img')
  for (let img of images) {
    const realSrc = img.dataset.src
    if (!realSrc) continue
    
    if (isVisible(img)) {
      img.src = realSrc
      img.dataset.src = ''
    }
  }
}

// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))

# 23. 手动编写一个ajax,不依赖第三方库

function ajax() {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', '/api', false)
    xhr.onreadystatechange = function(){
        if (xhr.readyState === 4 && xhr.status === 200){
            console.log(xhr.responceText)
        }
    }
    xhr.send(null)
}

# 23.1. xhr.readyState 有几种状态?

  • 0,未初始化(还没有调用send方法);
  • 1,载入(已经调用send方法,正在发送请求);
  • 2,载入完成(send方法执行完成,已经收到全部响应内容);
  • 3,交互(正在解析响应的内容);
  • 4,完成(响应内容解析完成,可以在客户端调用了)

# 24. 实现 a == 1 && a == 2 && a == 3true

// 第一种方法
var a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
console.log(a == 1 && a == 2 && a == 3) // true

// 第二种方法
var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

// 第三种方法
var val = 0;
Object.defineProperty(window, 'a', {
    get: function () {
        return ++val;
    }
});
console.log(a == 1 && a == 2 && a == 3) // true