手撕高频JavaScript面试题,祝你轻松斩获offer

1,516 阅读4分钟

前言

梳理总结一遍面试比较经常碰到的 JavaScript 手撕题,方便面试的时候复习,同时也巩固一下 JS 的基本功。

数据类型判断

function type_of(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1)
}

console.log('数据类型判断 :>> ',  typeof1([])); // Array

作用域安全的构造函数

直接看代码:

function Person(name, age) {
    if (this instanceof Person) {
        this.name = name
        this.age = age
    } else {
        return new Person(name, age)
    }
}

上面例子中,Person 构造函数使用 this 给属性赋值。当和 new 关键字连用时会创建一个新的对象,问题在当没有使用 new 关键字的情况下,this 会映射到全局对象 window 上,导致错误对象属性增加。

实现setInterval

原生的setInterval会出现两个问题:

  • 某些间隔会被跳过
  • 多个定时器的代码执行之间的间隔可能会比预期的小。

直接看代码:

setTimeout(function() {
    // 处理逻辑
    setTimeout(arguments.callee, wait)
}, wait)

上述模式链式调用了 setTimeout,每次函数执行的时候会创建一个新的定时器,内部的 setTimeout 调用使用 arguments.callee 来获取对当前执行函数的引用,并为其设置另外一个定时器。在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会缺失时间间隔。同时,也确保了下一次定时器执行之前,至少要等待指定的间隔,避免了连续的运行。

惰性函数

所谓惰性载入,指函数执行的分支只会发生一次。一般因为各浏览器之间的行为差异,经常会在函数中包含了大量的 if 语句,以检查浏览器特性,解决不同浏览器的兼容问题。

方式一:

function createXHR() {
    if (typeof XMLHttpRequest !== 'undefined) {
        createXHR = function() {...}
    } else {
        createXHR = function() {...}
    }
    return createXHR()
}

方式二:

const createXHR = (function() {
    if (typeof XMLHttpRequest !== 'undefined) {
        return createXHR = function() {...}
    } else {
        return createXHR = function() {...}
    }
})

继承

原型链继承会出现子类在实例化的时候不能给父类构造函数传参;原型中包含的引用类型属性将被所有实例共享;借用构造器继承会出现子类不能继承父类原型上属性;组合式继承则会两次调用构造函数。最好的继承方式是寄生组合式继承。ES6 关键字extends 底层实现也是采用寄生组合式继承。所以这边只实现寄生组合继承,代码如下:

function People(name) {
  this.name = name
}
function ChinesePeople(name, age) {
  this.age = age
  People.call(this, name)
}
function _extends(parent, son) {
  function Middle() {
    this.constructor = son
  }
  Middle.prototype = parent.prototype
  let middle = new Middle()
  return middle
}
ChinesePeople.prototype = _extends(People, ChinesePeople)

let chinesePeople = new ChinesePeople('lisi', 18)
console.log('寄生组合式继承:', chinesePeople);
// 寄生组合式继承: ChinesePeople { age: 18, name: 'lisi' }

数组去重

ES5 实现:

function unique(arr) {
  let res = []
  for(let i = 0; i< arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) {
      res.push(arr[i])
    }
  }
  return res
}

ES6 实现:

const unique1 = (arr) => [...new Set(arr)]

console.log('数组去重 :>> ', unique1([undefined,undefined,1,2,2,2,3,4,5]));
// 数组去重 :>>  [ undefined, 1, 2, 3, 4, 5 ]

数组扁平化

ES5 实现:

function flatten(arr) {
  let res = []
  for(let i = 0; i< arr.length; i++) {
    if (Array.isArray(arr[i])) {
      res = res.concat(flatten(arr[i]))
    } else {
      res.push(arr[i])
    }
  }
  return res
}
console.log('数组扁平化 :>> ', flatten([1, [2,3]])); // [1,2,3]

ES6 实现:

function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

浅拷贝

只拷贝第一层对象,深层次的属性改变,拷贝对象也会收到影响。

function shallowCopy(obj) {
  if (typeof obj !== 'object') return obj
  let newObj = obj instanceof Array ? [] : {}
  for(let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    }
  }
  return newObj
}
let a = {
  a: 1,
  b: {b:2},
  c: [3],
  d: Date
}
let b = shallowCopy(a)
b.b.b = 100
console.log('浅拷贝 :>> ', b, a);
// 浅拷贝 :>>  { a: 1, b: { b: 100 }, c: [ 3 ], d: [Function: Date] } 
// { a: 1, b: { b: 100 }, c: [ 3 ], d: [Function: Date] }

深拷贝

每个对象属性都用新的变量空间存储,改变属性值不会相互影响。

function deepClone (target, map = new Map()) {
  let constructor = target.constructor
  if (/^RegExp|Date$/i.test(constructor.name)) {
    return new constructor(target)
  }
  if (target && typeof target === 'object') {
    let clone = Array.isArray(target) ? [] : {}
    if (map.get(target)) {
      return map.get(target)
    }
    map.set(target, clone)
    for (let key in target) {    
      clone[key] = deepClone(target[key], map)
    }
    return map.get(target)
  } else {
    return target
  }
}
let a1 = {
  a: 1,
  b: {b:2},
  c: Symbol(3),
  d: new Date()
}
let b1 = deepClone(a1)
b1.b.b = 100
console.log('深拷贝 :>> ', b1, a1);
// 深拷贝 :>>  { a: 1, b: { b: 100 }, c: Symbol(3), d: 2021-12-06T09:10:20.403Z } 
// { a: 1, b: { b: 2 }, c: Symbol(3), d: 2021-12-06T09:10:20.403Z }

函数防抖

触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。

function debounce(fn, wait) {
  let timeoutId
  return function() {
    if (timeoutId) {
      const args = arguments
      const context = this
      clearTimeout(timeoutId)
      timeoutId = setTimeout(function(){
        fn.apply(context, args)
      }, wait)
    }
  }
}

函数节流

触发高频事件,且 N 秒内只执行一次。

function throttle(fn, wait) {
  let timer
  return function() {
    if (timer) return
    const args = arguments
    const context = this
    timer = setTimeout(function() {
      fn.aplly(context, args)
      timer = null
    }, wait)
  }
}

实现instanceof

function instance_of(L, R) {
  let O = R.prototype
  L = L.__proto__ 
  while(true) {
    if (L === null) return false
    if (L === O) return true

    L = L.__proto__
  }
}
console.log('实现instanceof :>> ', instance_of([], String));
// 实现instanceof :>>  false

实现new

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
Function.prototype.myCall = function(context) {
  var context = context || window
  context.fn = this
  const args = Array.prototype.slice.call(arguments, 1);
  let result = context.fn(...args)
  delete context.fn

  return result
}
function bar(name, age) {
  return {
    name,
    age
  }
}
console.log('模拟call', bar.myCall(this, 'lisi', 18));
// 模拟call { name: 'lisi', age: 18 }

实现apply

跟call实现同理。

Function.prototype.myApply = function(context, arr) {
  var context = context || window
  context.fn = this
  let result
  if (!arr) {
    result = context.fn()
  } else {
    result = context.fn(...arr)
  }
  delete context.fn
  return result
}
function bar(name, age) {
  return {
    name,
    age
  }
}
console.log('模拟apply', bar.myApply(this, ['lisi',18]));
// 模拟apply { name: 'lisi', age: 18 }

实现bind

简易版本实现:

Function.prototype.bind1 = function(context, ...args) {
  var context = context || window
  return (...newargs) => {
    return this.myCall(context, ...args, ...newargs)
  }
}

let binds = bar.bind(this, 'zhangsan', 20)
console.log('模拟bind :>> ', binds());
// 模拟bind :>>  { name: 'zhangsan', age: 20 }

柯里化函数

柯里化就是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下参数返回结果的一种应用。

function curry(fn, ...args) {
  return fn.length > args.length 
  ? (...arguments) => curry(fn, ...args, ...arguments)
  : fn(...args)
}

let addSum = (a, b, c) => a+b+c
let add = curry(addSum)
console.log('柯里化函数',add(1)(2)(3), add(1, 2)(3), add(1,2,3))
// 柯里化函数 6 6 6

偏函数

什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。

function partial(fn, ...args) {
  return (...newargs) => {
    return fn(...args, ...newargs)
  }
}
let partialAdd = partial(add, 1)
console.log('偏函数 :>> ', partialAdd(2, 3));
// 偏函数 :>>  6

实现sleep

某个时间后就去执行某个函数,使用Promise封装。

function sleep (fn, wait) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(fn)
    }, wait)
  })
}
// let saySomething = (name) => console.log(`hello,${name}`)
// async function autoPlay() {
//   let demo = await sleep(saySomething('张三'),1000)
//   let demo2 = await sleep(saySomething('李四'),1000)
//   let demo3 = await sleep(saySomething('xxs'),1000)
// }
// autoPlay()

手写ajax

const getJSON = function(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    })
}

实现trim

去掉首位多余的空格。

String.prototype.trim = function(){
    return this.replace(/^\s+|\s+$/g, '')
}
//或者 
function trim(string){
    return string.replace(/^\s+|\s+$/g, '')
}

console.log('trim :>> ', trim(' ssposc '));
// ssposc

实现数组reduce

简易版本实现:

Array.prototype.myReduce = function(fn, initVal) {
  let result = initVal, i = 0
  if (typeof initVal === 'undefined') {
    result = this[i]
    i++
  }
  while(i < this.length) {
    result = fn(result, this[i])
    i++
  }
  return result
}

const result = [1,2,3].myReduce((a, b) =>  a + b
, 1)
console.log('reduce :>> ', result);

实现Object.create


function create(proto) {
    function Fn() {};
    Fn.prototype = proto;
    Fn.prototype.constructor = Fn;
    return new Fn();
}
let demo = {
    c : '123'
}
let cc = Object.create(demo)

JSONP

JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;

const jsonp = ({ url, params, callbackName }) => {
    const generateUrl = () => {
        let dataSrc = ''
        for (let key in params) {
            if (params.hasOwnProperty(key)) {
                dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`
        return `${url}?${dataSrc}`
    }
    return new Promise((resolve, reject) => {
        const scriptEle = document.createElement('script')
        scriptEle.src = generateUrl()
        document.body.appendChild(scriptEle)
        window[callbackName] = data => {
            resolve(data)
            document.removeChild(scriptEle)
        }
    })
}

事件总线

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
	console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
	console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, 'lisi', 20)

字符串模板

function render(template, data) {
    const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
    if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的结构
    }
    return template; // 如果模板没有模板字符串直接返回
}

测试:

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = { name: 'lisi', age: 20 } render(template, person); 
// 我是lisi,年龄20,性别undefined

实现快速排序

function sort(arr) {
    if (!arr.length) return
    
    quickSort(arr, 0, arr.length - 1)
}

function quickSort(arr, start, end) {
    if (start >= end) return
    
    let left = start, right = end
    let pivot = arr[Math.floor((left + end) / 2)]
    
    while (left <= right) {
        while (left <= right && arr[left] < pivot) {
            left++
        }
        
        while (left <= right && arr[right] > pivot) {
            right--
        }
        
        if (left <= right) {
            let temp = arr[left]
            arr[left] = arr[right]
            arr[right] = temp
            right--
            left++
        }
    }
    quickSort(arr, start, right)
    quickSort(arr, left, end)
}

实现冒泡排序

function sortarr(arr) {    
    for (let i = 0; i < arr.length - 1; i++) {
        for (let j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return arr
}