来咯来咯!2020最新web前端面试题总结(欢迎收藏)

开头

这些内容送给正在准备面试中的小伙伴,有一些内容会写得特别特别详细,而有一些内容则写得比较少,但是保证里面的很多内容都是干货,很多都有详细的解释,干货都在后面啊,继续往下看吧。。。各位路过的小哥哥小姐姐们,希望看完了对你有所帮助。万字长文~~~如果你看完了所有内容算我输!!!

Let's go!!!

一些JS的基础题

Javascript中的数据类型(8种)

  • 简单数据类型:Number、String、Boolean、undefined、null、Symbol、Bigint(ES2020新增)
  • 复杂数据类型:Object Object又有一些子类型,Array、Function、RegExp、Date这些都属于Object类型

for循环setTimeout打印输出

如果不采用立即执行函数或者let的形式就会直接打印出10个10,通过采取闭包或者let有了块级作用域之后就不会出现这样的问题

for (var i = 0; i < 10; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j)
    }, 1000)
  })(i)
}
复制代码

给定时器传入第三个参数, 定时器可以传多个参数给定时器函数,此处将外层的i传递给了定时器中回调函数作为参数使用。

for(var i = 1;i <= 5; i++){
  setTimeout(function timer(j){
    console.log(j)
  }, 0, i)
}
复制代码

用let给定块级作用域

for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  }, 1000)
}
复制代码

关于setTimeout的一些冷知识

  • 由于消息队列的机制,不一定能按照自己设置的时间执行
  • settimeout嵌套settimeout时,系统会设置最短时间间隔为4ms
  • 未激活的页面,settimeout最小时间间隔为1000ms
  • 延时执行时间最大值为2147483647(32bit),溢出这个值会导致定时器立即执行 setTimeout(()=> { console.log('这里会立即执行') } ,2147483648) 复制代码

数组扁平化的方法

//使用ES6中的Array.prototype.flat方法
arr.flat(Infinity)
复制代码
//使用reduce的方式
function arrFlat(arr) {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? arrFlat(cur) : cur)
  }, [])
}
复制代码
//使用递归加循环的方式
function arrFlat(arr) {
  let result = []
  arr.map((item, index) => {
    if (Array.isArray(item)) {
      result = result.concat(arrFlat(item))
    } else {
      result.push(item)
    }
  })
  return result
}
复制代码
//将数组先变成字符串,再复原 toString()
//这种方法存在缺陷,就是数组中元素都是Number或者String类型的才能展开
function arrFlat(arr) {
    return arr.toString().split(',').map(item=> +item)
}
复制代码

数组去重

定义去重数据

let arr = [1, 1, "1", "1", null, null, undefined, undefined, /a/, /a/, NaN, NaN, {}, {}, [], []]

复制代码

我们先看下几种不能去掉重复的引用数据类型的写法

// 使用 Set
let res = [...new Set(arr)]
console.log(res)
复制代码

来咯来咯!2020最新web前端面试题总结(欢迎收藏)_第1张图片

 

这种方法虽然很简洁,但是使用该种方法我们可以看到里面的引用数据类型并没有能成功去重,只能去除基本数据类型

//使用filter
let res = arr.filter((item, index) => {
  return arr.indexOf(item) === index
})
console.log(res)

复制代码
//使用reduce
let res = arr.reduce((pre, cur) => {
  return pre.includes(cur) ? pre : [...pre, cur]
}, [])
console.log(res)
复制代码

使用该两种方法也和上面的方法一样,不能去掉引用数据类型。

我们再来看一下如何去除引用类型的重复值

利用对象的hasOwnProperty方法进行判断对象上是否含有该属性,如果含有则过滤掉,不含有则返回新数组中

let obj = {}
let res = arr.filter(item => {
  if (obj.hasOwnProperty(typeof item + item)) {
    return false
  } else {
    obj[typeof item + item] = true
    return true
  }
})
console.log(res)
复制代码

 

来咯来咯!2020最新web前端面试题总结(欢迎收藏)_第2张图片

 

这次可以看到成功的将引用数据类型也去掉了。

除了以上这几种方法,还有一些循环遍历的方法也是类似的

类数组变成数组

类数组是具有length属性,但不具有数组原型上的方法。 比如说arguments,DOM操作返回的结果就是类数组。那么如何将类数组变成数组呢

  • Array.from(document.querySelectorAll('div'))
  • Array.prototype.slice.call(document.querySelectorAll('div'))
  • [...document.querySelectorAll('div')]

数据类型检测

typeof 1  	 	 // number
typeof '1'  	 // string
typeof undefined // undefined
typeof true      // boolean
typeof Symbol()  // symbol
复制代码

上面的几种类型都能正确的检测,但是引用数据类型除了函数都会显示为object,而且对于 typeof null 也是 object 这是历史遗留下的bug,因为怕影响到一些现有的web项目,所以一直没有修复这个bug。

当检测引用数据类型的时候,用instanceof比较好,它会基于原型链进行查询,如果查询结果在原型链中,就会返回true。

Object.prototype.toString.call(检测数据类型最佳方案)

调用Object原型上的toString()方法,并且通过call改变this指向。返回字符串 ,我们看看八种数据类型分别返回的结果

function checkType(param) {
  return Object.prototype.toString.call(param)
}

console.log(checkType(123)) //[object Number]
console.log(checkType("123")) //[object String]
console.log(checkType(true)) //[object Boolean]
console.log(checkType({ a: 123 })) //[object Object]
console.log(checkType(() => {})) //[object Function]
console.log(Symbol(1)) //Symbol(1)
console.log(null) //null
console.log(undefined) //undefined
复制代码

我们再对上述函数进行一下处理

function checkType(param) {
  return Object.prototype.toString.call(param).slice(8, -1).toLowerCase()
}

console.log(checkType(1)) // number
复制代码

Object.is和===的区别

Object.is在严格等于上的基础修复了一些特殊情况下的错误,比如NaN 不等于 NaN

function is(x, y){
    if(x === y){
        // 1/+0 = +Infinity  1/-0 = -Infinity 这两个是不相等的
        // 当 x和y都等于0的时候,就对x/0和y/0做判断
        return x !== 0 || y !== 0 || x / 0 === y / 0
    }
}
复制代码

==和===的区别和隐式数据类型转化

===是严格相等,左右两边不仅值要相等,类型也要相等,例如'1'===1的结果是false,因为左边是string,右边是number。

==只要值相等就会返回true,而且使用==时会发生隐式类型转化, 在js中,当运算符在运算时,如果两边数据不统一,CPU就无法计算,这时我们编译器会自动将运算符两边的数据做一个数据类型转换,转成一样的数据类型再计算 。

  • 转成string类型:+ 字符串连接符如 1 + "1" = "11"
  • 转成number类型: ++、--(自增自减运算符) + 、-、*、/、%(加减乘除取余算术运算符) >、 <、 >=、 <=、 ==、 !=、 ===、 !== (关系运算符) let i = "1" console.log(++i) // 2 复制代码
  • 转成boolean类型 : !(逻辑非运算符取反操作),使用Boolean转化除了下面这八种情况得到false以外,其它的情况都转为true 。0、-0、NaN、undefined、null、“”(空字符串)、false、document.all()
  • 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较

例子:

//字符串连接符
console.log(1 + 'true')// +是字符串连接符, String(1) + 'true',打印出'1true'

//算术运算符
console.log(1 + true) // +是算术运算符,true被Number(true)->1,打印出2
console.log(1 + undefined) // 1 + Number(undefined) -> 1 + NaN, 打印NaN
console.log(1 + null) // 1 + Number(null) -> 1 + 0,打印出1

//关系运算符
// 一边数字一边字符串,Number("2")
// 2 > 5,打印false
console.log("2" > 5)
// 两边字符串,调用"2".charCodeAt() -> 50 
// "5".charAtCode()-> 53, 打印false
console.log("2" > "5") 

//多个字符串从左往右匹配,也是调用charCodeAt方法进行比较
//比较"a".charCodeAt() < "b".charCodeAt(),打印false
console.log("abc" > "b") 

// 左边第一个"a"和右边第一个"a"的unicode编码相等
// 继续比较两边第二个字符, "b" > "a",打印true
console.log("abc" > "aaa") 

//无视上述规则自成体系
console.log(NaN == NaN) // NaN和任何数据比较都是 false
console.log(undefined == undefined) //true
console.log(undefined === undefined) //true
console.log(undefined == null) //true
console.log(undefined === null) //false
复制代码

对于复杂的数据类型,比如对象和数组

对象和数组和字符串类型比较:先使用valueOf() 取得原始值,如果原始值不是number类型,则用toString()方法转成字符串类型valueOf -> toString

//发生了a.valueOf().toString()的转化,打印true
console.log([1,2] == "1,2") 

// 发生了a.valueOf().toString()的转化,打印true
let a = {}
console.log(a == "[object Object]") 
复制代码

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

  • 如果有设置Symbol.toPrimitive()方法,会优先调用并返回数据
  • 调用valueOf(),如果转换为原始类型,则返回
  • 调用toString(),如果转换为原始类型,则返回
  • 如果没有返回原始类型,则报错

让我们来看俩个例子

let obj = {
  value: 3,
  valueOf() {
    return 4
  },
  toString() {
    return 5
  },
  [Symbol.toPrimitive]() {
    return 6
  },
}
console.log(obj + 1) //打印7
复制代码

让 if(a ==1 && a == 2 && a == 3)成立

let a = {
  value: 0,
  valueOf() {
    return ++a.value
  },
}
// 每次调用这个a对象的时候都会在0的基础上加1,调用3次后就变成了3
console.log(a == 1 && a == 2 && a == 3) //true
复制代码

如果是数组和对象与number类型比较,先用valueOf取得原始值,原始值不是number类型则调用toString,然后再将字符串类型用Number转成数字类型,调用顺序valueOf() -> toString() -> Number()

空数组的toString()方法会得到空字符串,而空对象的toString()方法会得到字符串[object Object]

//发生了这样的转化:Number([].valueOf().toString()),打印true
console.log([] == 0) 

//逻辑非运算符优先级大于关系运算符
//空数组转布尔得到true,然后取反得到false
//false = 0 ,打印true
console.log(![] == 0) 

//左边:{}.valueOf().toString()得到”[object Object]“,Number(”[object Object]“)->NaN
//右边:!{}得到false ,Number(false) -> 0
//两边不相等,打印false
console.log({} == !{})

//左边:[].valueOf().toString()得到空字符串
//右边:![] 得到false
// Number("") = Number(false) 两边都为0
//打印true
console.log([] == ![])

//因为引用数据类型存储在堆中的地址,左边和右边分别属于两块不同的空间
//他们地址不相同,所以两边不相等
//下面两种情况都打印false
console.log([] == [])
console.log({} == {})
复制代码

记录遇到的一个另一个相关问题

下面这三个的打印结果

//typof null返回的是object
console.log(typeof null)

//从右往左看,先看右边的typeof null整体,返回object之后
//再将整体看成typeof object
//打印结果为string,原因是typeof null返回的是object字符串
console.log(typeof typeof null)

//到这里也是从右往左看,相当于typeof string
//结果打印是string
console.log(typeof typeof typeof null)
复制代码

实现一个instanceof

function myInstanceof(left,right) {
    if(typeof left !== 'object' || left === null) return false
    //获取原型
    let proto = Object.getPrototypeOf(left)
    while(true){
        //如果原型为null,则已经到了原型链顶端,判断结束
        if(proto === null) return false
        //左边的原型等于右边的原型,则返回结果
        if(proto === right.prototype) return true
        //否则就继续向上获取原型
        proto = Object.getPrototypeOf(proto)
    }
}
复制代码

实现继承

ES5中实现继承

    //实现一下继承
    function Parent() {
      this.name = "大人"
      this.hairColor = "黑色"
    }

    function Child() {
      Parent.call(this)
      this.name = "小孩"
    }

    Child.prototype = Object.create(Parent.prototype)
	//将丢失的构造函数给添加回来
    Child.prototype.constructor = Child

    let c1 = new Child()
    console.log(c1.name, c1.hairColor) //小孩,黑色
    console.log(Object.getPrototypeOf(c1))
    console.log(c1.constructor) //Child构造函数

    let p1 = new Parent()
    console.log(p1.name, p1.hairColor) //大人,黑色
    console.log(Object.getPrototypeOf(p1))
    console.log(p1.constructor) //Parent构造函数
复制代码

ES6中实现继承

// ES6的继承
class Parent {
  constructor() {
    this.name = "大人"
    this.hairColor = "黑色"
  }
}

class Child extends Parent {
  constructor() {
    super()  //调用父级的方法和属性
    this.name = "小孩"
  }
}

let c = new Child()
console.log(c.name, c.hairColor) //小孩 黑色

let p = new Parent()
console.log(p.name, p.hairColor) //大人 黑色
复制代码

如何在ES5环境下实现const

此处需要用到Object.defineProperty(Obj,prop,desc)这个API

function _const (key, value) {
	const desc = {
        value,
        writable:false
    }
    Object.defineProperty(window,key,desc)
}

_const('obj',{a:1}) //定义obj
obj = {} //重新赋值不生效
复制代码

手写Call

//手写call
let obj = {
  msg: "我叫王大锤",
}

function foo() {
  console.log(this.msg)
}

// foo.call(obj)
//调用call的原理就跟这里一样,将函数挂载到对象上,然后在对象中执行这个函数
// obj.foo = foo
// obj.foo()

Function.prototype.myCall = function (thisArg, ...args) {
  const fn = Symbol("fn") // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
  thisArg = thisArg || window // 若没有传入this, 默认绑定window对象
  thisArg[fn] = this //this指向调用者
  const result = thisArg[fn](...args) //执行当前函数
  delete thisArg[fn]
  return result
}

foo.myCall(obj)
复制代码

手写apply

// 手写apply (args传入一个数组的形式),原理其实和call差不多,只是入参不一样
Function.prototype.myApply = function (thisArg, args = []) {
  const fn = Symbol("fn")
  thisArg = thisArg || window
  thisArg[fn] = this
  //虽然apply()接收的是一个数组,但在调用原函数时,依然要展开参数数组
  //可以对照原生apply(),原函数接收到展开的参数数组
  const result = thisArg[fn](...args)
  delete thisArg[fn]
  return result
}

foo.myApply(obj)
复制代码

手写bind

Function.prototype.myBind = function (thisArg, ...args) {
  let self = this //这里的this是指向thisArg(调用者)
  let fnBound = function () {
    //this instanceof self ? this : thisArg 判断是构造函数还是普通函数
    //后面的args.concat(Array.prototype.slice.call(arguments))是利用函数柯里化来获取调用时传入的参数
    self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
  }
  // 继承原型上的属性和方法
  fnBound.prototype = Object.create(self.prototype)
  //返回已经绑定的函数
  return fnBound
}

//通过普通函数调用
// foo.myBind(obj, 1, 2, 3)()

//通过构造函数调用
function fn(name, age) {
  this.test = "测试数据"
}
fn.prototype.protoData = "原型数据"
let fnBound = fn.myBind(obj, "王大锤", 18)
let newBind = new fnBound()
console.log(newBind.protoData) // "原型数据"

复制代码

另外之前看到的一个关于bind的问题,在此也收录一下

对于foo.bind(A).bind(B).bind(C) 这个问题

let obj = { a: 1 }
let obj2 = { a: 2 }
let obj3 = { a: 3 }
let obj4 = { a: 4 }

function foo() {
  console.log(this.a)
}

let boundFn = foo.bind(obj).bind(obj2).bind(obj3)
boundFn.call(obj4)  //打印结果为1
boundFn.apply(obj4) //打印结果为1
boundFn()  //打印结果为1

复制代码

由此我们可以看出bind是永久绑定,往后的操作都不会再更改其指向

模拟实现一个new操作符

模拟实现一个new操作符,传入一个构造函数和参数

function myNew(constructFn, ...args) {
  // 创建新对象,并继承构造方法的prototype属性,
  //把obj挂原型链上, 相当于obj.__proto__ = constructFn.prototype
  let obj = Object.create(constructFn.prototype)

  //执行构造函数,将args参数传入,主要是为了进行赋值this.name = name等操作
  let res = constructFn.apply(obj, args)

  //确保返回值是一个对象
  return res instanceof Object ? res : obj
}

function Dog(name) {
  this.name = name

  this.woof = function () {
    console.log("汪汪汪")
  }
  //构造函数可以返回一个对象
  //return { a: 1 }
}

let dog = new Dog("阿狸")
console.log(dog.name) //阿狸
dog.woof() //汪汪汪

let dog2 = myNew(Dog, "大狗")
console.log(dog2.name) //大狗
dog2.woof() //汪汪汪
复制代码

节流

节流可以控制事件触发的频率,节流就跟小水管一样,如果不加节流的话,水就会哗啦啦啦啦啦啦的流出来,但是一旦加了节流阀,你就可以自己控制水的流速了,加了节流后水可以从哗啦啦啦变成滴答滴答滴答,放到我们的函数事件里面说就是可以让事件触发变慢,比如说事件触发可以让它在每一秒内只触发一次,可以提高性能。

function throttle(fn, wait) {
    let prev = +new Date()
    return function() {
        let now = +new Date()
        /*当下一次事件触发的时间和初始事件触发的时间的差值大于
			等待时间时才触发新事件 */
        if(now - prev > wait) {
            fn.apply(this, arguments)
        }
        //重置初始触发时间
        prev = +new Date()
    }
}
复制代码

防抖

防抖就是可以限制事件在一定时间内不能多次触发,比如说你疯狂按点击按钮,一顿操作猛如虎,不加防抖的话它也会跟着你疯起来,疯狂执行触发的方法。但是一旦加了防抖,无论你点击多少次,他都只会在你最后一次点击的时候才执行。 防抖常用于搜索框或滚动条等的监听事件处理,可以提高性能。

function debounce(fn, wait = 50) {
    //初始化一个定时器
    let timer
    return function() {
        //如果timer存在就将其清除
        if(timer) {
            clearTimeout(timer)
        }
        //重置timer
        timer = setTimeout(() => {
            //将入参绑定给调用对象
            fn.apply(this, arguments)
        }, wait)
    }
}
复制代码

深拷贝和浅拷贝

浅拷贝: 顾名思义,所谓浅拷贝就是对对象进行浅层次的复制,只复制一层对象的属性,并不包括对象里面的引用类型数据 , 当遇到有子对象的情况时,子对象就会互相影响 ,修改拷贝出来的子对象也会影响原有的子对象

深拷贝: 深拷贝是对对象以及对象的所有子对象进行拷贝,也就是说新拷贝对象的子对象里的属性也不会影响到原来的对象

我们先定义一个对象

let obj = {
  a: 1,
  b: 2,
  c: {
    d: 3,
    e: 4
  }
}

复制代码

实现浅拷贝

使用Object.assign()

let obj2 = Object.assign({}, obj)
obj2.a = 111
obj2.c.e = 555
console.log(obj)
console.log(obj2)

复制代码

使用展开运算符

let obj2 = {...obj}
obj2.a = 111
obj2.c.e = 555
console.log(obj)
console.log(obj2)
复制代码

 

来咯来咯!2020最新web前端面试题总结(欢迎收藏)_第3张图片

 

查看结果,发现第一层对象的a不互相影响,但是子对象c里的数据是会互相影响的

对数组的浅拷贝道理其实是一样的,对数组浅拷贝我们可以使用

  • 展开运算符...

定义一个数组

let arr = [1, 2, { a: 3 }]
复制代码

使用Array.prototype.slice()

let arr2 = arr.slice()
arr2[0] = 222
arr[2].a = 333
console.log(arr)
console.log(arr2)
复制代码

使用Array.prototype.concat()

let arr2 = arr.concat()
arr2[0] = 222
arr[2].a = 333
console.log(arr)
console.log(arr2)
复制代码

使用展开运算符...

let arr2 = [...arr]
arr2[0] = 222
arr[2].a = 333
console.log(arr)
console.log(arr2)
复制代码

他们最后的打印结果都是一样的

来咯来咯!2020最新web前端面试题总结(欢迎收藏)_第4张图片

 

我们可以也可以使用遍历的方式写一个浅拷贝函数对数组和对象进行判断

source:源输入

target:目标输出

function shallowCopy(source) {
    //开头可以判断一下入参是不是一个对象
    let target = Array.isArray(source) ? [] : {}
    for(let key in source) {
        //使用 hasOwnProperty 限制循环只在对象自身,不去遍历原型上的属性
        if(source.hasOwnProperty(key)) {
            target[key] = source[key]
        }
    }
    return target
}
复制代码

实现深拷贝

最简单的深拷贝方式

let target = JSON.parse(JSON.stringify(source))

复制代码

但是这种方法的话只支持object、array、string、number、true、false、null这几种数据或者值,其他的比如函数、undefined、Date、RegExp等数据类型都不支持。对于它不支持的数据都会直接忽略该属性。

递归的方式

浅拷贝由于只是复制一层对象的属性,当遇到有子对象的情况时, 其实我们也可以递归调用浅拷贝

function deepCopy(source) {
  //开头这里可以判断入参是不是一个对象
  let target = Array.isArray(source) ? [] : {}
  for (let key in source) {
    if (source.hasOwnProperty(key)) {
      //这里我们再做一层判断看看是否有子属性
      //这里也可以直接调用上面写过的那个checkType函数进行判断,就不用写两个typeof了
      if (source[key] && typeof source[key] !== null && typeof source[key] === "object")
      {
        target[key] = Array.isArray(source[key]) ? [] : {}
        //递归调用
        target[key] = deepCopy(source[key])
      } else {
        target[key] = source[key]
      }
    }
  }
  return target
}
复制代码

这里的深拷贝只能进行简单数据类型的拷贝,如果是复杂数据类型则拷贝的过程种会丢失掉数据,比如说拷贝的对象里含有正则表达式或者函数,日期,这些是无法拷贝的,如果需要拷贝这些的话就需要我们单独去对每一种类型做出判断,另外就是递归的版本还会存在循环引用的问题 比如obj.obj = obj,这样无限循环下去就会出现爆栈的情况,所以我们还需要进行优化

最后,想学习前端的小伙伴们!

如果还在IT编程的世界里迷茫,不知道自己的未来规划,学习没有动力,东也学一下,西也学习一下,那你可以加入web前端学习交流Q群:733581373, 里面有大神一起交流并走出迷茫。新手可进群免费领取学习资料,分享一些学习的方法和需要注意的小细节,每晚八点也会准时的讲一些前端的小案例项目。

​​在这里插入图片描述

你可能感兴趣的:(JavaScript,web前端,javascript)