前端面试真题

246 阅读18分钟

这是一个连续剧,希望大家可以开启坚持不懈的追剧模式。肯定比斗罗大陆更新的快,哈哈!

以下如有错误之处望在评论区指出,大家共同进步!每天进步一点点!

1. 编写一个方法,该方法接收两个参数,分别为 k 和 一个无序的纯数字数组。该方法在执行后,会返回数组中第 k 大的数字。特别注意,如果数组中,有两位数值一样的数字,同数值数字排名并列。如 [3,1,3,2,5,4,5] 中,第 1 大的数字为 5,第2大的数字为 4,第5大的数字为 1?

function getK(arr, k){
    // 排列数组
    let res = arr.sort((a, b)=> b-a);
    // 数组去重
    let set = new Set(res);
    // 将类数组转化成数组
    res = Array.from(set);
    return res[k-1] || null
}

let arr = [1,2,3,3,2,5,5,4]
console.log(getK(arr, 2))

2. proto(左右下划线不显示?) 和 prototype 之间有什么关系?

所有对象都有 __proto__ 属性。

函数这个特殊对象除了具有 __proto__ 属性,还有特有的原型属性prototype。

prototype对象默认有两个属性,constructor属性和__proto__ 属性。

prototype属性可以给函数和对象添加可共享(继承)的方法、属性。

而__proto__ 是查找某函数或对象的原型链方式。

constructor,这个属性包含了一个指针,指回原构造函数。

3. .call(), .apply(), .bind() 的区别和作用?bind 方法如何来实现?

call 、apply 、bind 作用是 改变函数执行时的上下文,简而言之就是改变函数运行时的this指向。

区别在于调用方式及参数传递上。具体如下:

function fn(...args){
    console.log(this, args)
}

fn(1,2) // fn函数默认this指向是 window对象
let obj = {
    name: '张三'
}
fn.call(obj,1,2); // this 会变成 传入的obj ,args 会变成[1,2]
fn.apply(obj,[1,2]); // this会变成传入的obj ,传入的参数必须是一个数组
fn.bind(obj)(1,2); // this 也会变成传入的obj ,bind不是立即执行,需要执行一次

综上所述call、apply、bind都可以改变this指向,区别在于 参数传递方式不同,call、apply是立即执行,bind不是立即执行。

顺带手写个bind:

Function.prototype.myBind = function(context){
    // 判断调用对象是否是函数
    if(typeof this !== 'function'){
        throw new TypeError('不是函数')
    }
    // 获取参数
    var args = [...arguments].slice(1);
    var fn = this;
    return function Fn(){
        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments));
    }
}

4. js中基础数据类型有哪几种?了解包装对象么?

基础数据类型有6种: boolean null undefined number string symbol

基础数据类型都是值,所以没有方法提供调用的

例如:undefined.split("");那为什么比如 "abc".split("")类似这种调用可以被允许?

原因是js中会存在包装对象,会把字符串先包装成对象然后再调用对象下的一些方法,方法调用完成之后再销毁对象,这样就完成了基础数据类型的函数调用功能。

5. 如何判断this?箭头函数的this是什么?

可以分成三种场景来描述this:

1、函数直接调用中的this。例如:

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

如上this会指向window

需要注意下在严格模式下this会是undefined情况

同样也需要注意在script标签type="module"下也会是undefined。

2.在对象里调用的情况,如下:

let obj = {
    myname:"对象",
    foo:function(){
        console.log(this);
    }
}
obj.foo(); // this会指向调用的对象

3.在构造函数及类中,this会指向实例化的对象。如下:

function Person(){
    this.name = "张三"
}
Person.prototype.foo = function(){
    console.log(this);
}
let zhangsan = new Person()
zhangsan.son(); // this指向实例化对象zhangsan

class Animal{
   constructor(name){
       this.name = name;
   }
   foo(){
       console.log(this)
   }
}

let maomao = new Animal('猫猫');
maomao.foo(); // this 会指向maomao

最后箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。

6. 如何中断ajax请求?

原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。

注意:abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。(就是请求已经发送出去了,这边已经管不了了,abort就是数据返回来了ajax不处理了)

7. 什么是同步?什么是异步?

同步和异步是一种消息通知机制:

同步阻塞: A调用B,B处理获得结果,才返回给A。A在这个过程中,一直等待B的处理结果,没有拿到结果之前,需要A(调用者)一直等待和确认调用结果是否返回,拿到结果,然后继续往下执行。

做一件事,没有拿到结果之前,就一直在这等着,一直等到有结果了,再去做下边的事

异步非阻塞: A调用B,无需等待B的结果,B通过状态,通知等来通知A或回调函数来处理。

8. 什么是宏任务?什么是微任务?

微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

宏任务:宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。

常见微任务:

1Promise.then
2MutaionObserver
3Object.observe(已废弃;Proxy 对象替代)
4、process.nextTickNode.js

常见宏任务 :

1、script (可以理解为外层同步代码)
2setTimeout/setInterval
3UI rendering/UI事件
4、postMessage,MessageChannel
5、setImmediate,I/O(Node.js

9. 什么是回调?回调使用中存在什么问题?

回调即是函数指针的调用,即是一个通过函数指针调用的函数。如下代码:

function foo(callback){
    callback && callback();
}
fu(()=>{
    console.log('回调函数')
})

使用回调函数有一个很大缺点,容易造成回调地狱。

回到地狱是 为了实现某些逻辑出现函数的层层嵌套,类似如下代码:

move(ele,300,"left",()=>{
    move(ele,300,"top",()=>{
        move(ele,0,"left",()=>{
            move(ele,0,"top",()=>{
                console.log("所有运动完毕");
            })
        })
    })
})

回调地狱会造成可读性及可维护性变差。同样每个嵌套函数耦合性强,一层变动会引起其他的结果变动。同样回调地狱如果出现错误,不好处理。

解决回调地狱问题可以通过观察者模式、promise、async/await 来处理。

10. Promise.allSettled 了解吗?动手实现一下 Promise.allSettled?

Promise.allSettled是ES2020新特性,可以执行多个promise对象,获取多个promise对象状态,无论成功或者失败的状态。实现代码如下:

function myAllSettled(list){
    let resArr = new Array(list.length);
    let num = 0;
    return new Promise(resolve=>{
        list.forEach((item,index)=>{
            let obj = {};
            item.then(res=>{
                obj['status'] = "fulfilled";
                obj.value = res;
                resArr[index] = obj;
                num++
                if(num===list.length){
                    resolve(resArr)
                }
            },err=>{
                obj['status'] = "rejected";
                obj.reson = err;
                resArr[index] = obj;
                num++
                if(num===list.length){
                    resolve(resArr)
                }
            })
        })
    })
}

11. vue 中组件间有哪些通信方式?

1、props / $emit适用于父子组件通信。

2、ref 与 parent/parent / children适用于父子组件通信。

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;

如果用在子组件上,引用就指向组件实例parent/parent / children:访问父 / 子实例。

3、EventBus (emit/emit / on)适用于父子、隔代、兄弟组件通信。

这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

4、attrs/attrs/listeners适用于隔代组件通信。

$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。

当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind=“$attrs” 传入内部组件。

通常配合 inheritAttrs 选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。

它可以通过 v-on=“$listeners” 传入内部组件

5、provide / inject适用于隔代组件通信。

祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

6、Vuex适用于父子、隔代、兄弟组件通信Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

12. vue 中 v-show 和 v-if 的区别是什么?

v-show 只是在 display: none 和 display: block 之间切换。无论初始条件是什么都会被渲染出来,后面只需要切换 CSS,DOM 还是一直保留着的。

v-if 的话就得说到 Vue 底层的编译了。当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件, 并且基于 v-if的这种惰性渲染机制,可以在必要的时候才去渲染组件,减少整个页面的初始渲染开销。

13. keep-alive 组件有什么作用?

如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

对于 keep-alive 组件来说,它拥有两个独有的生命周期钩子函数,分别为 activated 和 deactivated。

用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

14. 说下vue生命周期钩子函数?

beforeCreate:这个时期,this变量还不能使用,在data下的数据,和methods下的方法,watcher中的事件都不能获得到。

created:这个时候可以操作vue实例中的数据和各种方法,但是还不能对"dom"节点进行操作。

beforeMounte:在挂载开始之前被调用,相关的 render 函数首次被调用。

mounted:挂载完毕,这时dom节点被渲染到文档内,一些需要dom的操作在此时才能正常进行

beforeUpdate:data中数据已经更新完毕,页面视图还未响应更改。

updated:数据和视图都更新完毕。

beforeDestroy:销毁之前,实例上事件、指令等都可以使用,这里组件没有真正的销毁。

destroyed:数据、指令、等完全销毁.

15. Vue中computed和watch区别?

computed:是计算属性,依赖其他属性计算值,并且 computed的值有缓存,只有当计算值变化才会返回内容。

watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

16. React 的组件间通信都有哪些形式?

1、父传子:在 React 中,父组件调用子组件时可以将要传递给子组件的数据添加在子组件的属性中,在子组件中通过 props 属性进行接收。这个就是父组件向子组件通信。

2、子传父:React 是单向数据流,数据永远只能自上向下进行传递。当子组件中有些数据需要向父级进行通信时,需要在父级中定义好回调,将回调传递给子组件,子组件调用父级传递过来的回调方法进行通信。

3、跨组件通信 - context。使用 context API,可以在组件中向其子孙级组件进行信息传递。

17. React 中如何实现路由懒加载?

在 React 16 中,新增了 lazy 方法,通过 lazy 方法可以轻松实现组件懒加载,当然要实现路由懒加载的话,其实也只需要把路由组件结合 lazy 使用即可。 参考代码如下:

import {Route} from "react-router-dom";
import React, {Suspense} from 'react';
const HomeView = React.lazy(()=>import("./home"));

function App(){
    return (<div>
        <h1>路由懒加载</h1>
        <Route path="/" exact render={()=>{
            return <Suspense fallback={<div>组件Loading进来之前的占位内容</div>}>
                <HomeView/>
            </Suspense>
        }}></Route>
    </div>)
};
export default App;

在上述代码中,我们使用 lazy 引入了一个动态组件,然后将该组件放入了根路由中。这样的话只有用户访问网站首页时,才会动态加载这个组件。

这里要注意,在 React 规范中,lazy 和 Suspense 必须配合使用,lazy 引入的动态组件必须要放入 Suspense 中,Suspense 的 fallback 属性是 lazy 的组件没有加载进来之前的占位内容。

18. React 的生命周期函数都有哪些,分别有什么作用?

React 的生命周期已经经历了3次改动,我们以最新的版本为准。 具体可以看下图:

挂载阶段:

1)constructor: 初始化组件,初始化组件的 state 等。
(2static getDerivedStateFromProps():该函数用于将 props 中的信息映射到 state 中。
(3)render: 生成虚拟DOM。
(4)componentDidMount:组件挂载完成,通过在该函数中去处理副作用 

更新阶段:

5)static getDerivedStateFromProps()
(6shouldComponentUpdate():该生命周期函数用于判断是否要进行组件更新。
(7render():生成虚拟DOM
(8getSnapshotBeforeUpdate():组件已经完成 diff,即将更新真实 DOM,用户获取上一次的DOM快照。该函数必须搭配 componentDidUpdate 一块使用,返回值会变成 componentDidUpdate 第三个参数。
(9componentDidUpdate(): 组件更新完成,通常在该函数中进行副作用处理。 

即将卸载:

componentWillUnmount:组件即将卸载,用于删除组件添加到全局的数据或事件。

19. 说一下React Hooks在平时开发中需要注意的问题和原因?

React Hooks 在使用时注意事项:

1)只能在 React 函数中使用(函数式组件或自定义hook) 
(2)只能在函数最外层调用 hook,不能包括在 iffor 等语句中或者子函数中。
(3)useState 中存储的是引用类型的数据时,修改 state 时,一定要返回新的引用。 

原因:

面试时关于框架的问题一般问到问题的产生原因,更多考察的就是你对框架原理的理解。

以这个问题为例,如果不了解原理,我们会得出以下答案:

1. Hooks 专为函数组件的逻辑复用而设计所以只能用在函数式组件和自定义hooks 
2. hooks 在调用的时候,需要确保先后调用顺序,一个顺序出问题,会导致整个程序的混乱。 
3. 如果在 useState 中存储的是引用类型,更新时不更新引用地址时的话,React 会认为我们没有更新数据,则不进行组件更新。 

如果在面试时只回答到整个层面,并不会引起面试官的高度注意,也没有办法拉开和普通候选人的区别。

这道题在解答的时候,最好结合到原理源码上,在原理实现的层面去深入说明为什么会出现这个问题。

待补充原理部分

20. React 的 setState 方法是异步还是同步?

setState 在 React 方法中或者 React 事件中是异步的。

但是在原生事件,或者异步程序中是同步的。

待补充原理

21. 有没有写过 Koa 中间件,说下中间件原理,介绍下自己写过的中间件?

koa 本来就是一个轻量级框架,本身支持的功能并不多,功能都是通过中间件来实现不同的需求。开发者可以通过不同的中间件来按需求扩展不同的功能。

koa中中间件本质上就是函数,可以是一个async函数,也可以是一个普通的函数。如下代码:

// 普通函数
const middleWare1 = function(ctx,next){
    return next().then(()=>{
        console.log("I am middleWare1");
    })
}
// async 函数
const middleWare2 = async function(ctx,next){
    console.log("I am middleWare2")
    await next();
}
app.use(middleWare1);
app.use(middleWare2);

中间件原理:中间件会遵循洋葱模型,中间件执行循序并不是会 从头执行到尾,而是会先执行最外层中间件,当调取next()函数后进入下一个中间件执行,一路执行到最里层中间件,然后再从最里层执行到最外层。

编写中间件:

例如log中间件:通过日志中间件记录网络请求类型及日志,记录请求ip、方式、地址及请求时间。

const fs = require("fs");

module.exports = async (ctx,next) =>{
    const startTime = Date.now();
    const requestTime = new Date();
    await next();
    const ms = Date.now() - startTime;
    const logout = `${ctx.request.ip}--${requestTime}--${ctx.method}--${ctx.url}--${ms}ms`;
    fs.appendFileSync("./log.txt",logout+"\n")
}

22. 如何判断当前脚本运行在浏览器还是node环境中?

可以通过判断在浏览器端或者是node端独特的全局对象来区分环境。例如:浏览器端的window 或者是node端的process 全局对象。具体代码如下 :

if(typeof process !== 'undefined'){
    console.log("node");
}else{
    console.log("浏览器");
}

23. 请描述一下 cookies sessionStorage和localstorage区别?

相同点:都存储在客户端

不同点:

1、存储大小:cookie数据大小不能超过4k。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。 

2、有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage 数据在当前浏览器窗口关闭后自动删除。cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。

3、数据与服务器之间的交互方式:cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端,sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。

24. 介绍一下 node 常用模块,并且详细介绍下 Stream?

常用模块如:

内置模块 http,通过http来构建本地服务器,例如:

const http = require("http");

const server = http.createServer((req,res)=>{
    res.write("hello world");
    res.end();
})

server.listen(8888);

同样也可以通过http实现服务端的网络请求。

还有fs模块,可以通过fs模块实现服务端的文件操作,实现服务端文件的增删改查操作。

还有path模块提供各种处理路径的api。

body-parser模块来处理接收post请求到服务端的数据。

......

stream 流是一种抽象数据结构,可以用它来读取和写入数据,通过流来读取和写入数据可以防止内存溢出,采取流方式处理数据会把数据分成64k小块数据,异步依次来进行传递,更加节约性能。

25. node里为什么有些模块需要安装,有些不需要安装?

nodejs在安装好之后会有很多内置模块,如:path、http、util、fs模块等等。

还有一些需要第三方模块支持。可以通过包管理器来安装,如:cookie、session、cheerio等第三方模块。可以在npm官网查找相关模块。

26. Node 如何和 MySQL 进行通信?

可以借助一些sql相关模块实现node和mysql数据库的通行,这里以mysql2模块为例,代码如下:

第一步:安装mysql2模块

npm install --save mysql2

第二步:连接数据库

const mysql = require('mysql2');
//引入mysql2模块
const connection = mysql.createConnection({
    host: 'localhost', //主机地址
    user: 'root', // 数据库用户名
    database: 'test' //数据库名称
});

第三步:通过query来执行sql语句 ,如下:

connection.query('SELECT * FROM `table` WHERE `name` = "Page" AND `age` > 45', function(err, results, fields) {
    console.log(results); // 查询到的结果
    console.log(fields); // 获取字段的相关信息
});

27. 浏览器为什么要阻止跨域请求? 如何解决跨域?每次跨域请求都需要 到达服务端吗?

浏览器阻止跨域请求的原因是“同源策略”,“同源策略”主要解决的问题是浏览器的安全问题,同源是 协议 、域名 、端口都相同,非同源是只要协议、域名、端口有一个不同就会造成非同源。如下:

http://www.baidu.com:443

//http:协议  www.baidu.com:域名  443:端口 

http://www.baidu.com:3000

如上两地址由于端口不同就造成跨域问题。

非同源会造成:

1、无法获取cookie、localstroage、indexedDB。

2、无法访问网页中dom。

3、无法发送网络请求。

所以浏览器阻止跨域的原因是基于网络安全考虑。

解决跨域方式有很多种例如:

1、jsonp跨域。

2、postMessage解决跨域 

3、跨域资源共享(CORS)

4、nginx 反向代理 

5、nodejs中间件正向代理

6、websocket协议跨域。

跨域是浏览器出于安全策略阻止非同源请求,但是每次跨域请求其实都是正常发送的,服务端也会正常返回,只是被浏览器拦截起来了。所以每次跨域请求都会到达服务端。

28. Token 一般是存放在哪里? Token 放在 cookie 和放在localStorage、sessionStorage 中有什么不同?

Token 其实就是访问资源的凭证。

一般是用户通过用户名和密码登录成功之后,服务器将登陆凭证做数字签名,加密之后得到的字符串作为 token。

它在用户登录成功之后会返回给客户端,客户端主要有这么几种存储方式:

存储在 localStorage 中,每次调用接口的时候都把它当成一个字段传给后台

存储在 cookie 中,让它自动发送,不过缺点就是不能跨域

拿到之后存储在 localStorage 中,每次调用接口的时候放在 HTTP 请求头的 Authorization 字段里

所以token 在客户端一般存放于 localStorage,cookie,或 sessionStorage 中。

将token存放在webStroage中,可以通过同域的js来访问 。这样会导致很容易受到xss攻击,特别是项目中引入很多 第三方js类库的情况下。如果js脚本被盗用,攻击者就 可以轻易访问你的网站,webStroage作为一种储存机制,在传输过程中不会执行任何安全标准。

XSS攻击:cross-site Scripting(跨站脚本攻击)是一种注入代码攻击 。恶意攻击者在目标网站上注入script代码,当访问者浏览网站的时候通过执行注入的script代码达到窃取用户信息,盗用用户身份等。

将token存放在cookie中可以指定 httponly,来防止被Javascript读取,也可以指定secure,来保证token只在HTTPS下传输。缺点是不符合Restful 最佳实践,容易受到CSRF攻击。

CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性。简单来说就是恶意攻击者盗用已经认证过的用户信息,以用户信息名义进行一些操作(如发邮件、转账、购买商品等等)。由于身份已经认证过,所以目标网站会认为操作都是真正的用户操作的 。CSRF并不能拿到用户信息,它只是盗用的用户凭证去进行操作。

29. WebSocket 是怎么实现点对点通信和广播通信的?

webSocket是一种全双工通信协议。

websocket 让服务端和客户端通信变得简单。

最大的特点是可以通过服务端主动推送消息到客户端。

前端基于nodejs 和 WebSocket实现 点对点 及广播通信。

广播通信顾名思义是类似广播一样给多个人进行广播消息。实现广播通信可以使用很多模块 主要能够把流程描述清楚就可以了。我这里采取的是socket.io模块。

// 服务端监听socket链接:
io.on("connection",(socket)=>{
    console.log("有socket连接");
})


// 通过监听连接过来的socket对象广播对应的信息:
socket.on("addData",function(data){
    // 广播除了自己之外的其他订阅者
    socket.broadcast.emit("addInputData",data);
})

// 客户端连接及发送对应的socket请求:
let socket = io.connect("ws://localhost:3000"); //连接socket服务器
socket.emit("addData",JSON.stringify(info)); // 发送socket事件

点对点通信顾名思义就是一对一的通信,例如多人实时聊天,可以指定用户来发送消息 。

点对点通信中需要注意服务端需要记录每个socket客户端的连接 ,需要将客户端及服务端socket对象关联起来。广播数据的时候,广播指定对象就可以了。如下:

// 服务端记录每一个连接过来的socket对象,且和用户id进行关联:
socket.on("uid",data=>{
    usersObj[data] = socket; // 通过usersObj来记录连接过来的用户
})

// 给指定的socket对象进行广播:
socket.on("user",data=>{
    let uid = JSON.parse(data).uid;
    userObj[uid].emit("content",data);
})

// 客户端监听点对点广播事件:
socket.on("content", function (data) {
    console.log(data);
})

总结,WebSocket 区分广播通信及点对点通信核心在于区分每一个连接的socket对象。广播通信需要对于非自身的所有连接的socket对象进行通信。而点对点通信,通过关联用户及socket对象,且保存每一个socket连接,查找指定的socket对象,来达到发送指定socket连接的目的。

30. 客户端缓存有几种方式?浏览器出现 from disk、from memory 的 策略是啥?

1、强缓存

服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。

Cache-control (相对值) / Expries(绝对值)

Expries是http1.0的标准

let nowTime = new Date();
nowTime.setTime(new Date().getTime() + 3600*1000);
ctx.set("Expires",nowTime.toUTCString());

到了HTTP/1.1,Expire已经被Cache-Control替代

ctx.set("Cache-control","max-age=3600") // 设置缓存时间3600s
1public:所有内容都将被缓存(客户端和代理服务器都可缓存)

(2private:所有内容只有客户端可以缓存,Cache-Control的默认取值

(3)no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定

(4)no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

(5)max-age=xxx :缓存内容将在xxx秒后失效

Cache-Control优先级比Expires高

from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。

2、协商缓存

让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

出现 from disk、from memory 的 策略是强缓存。

Last-Modify/if-Modify-Since

ETag/if-None-Macth

协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的。

控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,

其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。

3、缓存关系

强缓存优于协商缓存,强缓存中 Cache-control 优于 Expries,协商缓存中ETag/ If-None-Match 优先级高于 Last-Modified / If-Modified-Since。

31. 说一下 CORS 的简单请求和复杂请求的区别?

CORS(Cross-origin resource sharing),跨域资源共享,是一份浏览器技术的规范,用来避开浏览器的同源策略。

相关头部设置如下:

Access-Control-Allow-Origin 指示请求的资源能共享给哪些域。

Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求。

Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。 

Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。

Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出。 

Access-Control-Max-Age 指示预请求的结果能被缓存多久。

Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。 

Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。 

Origin 指示获取资源的请求是从什么域发起的。

CORS可以分成两种简单请求和复杂请求。简单请求是满足以下下条件的请求:

HTTP方法是下列之一

HEAD
GET
POST

HTTP头信息不超出以下几种字段

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type,但仅能是下列之一
application/x-www-form-urlencoded
multipart/form-data
text/plain

反之就是复杂请求,复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种"预请求",此时作为服务端,也需要返回"预回应"作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。

32. 节流和防抖分别是什么?在什么场景下使用?请分别实现一个节流函数和一个防抖函数。

防抖(debounce)就是在事件触发后的n秒之后,再去执行真正需要执行的函数,如果在这n秒之内事件又被触发,则重新开始计时。防抖函数实现如下:

const  debounce = (fn, delay=500)=> {
    let timer;
    return function (...args) {
        clearTimeout(timer) //清空定时器
        timer = setTimeout( ()=> {
            fn.apply(this, args); //改变this指向
    }, delay)  //延迟一定时间执行
}}

截流(throttle)就是规定好一个单位时间,触发函数一次。如果在这个单位时间内触发多次函数的话,只有一次是可被执行的。想执行多次的话,只能等到下一个周期里。实现代码如下:

// 定时器版本
const trottle1 = (fn,delay=500)=>{
    let timer;
    return function(...args){
        if(!timer){
            timer = setTimeout(() => {
                timer = null;
                fn.apply(this,args);
            }, delay);
        }
    }
}

// 时间戳版本
const trottle2 = (fn,delay=500)=>{
    let oldTime = Date.now();
    return function(...args){
        const nowTime = Date.now();            if(nowTime - oldTime >= delay){
            oldTime = Date.now();
            fn.apply(this,args);
        }
    }
}

实际运用中比如,按键快频率重复触发,拖拽场景、表单验证场景resize ,scroll、onmosemove等等触发事件。 性能优化相关。

33. 怎么禁止让js读取cookie?怎么让cookie只在HTTPS下传输?

由于cookie会存放在客户端,一般情况下会保存一些凭证及状态信息,为了防止cookie泄露造成安全问题。可以设置这只cookie的 HttpOnly属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。

cookie 中有个属性secure,当该属性设置为true时,表示创建的 Cookie 会被以安全的形式向服务器传输,也就是只能在 HTTPS 连接中被浏览器传递到服务器端进行会话验证,如果是 HTTP 连接则不会传递该cookie信息,所以不会被窃取到Cookie 的具体内容。就是只允许在加密的情况下将cookie加在数据包请求头部,防止cookie被带出来。secure属性是防止信息在传递的过程中被监听捕获后信息泄漏。但是这两个属性并不能解决cookie在本机出现的信息泄漏的问题。