前端面试题目汇总

HTTP

  • HTTP 缓存机制

    • 基本原理

      1. 浏览器在加载资源时,根据请求头的expires和cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。

      2. 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modified和etag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源

      3. 如果前面两者都没有命中,直接从服务器加载资源

  • HTTP 强缓存协商缓存

    • 相同点

      如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据。

    • 不同点

      强缓存不发请求到服务器,协商缓存会发请求到服务器。

    • 强缓存

      1. Expires

        Expires 是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回。Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效

        1
        Expires: Wed, 11 May 2018 07:20:00 GMT
      2. Cache-Control

        Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires ,表示的是相对时间

        1
        Cache-Control: max-age=315360000

        Cache-Control: no-cache 不会缓存数据到本地的说法是错误的,详情《HTTP权威指南》P182

        Cache-Control: no-store 才是真正的不缓存数据到本地

        Cache-Control: public 可以被所有用户缓存(多用户共享),包括终端和CDN等中间代理服务器

        Cache-Control: private 只能被终端浏览器缓存(而且是私有缓存),不允许中继缓存服务器进行缓存

    • 协商缓存

      当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串。

      1. Last-ModifiedIf-Modified-Since

        Last-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的 Last-Modified 的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。

      2. ETagIf-None-Match

        Etag 就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的

        If-None-Match 的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来。

      ETag的优先级比Last-Modified更高,
      具体为什么要用ETag,主要出于下面几种情况考虑:

      1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET
      2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
      3. 某些服务器不能精确的得到文件的最后修改时间
  • 介绍一下 HTTP/2 新增特性

    • 多路复用

      HTTP/2将一个TCP连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。这也是HTTP/1.1与HTTP/2最大的区别所在。 HTTP/2中,每个用户的操作行为被分配了一个流编号(stream ID),这意味着用户与服务端之间创建了一个TCP通道;协议将每个请求分割为二进制的控制帧与数据帧部分,以便解析。

    • HPACK 算法

      HTTP/2 引入HPACK算法对HTTP头部做压缩。

    • 服务器推送

      服务器推送简单来说就是,还没有收到浏览器的请求,服务器就把各种资源推送给浏览器。

      比如,浏览器只请求了index.html,但是服务器把index.html、style.css、example.png全部发送给浏览器。这样的话,只需要一轮 HTTP 通信,浏览器就得到了全部资源,提高了性能。

JavaScript

  • 语言基础问题

    1. 下列代码运行后结果

      1
      2
      3
      4
      5
      6
      7
      var a = 2

      function a() {
      console.log('1')
      }

      a() // 1
    2. [1, 2, 3, 4, 5] 变成 [2, 3, 4, 5, 6] 都有哪些方式?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      let arr = [1, 2, 3, 4, 5]

      arr = arr.map(item => item + 1)

      arr.forEach((item, index) => {
      arr[index] = arr[index] + 1 // 改变原数组
      })

      Array.form(arr, t => t + 1)
    3. 写出至少5种数组浅拷贝的方式

      1
      2
      3
      4
      5
      6
      7
      8
      let arr = [{a: 1}, {a: 2}, {a: 3}, {a: 4}, {a: 5}]

      const arr1 = [...arr]
      const arr2 = arr.concat()
      const arr3 = arr.map(item => item)
      const arr4 = Array.from(arr)
      const arr5 = arr.slice(0)
      const arr6 = JSON.parse(JSON.stringify(arr)) // 不安全的
    4. 哪些方法不会修改原数组,哪些方法会修改原数组

      1
      2
      3
      4
      // 会修改原数组的方法


      // 不会修改原数组的方法
    5. 如何判断变量类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var val1 = null
      var val2
      var val3 = function() {
      console.log(111)
      }

      Object.prototype.toString.call(val1) // "[object Null]"
      Object.prototype.toString.call(val2) // "[object Undefined]"
      Object.prototype.toString.call(val3) // "[object Function]"
    6. 如何截取上面类型字符串

      1
      2
      3
      4
      let str = "[object Function]"
      console.log(str.slice(8, -1))

      str.match(/\[object\s(\w+)\]/)[1]

proxy 对象浅谈

有什么用

Proxy类用于创建js对象的代理。proxy实例可以重新定义对象的基本操作,可以用来拦截赋值,防止调用等。

怎么用

Proxy是一个构造器,在js中通过new关键字来创建实例。构造器接受两个参数:

  1. target

    用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理,不可以为null,字符串,数字,布尔型,Symbol,undefuned)。

  2. handler

    一个定义了被代理对象执行操作时的行为的对象。

一些例子

这里用于展示基本的set和get捕获器的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const obj = {
a: 111,
b: 222,
c: 333,
}

obj.a // 111

const proxy = new Proxy(obj, {
get: (target, prop) => {
if (prop !== 'c') {
return 'proxy yes!'
}
return target[prop]
},
set: (target, prop, value) => {
if (prop === 'a') {
target[prop] = value
return true
}
return true
},
})

proxy.a // proxy yes!

proxy.c // 333

proxy.a = 222

obj.a // 222

proxy.b = 333

obj.b // 222

可以看到,代理的实例拦截了对原对象的取值和赋值操作。当然proxy还提供了很多其他的捕获器,如 applyconstructdefinePropertydeletePropertyhas。。。等等这些,你都可以在这里找到对应的文档:处理器对象 MDN

应用场景

Proxy可以用来实现叫做代理模式的软件开发模式。你可以点击这里了解一下代理模式:代理模式。当然这一篇科普你可能看不懂,看不懂也没关系。建单表述一下,代理就是帮你访问目标实例的中间层

js的代理和传统的代理模式略有不同,我们可以通过Proxy类方便的创建一个对象的代理,然后对外发布。在这个对象的代理实例中可以简单的控制对象的访问权限,或者帮你访问到你无法/无权访问的实例。下面给出一些我在业务场景中用到Proxy类的例子:

Vue Provide

Vue 在 ^2.2 版本增加了 provide / inject 特性,可以跨多层子孙组件传递祖先组件中的一些状态。本身provide并不是响应式的。如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。不过有些data属性并不是组件在实例化时就已经存在的,这时候如果你想让子孙组件通过响应式更新这个provide,就必须在外面再套一层,导致用起来结构就比较复杂。这时候可以通过proxy来解决:

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div>
<sub-component></sub-component>
</div>
</template>

<script>
import SubComponent from './components/sub-component.vue'

function genProxy(context, dataName) {
// 这里将一个空对象作为代理的原始target传入,将操作映射到真实的target上。
return new Proxy({}, {
get: (t, propName) => {
if (context[dataName]) {
return context[dataName][propName]
}
return null
},
set: (t, propName, value) => {
if (context[dataName]) {
context[dataName][propName] = value
}
return true
},
})
}

export default {
components: {
SubComponent,
},

data() {
return {
asyncData: null,
}
},

provide() {
return {
provideData: genProxy(this, 'asyncData'),
}
},

created() {
setTimeout(() => {
this.asyncData = {
title: 'yourwilddad',
subTitle: 'other props',
}
}, 5000)
},
}
</script>

子孙组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<h1>{{ provideData.title }}</h1>
<h2>{{ provideData.subTitle }}</h2>
</div>
</template>
<script>
export default {
name: 'sub-component',

inject: [
'provideData',
],
}
</script>

这样,在子孙组件中注入了祖先组件的provideData后,仍能在asyncData被赋值后响应式更新自己的内容。

多层代理

在Proxy官方解释中有这么一句话:用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理),当我们代理的targer还是一个代理时,就可以变成一个代理串,每一个代理实例都可以直接使用,也可以拿来生成另一个代理。

我在业务中用来创建了一个axios配置代理,每一层代理实例都可以用来发送xhr请求,当一部分后端接口增加了额外的选项(比如部分接口需要传递额外的header,或者拥有其他的baseURL),就可以用之前的代理实例生成一个新的代理实例,覆盖掉这部分配置。从而减少axios请求部分的代码量。降低code复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { extend } from 'lodash'

/**
* Axios实例代理
* @param {AxiosInstance} axiosInstance Axios 实例
* @param {AxiosRequestConfig} options 要覆盖的配置项
*/
function axiosProxy(axiosInstance, options) {
return new Proxy(axiosInstance, {
get(target, name) {
if (['delete', 'get', 'head', 'options'].includes(name)) {
return (url, config) => target.request(extend({}, options, config || {}, {
method: name,
url: url,
}))
}
if (['post', 'put', 'patch'].includes(name)) {
return (url, data, config) => target.request(extend({}, options, config || {}, {
method: name,
url: url,
data: data,
}))
}
if (name === 'request') {
return (url, config) => {
if (typeof url === 'string') {
return target.request(extend({}, options, config || {}, {
url: url,
}))
}
return target.request(extend({}, options, url || {}))
}
}
return target[name]
},

apply(target, thisArg, argumentsList) {

if (typeof argumentsList[0] === 'string') {
return target.request(extend({}, options, argumentsList[1] || {}, {
url: argumentsList[0],
}))
}

return target.request(extend({}, options, argumentsList[0] || {}))
},
})
}

export default axiosProxy

拥有了这个功能的axios用起来像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import axios from 'axios'
import genProxy from './axios-proxy'

const config = {
// some options
}

const axiosClient = axios.create(config)

const proxy1 = genProxy(axiosClient, {
baseURL: '//new_base_url'
})

const proxy2 = genProxy(proxy1, {
headers: { 'test-header': 'test' }
})

// get some/path/1
axiosClient.get('some/path/1')

// get //new_base_url/some/path/2
proxy1.get('some/path/2')

// post //new_base_url/some/path/3 with custom header [test-header]
proxy2({ url: 'some/path/3', method: 'post', data: { test: 'test2' } })

兼容性

写在最后

Proxy类通过简单的方式帮我们创建任意对象的代理,他在没有Proxy时上面的这些操作并不是不能完成,只不过Proxy通过更简单的方法让我们实现这些功能。当然还有很多有趣的逻辑都可以通过Proxy实现。具体怎么做,就需要各位看官自己发掘了。当然你们如果有好的想法,非常欢迎在下面留言教教我~

谢谢观看!

React router v4 在组件外通过JS跳转

React router升级到v4版本后,不再提供导出browerHistory的功能。如果想要在代码中跳转,只能通过组件里的props中所包含的history操作,无法完成在组件外通过JS代码跳转的功能。如果想要自己封装一些服务,比如说为AJAX请求添加拦截器的功能,不通过拦截器的请求强制跳转到登陆页面,这种操作就无法实现了。

其实React router已经给出了解决办法,这里要用到一个额外的库 history 具体用法如下:

  1. 安装

    可以通过npm包管理器安装

    1
    npm i history --save

    或者通过yarn包管理器安装

    1
    yarn add history
  2. 引入

    可以通过CommonJS的方式引入

    1
    const createHistory = require("history").createBrowserHistory

    或者通过ES6标准的写法引入

    1
    import createHistory from "history/createBrowserHistory"

    也可以通过<script>标签引入

    1
    <script src="history.min.js"></script>
  3. 使用

    首先创建history对象,通过代码:

    1
    const history = createHistory()

    这里拿到的history对象和在组件中拿到的 this.props.history 的API都是一样的,可以直接使用,不过如果直接写作history.push('/index')则会只有地址栏的URL变化了,路由却没有切换。正确的用法应该是这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import React, { Component } from "react";
    import { BrowserRouter, Router, Route } from "react-router-dom";
    import createHistory from "history/createBrowserHistory"
    ...

    const history = createHistory();
    class App extends Component {
    render() {
    return (
    <Router history={history}>
    <div className="body">
    <Route path="/login" component={Login} />
    <Route path="/main" render={props => {
    return (
    <BrowserRouter basename="/main">
    <div className="App">
    <NavBar />
    <SideBar />
    <MainPage />
    </div>
    </BrowserRouter>
    );
    }} />
    </div>
    </Router>
    )
    }
    }

    history.push('/login');

    将你需要在外部控制的<BrowserRouter>替换为<Router>,并将创建的history对象作为props传递给<Router>组件,这样在外部通过操作history对象就可以正确跳转了。

    当然,也可以把history单独封装为一个模块,方便其他地方使用:

    1
    2
    3
    4
    import createHistory from "history/createBrowserHistory"

    const history = createHistory();
    export default history;

总结

在react-router v4版本中,官方推荐使用<BrowserRouter>作为路由,但是由于<BrowserRouter>没有额外兼容这个库,所以我没有找到使用<BrowserRouter>作为路由后仍在外部使用代码控制跳转的方法,如果有人知道,可以在下面留言告诉我,不胜感激。