2021春招遇到面试题

314 阅读12分钟

CSS

css优化的方法

  • 将渲染首屏样式的关键css内联到html中但是要严格控制大小.
  • 文件压缩如webpack、gulp/grunt、rollup
  • 去除无用的css借助Uncss库来进行
  • 有选择地使用选择器CSS选择器的匹配是从右向左进行的
    1. 保持简单,不要使用嵌套过多过于复杂的选择器
    2. 通配符和属性选择器效率最低,要匹配的元素最多
    3. 使用后代选择器的时候浏览器会遍历所有子元素,效率也较低
    4. 不要使用类选择器和ID选择器修饰元素标签,如h3#markdown,这样多此一举,还会降低效率。
  • 减少使用昂贵的属性如box-shadow/border-radius/filter/:nth-child等。
  • 减少重绘和重排

什么是重绘和重排(回流)

重绘

重绘是当元素的外观发生改变时候触发浏览器的行为,例如改变outline,背景色等属性,浏览器会根据元素的新属性重新绘制,重绘不一定会带来重排

重排(回流)

重排是当元素的位置大小发生改变时候触发的,计算这些位置和宽高的值的过程叫做重排.一个结点的重排很有可能导致子结点,甚至父点以及同级结点的 Reflow。重排大多数情况下会导致重绘

常见触发重排的操作

  • 获取元素的宽高,位置信息(top,left等)
  • 移动dom的位置
  • 使用display:none隐藏元素,但是使用visibility:hidden只会触发重绘
  • 当你 Resize(调整) 窗口的时候

优化方式

  • 元素位置变化时候使用transform来替代top,left的操作
  • 使用opacity来代替visibility
  • 将DOM离线后再修改
  • 不要一条一条的改变DOM样式,预先定义好class,改变DOM的className

介绍下flex布局和应用场景

JS

new操作符具体干了什么呢?

function Base(){}
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
  • 创建一个空对象
  • 我们将这个空对象的__proto__成员指向了构造函数prototype
  • 改变this指向,给空对象赋值 一般的构造函数是没有返回值的但是也有特殊的情况
// 如果返回值是基础数据类型,则忽略返回值
  function Person(name){
  this.name = name;
  return 1; // undefined/NaN/'string'/null
}
let me = new Person('快乐每一天');
console.log(me); // { name:'快乐每一天' }

//如果返回值是引用数据类型,则使用该返回值
  function Person(name){
  this.name = name;
  return [2,3]}
let me = new Person('快乐每一天');
console.log(me); // [2,3]

实现call apply bind

实现call

  • 将函数设置为对象的属性
  • 将函数的接收的参数中的第一个去掉,因为那个是this指向
  • 接收执行返回值然后删除该函数
  • 把返回值return 出去
function fn(a,b,c){
  // console.log(a);
}
var egg = {name:'zxx'}
Function.prototype.myCall = function(obj=window){
  obj.flag = this //  这里的obj为egg对象,this为fn这个函数(谁调用就指向谁)
  /**
   * 相当于(如下图)obj = {
   * name:'zxx',
   * flag:function(a,b,c){}
   * }
   * 
   * */
  // arguments是一个伪数组所以先转为真数组,然后去掉第一个因为第一个是egg这个对象
  let args = Array.from(arguments).slice(1)
  let result = obj.flag(...args) // 因为call执行后会立即调用
  delete obj.p // 因为不能改变对象中的属性所以把刚加的方法去掉
  return result
}

fn.myCall(egg,1,2,3)

obj的图 image.png

实现apply

  • apply 和bind的使用方法和返回值类型,只是接收参数的形式不同 apply接收的是数组类型的
  var egg = {name:'zxx'}
  function fn(a,b,c){
    console.log(a);
  }
  Function.prototype.myApply=function(flag=window){
    flag.fn = this
    let result = flag.fn(...arguments[1]) // arguments的形式为[egg,[1,2,3]]
    delete flag.fn
    return result
  }
  fn.myApply(egg,[1,2,3])

实现bind

bind会复杂一点,他返回的是一个函数

var egg = {name:'zxx'}
  function fn(a,b,c){
    console.log(b);
  }
  Function.prototype.myBind = function (flag = window){
    let args = Array.from(arguments).slice(1) //获取到参数
    return ()=>{
      this.apply(flag,args) // 这里是指向他外层作用域的this箭头函数的原因,也就是fn
    }
  }
  fn.myBind(egg,1,2,3)()

这时候有一个问题

 var egg = {name:'zxx'}
 function fn(a,b,c,d,e,f){
    console.log(e);
  }
  fn.bind(egg,1,2,3)(4,5,6) // 原生的bind会把1,2,3,4,5,6合并作为函数参数传给fn

优化一下如下

var egg = {name:'zxx'}
  function fn(a,b,c){
    console.log(b);
  }
  Function.prototype.myBind = function (flag = window){
    let args = Array.from(arguments).slice(1) //获取到参数
    return (...innerArgs)=>{
      this.apply(flag,[...args,...innerArgs]) // 将里外接收到的参数合并
    }
  }
  fn.myBind(egg,1,2,3)()

还有一个问题new对this的影响比bind大

  var egg = {name:'zxx'}
  function fn(a,b,c,d,e,f){
    console.log(this); // 这时候打印的是fn这个函数,bind绑定的this就失效了
  }
var bilibi = fn.bind(egg,1,2,3)
var b = new bilibi()

再优化一下,在返回函数那里做一下判断,判断是否为用new关键字.

  var egg = {name:'zxx'}
  function fn(a,b,c){
    console.log(this);
  }
  Function.prototype.myBind = function (flag = window){
    let args = Array.from(arguments).slice(1)
    let that = this //保存下this
    let F = function (...innerArgs){
      if(this instanceof F){ // 如果是true的话就证明使用了new
        that.apply(this,[...args,...innerArgs])
      }
      else {
        that.apply(flag,[...args,...innerArgs]) // 这里是指向fn
      }    
    }
    F.prototype = this.prototype
    return F
  }
  let b = fn.myBind(egg,1,2,3)
  let aa = new b()

说一下async和await

  • async和await的出现可以让我们把异步的代码写的更加简洁
// 假设getData()返回的是一个promise
async function logData(){
  const data = await getData()
  console.log('data');
}
  • await 是一个操作符,功能是暂停async函数的执行,等待Promise处理后的结果
  • 假如Promise处理的结果是rejected,会抛出异常
  • async函数是通过一个隐式的Promise返回pending状态的

Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

let p1 = new Promise((reslove,reject)=>{
    reject(1)
  })
  let p2 = new Promise((reslove,reject)=>{
    reject(2)
  })
  let p3 = new Promise((reslove,reject)=>{
    reslove(3)
  })
  let promiseAll = Promise.allSettled([p1, p2, p3]);
  promiseAll.then(res=>{
    console.log(res);
  }).catch(err=>{
    console.log(err);
  })

image.png

实现promise

const PENDING = 'PENDING',
      FULFILLED = 'FULFILLED',
      REJECTED = 'REJECTED'
class Mypromise {
  constructor(executor){
    this.status = PENDING
    this.value = undefined // 成功的值
    this.reason = undefined // 失败的值

    // 用来存pendding时候的容器
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value)=>{
      if(this.status === PENDING){
        this.value = value
        this.status = FULFILLED
        this.onFulfilledCallbacks.forEach(fn=>{
          fn()
        })
      }
    }
    const reject = (reason)=>{
      if(this.status === PENDING){
        this.reason = reason
        this.status = REJECTED
        this.onRejectedCallbacks.forEach(fn=>{
          fn()
        })
      } 
    }
    try {
      executor(resolve,reject)
    } catch (error) {
      reject(error)
    }  
  }
  then(onFulfilled,onRejected){
    if(this.status ===FULFILLED){
      onFulfilled(this.value)
    }

    if(this.status === REJECTED) {
      onRejected(this.reason)
    }
    // 是异步操作的时候
    if(this.status === PENDING){
      this.onFulfilledCallbacks.push(()=>{
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(()=>{
        onRejected(this.reason)
      })
    }
  }
  
}

实现数组reduce方法

实现深浅拷贝

深拷贝

  • 版本1.0 首先先了解一个判断数据类型的办法
  console.log(Object.prototype.toString.call([])); // [object Array]
  console.log(Object.prototype.toString.call({})); // [object Object]
  console.log(Object.prototype.toString.call(null));//[object Null]
  console.log(Object.prototype.toString.call(undefined));//[object Undefined]
  function deepCopy(copy, target={}) {// 如果为空则给他空对象
    let toStr = Object.prototype.toString
    for (let k in copy) {
    // 只拷贝他实例上的key,不拷贝原型链上的
      if (copy.hasOwnProperty(k)) { 
          // typeof是判断机器码的后三位的,null和obj后三位都是0
        if (typeof copy[k] === 'object' && typeof copy[k] !== 'null') { 
          target[k] = toStr.call(copy[k])==='[object Object]'?{}:[]
          deepCopy(copy[k],target[k])
        } else {
          target[k] = copy[k]
        }
      }
    }
    return target
  }
  • 版本2.0(解决了循环引用的问题)
  function deepCopy(copy,hashMap = new WeakMap()){
    const hashKey = hashMap.get(copy) 
    // 如果hashKey有值说明已经拷贝过了
    if(hashKey){
      return hashKey
    }
    // 这样可以不用判断是数组还是对象,用构造器来创建target(下面解释为什么)
    const target = new copy.constructor() 
    // 拷贝前先添加进hashMap里作为以后判断的依据
    hashMap.set(copy,target)
    for(let k in copy){
      if(copy.hasOwnProperty(k)){
        if(typeof copy[k] === 'object' && copy[k]!==null){
          target[k] = deepCopy(copy[k],hashMap)
        }else {
          target[k] = copy[k]
        }
      }
    }
    return target
  }
  let test1 = {}
 let test2 = {}
 test2.test1 = test1
 test1.test2 = test2
  console.log(deepCopy(test1));

用实例的构造器来new得到相同的数据类型

  const obj = {}
  const arr =[1]
  const newObj = new obj.constructor()
  const newArr = new arr.constructor()
  console.log(newObj);//{}
  console.log(newArr);// []

浅拷贝

  • Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
  • 使用...运算符
let arr = [1,2,{name:'aa'}]
let arr1 = [...arr]
let obj = {name:'zxx',number:[1,2,3]}
let obj2 = {...obj}

求两个超大整数的和

function sumBigNumber(a, b) {
  var res = '',
    temp = 0;
  a = a.split('');
  b = b.split('');
  while (a.length || b.length || temp) {
    // ~~两次取反去除小数部分
    temp += ~~a.pop() + ~~b.pop(); 
    // 对10求余为了得到个位的数字 res是为了隐式转换为字符串
    res = (temp % 10) + res; 
    // 如果大于9则temp为true,但时候会隐式转换为1
    temp = temp > 9;
  }
  return res.replace(/^0+/, ''); // 都为0的时候返回空字符串
}

实现promise.all

按如下代码需求实现PromiseAll这个函数

const p1 = new Promise((res,rej)=>{
    setTimeout(()=>{
      res('1')
    },1000)
  })
  const p2 = new Promise((res,rej)=>{
    setTimeout(()=>{
      res('2')
    },2000)
  })
  const p3 = new Promise((res,rej)=>{
    setTimeout(()=>{
      res('3')
    },3000)
  })

  const proAll = PromiseAll([p3,p2,p1]).then(res=>{
    console.log(res); // 3秒后打印[3,2,1]
  })

promiseAll函数实现

function PromiseAll(promiseArray){
  return new Promise((resolve,reject)=>{
    if(!Array.isArray(promiseArray)){
      return reject(new Error('传入的参数必须是数组'))
    }
    let length = promiseArray.length
    let res = []
    let count = 0
    for(let i=0;i<length;i++){
      // 通过 Promise.resolve()方法将传入的元素全部转为promise,这样就不用判断类型了。
      Promise.resolve(promiseArray[i]).then(value=>{
        res[i] = value
        count++
        // 只能用count来计数,不能用res.length来比较
        if(count === length) {
          resolve(res)
        }
      }).catch(err=>{
        reject(err)
      })
    }
  })
}
  • promise.resolve 将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
  • 代码中为啥需要用count来计数,而不能使用res.length来看数组长度?
let arr = []
arr[5] = 1
console.log(arr.length) // 6

js DOM BOM

事件的捕获和冒泡有了解么?

  • 捕获是自顶向下的
  • 冒泡是自底向上的

window.addEventListener监听的是什么阶段的事件?

有两种情况,根据它接收的第三个参数决定

  • 第三个参数默认为false,为监听冒泡阶段
  • 先执行捕获阶段,再到冒泡阶段
// 冒泡阶段
 window.addEventListener('click',()=>{
 console.log(2);
  })
// 捕获阶段
 window.addEventListener('click',()=>{
 console.log(1);
  },true)

场景题

给每个访问网页的用户添加一个属性,当banned=true,用户点击网页上的任何元素或者按钮,都不不响应原来的函数,而是直接alert提示,你被封禁了。

  • 利用捕获阶段,判断banned是否存在
  window.addEventListener('click',()=>{
    if(banned) {
      e.stopPropagation();
      alert('你被封禁了')
    }
  },true)

浏览器相关

浏览器的渲染过程

  • 浏览器渲染页面的整个过程:浏览器会从上到下解析文档。
  1. 遇见 HTML 标记,调用HTML解析器解析为对应的 token (一个token就是一个标签文本的序列化)并构建 DOM 树(就是一块内存,保存着tokens,建立它们之间的关系)。
  2. 遇见 style/link 标记调用相应解析器处理CSS标记,并构建出CSS样式树。
  3. 遇见 script 标记 调用javascript引擎 处理script标记、绑定事件、修改DOM树/CSS树等
  4. 将 DOM树 与 CSS树 合并成一个渲染树。
  5. 根据渲染树来渲染,以计算每个节点的几何信息(这一过程需要依赖GPU)。
  6. 最终将各个节点绘制到屏幕上。

强缓存和协商缓存

强缓存

  • 强缓存不会向服务器发出请求
  • 请求的状态码为200 OK(memory cache) 强缓存的header参数:
  • expires 强缓存的过期时间(相当于到期时间)
  • cache-control 计算资源的有效期(相当于保质期) cache-controlexpires优先级高

image.png

协商缓存

  • 协商缓存会向服务器发出请求,服务器会根据请求头的资源判断是否命中协商缓存
  • 如果命中协商缓存则返回304 协商缓存的header参数:
  • Last-Modified/If-Modified-Since:Last-Modified是响应头接收的最后修改时间,If-Modified-Since是请求头传过去的修改时间,两个进行比对,如果一样则命中协商缓存。
  • Etag/If-None-Match 是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变。

image.png

image.png 有了Last-Modified为什么要有Etag?

  • Last-Modified只能精确到秒,秒一下的无法判断

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

长连接和短连接

长连接:

  • 通信双方建立TCP连接后进行通信,通信完毕后,不主动断开连接而是保持连接(心跳包机制) 短连接:
  • 通信双方有数据交互时就建立tcp连接
  • 数据传送完成时,就断开连接。

了解过前端的内存处理么?

内存的生命周期

  • 内存分配:声明变量,函数,对象的时候,js会自动分配内存
  • 内存使用:调用,使用的时候
  • 内存回收:js垃圾回收机制

js的垃圾回收机制讲一下?

  • 引用计数

当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时候就进行回收 缺陷:处理不了循环引用

  • 标记清除 无法到达的对象
  1. 在运行的时候给存储在内存的所有变量都加上标记
  2. 从根部出发,能触及到的对象,把标记清除
  3. 哪些有标记的就被视为即将要删除的变量

怎么实现多页签通信?

都是在同源策略下的多页签通信

使用浏览器的localstorage

通过localstorage.setItem()来传数据,通过监听window上的storage事件来监听数据。

使用浏览器的cookie

通过document.cookie发数据,setInterval不停的去cookie上去数据

使用websocket

是一个全双工的通信模式,使用一个服务器作为桥梁进行多页签通信。

使用H5增加的shareworker

iframe(非同源页面的通信)

iframe 与父页面间可以通过指定origin来忽略同源限制。

xss(跨站脚本攻击)和csrf(跨站请求伪造)

xss

  • 浏览器向服务器请求的时候注入脚本攻击
  • 分为反射型(非持久型),存储型(持久型),基于Dom

防范方法:输入过滤,输出过滤,加httponly请求头锁死cookie

csrf

  • 诱导用户打开黑客的网站,在黑客的网站中,利用用户登录状态发起跨站点请求

防范方法:1.服务器验证 http请求的refer头信息 2.请求的时候传token 3.加验证码

跨域问题是啥怎么解决

由于浏览器的同源策略造成的 image.png

JSONP解决跨域

  • 利用 script 标签,规避跨域,<script src="url">
  • 在客户端声明一个函数,function jsonCallback() {}
  • 在服务端根据客户端传来的信息,查找数据库,然后返回一份字符串。
  • 客户端,利用<script>标签解析为可运行的JavaScript代码,调用 jsonCallback()函数
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Ajax测试</title>
</head>
     
<body>
    
    <script>
        function jsonpCallback(data) {
            console.log(data);
        }
    </script>
    
    <!-- 负责解析字符串为 JavaScript 代码 -->
    <script src="http://localhost:3000"></script>
</body>

</html>

后端是怎么判断前端传的数据是什么格式的?

  • 根据请求头中的content-type就可以进行判断

Vue

nextTick()的作用?

  • 首先明确vue的渲染是一个异步的渲染。(批量的)
  • this.$nextTick()会在vue异步渲染完成之后再执行这个函数。
  • 在 created 和 mounted 阶段,如果需要操作渲染后的视图,也要使用 nextTick 方法。