前端面试要点考点

297 阅读7分钟

盒子水平居中

flex布局

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .father {
        height: 300px;
        display: flex;
        justify-content: center;
        align-items: center;
        background: rebeccapurple;
    }
    .box {
        height: 200px;
        width: 300px;
        text-align: center;
        line-height: 200px;
        background: #4078c0;
    }
</style>
<body>
<div class="father">
    <div class="box">盒子水平居中</div>
</div>
</body>
</html>

定位

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .father {
        height: 300px;
        position: relative;
        background: rebeccapurple;
    }
    .box {
        position: absolute;
        top: 50%;
        left: 50%;
        height: 200px;
        width: 300px;
        transform: translate(-50%, -50%);
        text-align: center;
        line-height: 200px;
        background: #4078c0;
    }
	或者如下,但是这种都为0的写法要求盒子本身必需有宽高,不然不行就会沾满整个宽高
    .box {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      height: 200px;
      width: 300px;
      margin: auto;
      text-align: center;
      line-height: 200px;
      background: #4078c0;
  }
</style>
<body>
<div class="father">
    <div class="box">盒子水平居中</div>
</div>
</body>
</html>

使用display: table-cell

组合使用display:table-cell和vertical-align: middle、text-align: center,使父元素内的所有行内元素水平垂直居中(内部div设置display:inline-block即可)。这在子元素不确定宽高和数量时,特别实用!但是父元素本身需要给定固定宽和高,百分比不是固定宽高

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .father {
        display: table-cell;
        vertical-align: middle;
        text-align: center;
        height: 300px;
        width: 880px;
        background: rebeccapurple;
    }
    .box {
        display: inline-block;
        height: 200px;
        width: 300px;
        text-align: center;
        line-height: 200px;
        background: #4078c0;
    }
</style>
<body>
<div class="father">
    <div class="box">盒子水平居中</div>
</div>
</body>
</html>

通过js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .father {
        height: 300px;
        width: 600px;
        position: relative;
        background: rebeccapurple;
    }

    .box {
        height: 200px;
        width: 300px;
        text-align: center;
        line-height: 200px;
        background: #4078c0;
    }
</style>
<body>
<div class="father">
    <div class="box">盒子水平居中</div>
</div>
<script>
    let father = document.querySelector('.father')
    let box = document.querySelector('.box')
    let fatherHeight = father.offsetHeight
    fatherWidth = father.offsetWidth
    boxHeight = box.offsetHeight
    boxWidth = box.offsetWidth
    box.style.position = 'absolute'
    box.style.left = (fatherWidth - boxWidth) / 2 + 'px'
    box.style.top = (fatherHeight - boxHeight) / 2 + 'px'
</script>
</body>
</html>

Dom事件处理函数中this的指向

  <body>
  	// 如果直接写在标签里面,相当于告诉浏览器全局去找到这个click函数。所以this指向btnClick这个实例
    <button class="btn" onclick="btnClick.click() ">点击</button>
  </body>
  <script>
    function Button() {
      this.clicked = false
      this.click = function () {
        this.clicked = true
        console.log(this)
      }
    }
    let btnClick = new Button()
    let btn = document.querySelector('.btn')
    btn.addEventListener('click', btnClick.click)// 作为回调函数调用,this指向该DOM对象
    let button = {
      clicked: false,
      click: () => {
        this.clicked = true
        console.log(this)
      },
    }
    btn.addEventListener('click', button.click) // 作为回调函数调用,箭头函数由于不会创建自己的this,它只能从作用域链的上层继承this,所以this指向全局对象window
  </script>

手动实现call,apply,bind函数

call

    Function.prototype.myCall = function (context, ...args) {
        // 判断是否是undefined和null
        /* 
          if (typeof context === 'undefined' || context === null) {
            context = window
          } 
        */
        context = context || window
        let fn = Symbol()
        context[fn] = this // 给context添加一个fn函数,谁调用myCall,this就指向谁,所以fn就是要执行的函数,
        let result = context[fn](...args)
        delete context[fn] // 因为context没有fn,所以还需要做一下删除
        return result
    }
    // 测试
    let name = 'Jerry'
    let Person = {
        name: 'Jack',
        say: function () {
            console.log('my name is', this.name)
            console.log(arguments)
        }
    }
    let Person1 = {
        name: 'Tom'
    }
    Person.say.myCall(Person1, 1, 2, 3) // my name is Tom 1 2 3
    Person.say.call(undefined, 1, 2, 3) // my name is Jerry 1 2 3

apply

    Function.prototype.myApply = function (context, args) {
        context = context || window
        let fn = Symbol()
        context[fn] = this
        let result = context[fn](...args)
        delete context[fn]
        return result
    // 测试
    let name = 'Jerry'
    let Person = {
        name: 'Jack',
        say: function () {
            console.log('my name is', this.name)
            console.log(...arguments)
        }
    }
    let Person1 = {
        name: 'Tom'
    }
    Person.say.myApply(Person1, [1, 2, 3]) // my name is Tom 1 2 3
    Person.say.apply(undefined, [1, 2, 3]) // my name is Jerry 1 2 3

bind

  Function.prototype.mybind = function (context, ...args1) {
      if (typeof context === 'undefined' || context === null) {
            context = window
      }
      let self = this // 函数本身
      let bound = function (...args2) {
          let allArgs = [...args1, ...args2]
          return self.apply(context, allArgs)
      }
      // 同时还需要考虑到原型链的问题
      // 为什么要考虑?因为在new 一个bind过生成的新函数的时候,必须的条件是要继承原函数的原型
      // 寄生组合继承
      let fn = function () {
      }
      fn.prototype = context.prototype
      bound.prototype = new fn()
      return bound
  }
  // 测试
  function demo() {
      console.log(this.name, [...arguments])
  }

  let obj = {
      name: '测试bind函数'
  }
  demo.mybind(obj, 1, 2)(4, 5, 6) // 测试bind函数 [1, 2, 4, 5, 6]
  demo.bind(obj, 1, 2)(4, 5, 6)   // 测试bind函数 [1, 2, 4, 5, 6]

柯里化函数实现

使用场景

  • 参数复用
    function hobby(eat, drink, play, study) {
        return `${eat}${drink}${play}${study}`
    }
    let res = hobby('吃','喝', '玩', '学习')
    console.log(res) // 吃喝玩学习
    // after curry
    function hobby1(eat,drink) {
        return function otherHobby(play, study) {
            return `${eat}${drink}${play}${study}`
        }
    }
    let res1 = hobby1('吃', '喝') // it's a function
    console.log(res1('玩', '学习')) // 吃喝玩学习
    console.log(res1('玩', '上班')) // 吃喝玩上班
    const roleList1 = [
        {male: '张三', skill: 'coding'},
        {male: '赵四', skill: 'coding'}
    ]
    const roleList2 = [
        {female: 'aaa', skill: 'swimming'},
        {female: 'bbb', skill: 'swimming'}
    ]
    const curring = name => item => item[name]
    const name1 = curring('male')
    const name2 = curring('female')
    console.log(roleList1.map(name1)) // ["张三", "赵四"]
    console.log(roleList2.map(name2)) //  ["aaa", "bbb"]
  • 提前确认,避免每次都重复判断
    let addEvent = function (el, type, fn, capture) {
        if (window.addEventListener) {
            el.addEventListener(type, function (e) {
                fn.call(el, e)
            }, capture)
        } else if (window.attachEvent) {
            el.attachEvent('on' + type, function (e) {
                fn.call(el, e)
            })
        }
    }
    // 上面代码,每次使用addEvent为元素添加事件的时候,IE6,7,8都会走一遍if...else if...但是只要一次判定就可以了,使用函数柯里化优化
    let addEvent = (function () {
        if (window.addEventListener) {
            return function (el, type, fn, capture) {
                el.addEventListener(type, function (e) {
                    fn.call(el, e)
                }, capture)
            }
        } else if (window.attachEvent) {
            return function (el, type, fn, capture) {
                el.attachEvent('on' + type, function (e) {
                    fn.call(el, e)
                })
            }
        }
    })()
  • 延迟计算运行
    function hobby(eat, drink, play, study) {
        return `${eat}${drink}${play}${study}`
    }
    let res = hobby('吃','喝', '玩', '学习')
    console.log(res) // 吃喝玩学习
    // after curry
    function hobby1(eat,drink) {
        return function otherHobby(play, study) {
            return `${eat}${drink}${play}${study}`
        }
    }
    let res1 = hobby1('吃', '喝') // it's a function //这里返回的是一个函数,达到了延迟计算运行的目的

curry封装

    function curry(fn) {
        let len = fn.length
        return function temp() {
            let args = [...arguments]
            if (args.length >= len) {
                return fn(...args)
            } else {
                return function () {
                    return temp(...args, ...arguments)
                }
            }
        }
    }
	// 测试
    function print(a, b, c) {
        console.log(a + b + c)
    }
    let res = curry(print) // 延迟计算运行,没有足够的参数时,不会返回结果
    res(1, 2)(3)  // 6
// 可以curry的同时进行传参
    function curry(fn, ...args) {
        if (args.length < fn.length) {
            return (...arguments) => curry(fn, ...args, ...arguments)
        } else {
            return fn(...args)
        }
    }
    function print(a, b, c) {
        console.log(a + b + c)
    }
    let res = curry(print,2)
    res(1)(2) // 5

防抖函数

防抖函数的作用

防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着N秒内函数只会被执行一次,如果N秒内再次被触发,则重新计算延迟时间。

举例说明: 小思最近在减肥,但是她非常吃吃零食。为此,与其男朋友约定好,如果10天不吃零食,就可以购买一个包(不要问为什么是包,因为包治百病)。但是如果中间吃了一次零食,那么就要重新计算时间,直到小思坚持10天没有吃零食,才能购买一个包。所以,管不住嘴的小思,没有机会买包(悲伤的故事)... 这就是 防抖

    function debounce(func, delay) {
        let timer
        return function (...args) {
            if (timer) {
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
                func.apply(this, args)
            }, delay)
        }
    }
    function demo() {
        console.log('123')
    }
	setInterval(debounce(demo,500),1000) // 第一次在1500ms后触发,之后每1000ms触发
    setInterval(debounce(demo, 2000),1000) // 永远不会执行,因为它每过一秒钟就执行一次,而debounce要delay两秒钟

防抖的应用场景

  1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  2. 表单验证
  3. 按钮提交事件。
  4. 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。

节流函数

节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。

function throttle(fn, gapTime) {
  let _lastTime = null;
  return function () {
    let _nowTime = Date.now()
    if (_nowTime - _lastTime > gapTime || !_lastTime) {
      fn();
      _lastTime = _nowTime
    }
  }
}

let fn = ()=>{
  console.log('boom')
}
setInterval(throttle(fn,1000),10)

节流的应用场景

  1. 按钮点击事件
  2. 拖拽事件
  3. onScoll
  4. 计算鼠标移动的距离(mousemove)

请实现一个 flattenDeep 函数,把嵌套的数组扁平化

这里提供三种方法,分别如下:

  1. 调用ES6中Array的新原型方法flat
   // flat方法默认是压平一层
   // 当传递的整数大于数组嵌套的层数时,会将数组拉平为一维数组,JS能表示的最大数字为 Math.pow(2, 53) - 1,因此我们可以这样定义 flattenDeep 函数
    function flattenDeep(arr) {
        // 正常情况下我们都不需要这么大的层级嵌套
        return arr.flat(Math.pow(2, 53) - 1)
    }
    console.log(flattenDeep([1, [2, [3, [4]], 5]])) // [1,2,3,4,5]
  1. 利用reduce和concat方法

关于reduce方法:回调函数第一次执行时,accumulator 和currentValue的取值有两种情况:如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值`

    function flattenDeep(arr) {
        return arr.reduce((acc, curVal) => Array.isArray(curVal)
        	// 当currentVlaue是一个数组时,利用递归处理
            ? acc.concat(flattenDeep(curVal))
            // initialValue设置为一个空数组
            : acc.concat(curVal), [])
    }
    console.log(flattenDeep([1, [2, [3, [4]], 5]])) // [1,2,3,4,5]
  1. 利用栈数据结构处理
  function flattenDeep(arr) {
      const stack = [...arr]
      const res = []
      while (stack.length) {
          // 栈结构,使用pop从stack取出值
          const next = stack.pop()
          if (Array.isArray(next)) {
              // 如果pop出来是数组,就把它展开拷贝一份在push进去
              stack.push(...next)
          } else {
              res.push(next)
          }
      }
      return res.reverse()
  }
  console.log(flattenDeep([1, [2, [3, [4]], 5]])) // [1,2,3,4,5]

数组去重

// 使用ES6的Set去重
function unique(arr) {
  return Array.from(new Set(arr))
}
// 使用for循环嵌套for循环,然后利用splice去重
function unique(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
    // 第一个等于第二个,使用splice方法删除第二个
      if (arr[i] == arr[j]) { 
        arr.splice(j, 1)
        // 因为删除了一个,所以数组中的后面的元素索引发生了改变,因此需要j--拿到删除之后新的索引
        j--
      }
    }
}
return arr
}
// 使用indexOf去重
function unique(arr){
	if (!Array.isArray(arr)) {
    	throw new Error('args should a array‘)
    }
	let array = []
    for (let i = 0; i < arr.length; i++) {
    	if (array.indexOf(arr[i]) === -1) {
        	array.push(arr[i])
        }
    }
    return array
}
// 使用includes
function unique(arr) {
  if (!Array.isArray(arr)) {
    throw new Error('args should a array')
  }
  let array = []
  for (let i = 0; i < arr.length; i++) {
    if (!array.includes(arr[i])) {
      array.push(arr[i])
    }
  }
  return array
}
let arr = [1, 1, 3, 6, 8, 8, 9]
let array = unique(arr)
console.log(array) [1,3,6,8,9]

实现一个深拷贝函数

  function deepClone(target) {
  	// typeof判断是否是引用类型
    if (typeof target !== 'object' || target == null) {
      return target
    }
    // 如果是引用类型,在判断它是数组还是对象
    let res = Array.isArray(target) ? [] : {}
    for (const key in target) {
      // hasOwnProperty方法确保key属性是在自身上,而不是原型上
      if (target.hasOwnProperty(key)) {
        res[key] = deepClone(target[key])
      }
    }
    return res
  }
  let source = {
    name: 'Tom',
    age: 19,
    address: {
      city: 'HangZhou',
      population: '30 million',
    },
  }
  let res = deepClone(source)
  source.address.city = 'ShangHai'
  console.log(res.address.city) // HangZhou

数组排序

1. 冒泡排序

let arr = [1, 5, 7, 8, 2, 89, 10]

function bubbleSort(arr) {
    // 第一层循环就是决定全部排好要循环多少次
    for (let i = 0; i < arr.length - 1; i++) {
        // 第二层循环对元素大小进行比较,随着不断的比较,范围也要不断的缩小,所以要减i
        for (let j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                let tmp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = tmp
            }
        }
    }
    return arr
}

2. 选择排序

let arr = [1, 5, 7, 8, 2, 89, 10]

function selectSort(arr) {
    // 第一层循环就是决定全部排好要循环多少次
    for (let i = 0; i < arr.length - 1; i++) {
        let minIndex = i
        for (let j = i + 1; j < arr.length; j++) {
        // 找出最小的值,将他们的索引进行替换
            if (arr[j] < arr[minIndex]) {
                minIndex = j
            }
        }
        // 声明一个临时变量将他们的值进行替换
        let temp = arr[i]
            arr[i] = arr[minIndex]
            arr[minIndex] = temp
    }
    return arr
}
console.log(selectSort(arr))

3. 归并排序(这种排序的复杂度为o(nlogn),场景中用的比较多)

function mergeSort(arr) {
// 如果数组中只有一个元素,就return这个arr
	if (arr.length === 1) {
    	return arr
    }
    let middle = Math.floor(arr.length / 2),
    	left = arr.slice(0, middle),
        right = arr.slice(middle)
    // mergeSort递归处理
    return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
    let result = []
    while (left.length && right.length) {
        if(left[0] <= right[0]) {
            result.push(left.shift())
        } else {
            result.push(right.shift())
        }
    }
    while (left.length)
    	result.push(left.shift())
    while (right.length)
    	result.push(right.shift())
    return result
}

洗牌算法

function shuffle(arr) {
	const copyedArr = [...arr] 
    const len = copyedArr.length
    for (let i = 0; i < len; i++) {
    	const randomIndex = random(i, len-1);
        	[copyedArr[i], copyedArr[randomIndex]] = [copyedArr[randomIndex], copyedArr[i]]
    }
    return copyedArr
}
function random(n, m) {
	return Math.floor(Math.random()*(m-n+1)) + 1
}
let arr = [5, 7, 89, 3, 7, 8, 2, 9, 0]
console.log(shuffle(arr))