面试题复习

146 阅读18分钟

js

Promise async 事件循环

 事件循环 是分为同步任务异步任务,同步任务在浏览器主线程上执行,等同步任务执行完之后再去查看宏任务队列和微任务队列,微任务的优先级比宏任务高,因此先执行微任务队列里面的任务;而promise的async和await就属于微任务。

async await

当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象,Promise的同步代码放到同步队列,async的同步代码放到微任务队列

async function test() {
    return 1   // async的函数会在这里帮我们隐士使用Promise.resolve(1)
}
// 等价于下面的代码
function test() {
   return new Promise(function(resolve, reject) {
       resolve(1)
   })
}

await表示等待,是右侧「表达式」的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且只能在带有async的内部使用

使用await时,会从右往左执行,当遇到await时, 会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码, 并且当await执行完毕之后,会先处理微任务队列的代码

可见async只是一个语法糖,只是帮助我们返回一个Promise而已

console.log(1);
async function fn(){
    console.log(2)
    await console.log(3)
    await console.log(4)
    await console.log("await之后的:",11)
    await console.log("await之后的:",22)
    await console.log("await之后的:",33)
    await console.log("await之后的:",44)
}
setTimeout(()=>{
    console.log(5)
},0)
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

/**
 * 执行结果为:
 * 1
 * 2
 * 3
 * 6
 * 8
 * 4
 * 7
 * await之后的: 11
 * await之后的: 22
 * await之后的: 33
 * await之后的: 44
 * 5 */

 

由此可见,代码执行的时候,只要碰见 await ,都会执行完当前的 await 之后,把 await 后面的代码放到微任务队列里面。但是定时器里面的 5 是最后打印出来的,可见当不断碰见 await ,把 await 之后的代码不断的放到微任务队列里面的时候,代码执行顺序是会把微任务队列执行完毕,才会去执行宏任务队列里面的代码。

闭包

闭包是指有权访问另外一个函数作用域中的变量的函数 坑点1: 引用的变量可能发生变化

**

function outer() {
      var result = [];
      for (var i = 0i<10; i++){
        result.[i] = function () {
            console.info(i)
        }
     }
     return result
}

看样子result每个闭包函数对打印对应数字,1,2,3,4,...,10, 实际不是,因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束,i已经变成10了,所以执行每个闭包函数,结果打印10, 10, ..., 10
怎么解决这个问题呢?

**

function outer() {
      var result = [];
      forvar i = 0; i<10; i++){
        result.[i] = function (num) {
             return function() {
                   console.info(num);    // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
             }
        }(i)
     }
     return result
}

坑点2: this指向问题

**

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined
// 因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows

坑点3:内存泄露问题

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}

// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el
}

(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

用promise写一个sleep函数

function sleep(time){
return new Promise(resolve => setTimeout(resolve,time))
}
sleep(3000).then(()=>console.log(1))

注意:return new Promise((resolve)=>{}).then(()=>{})都是内部返回函数

原型链

每个对象都有__proto__属性

每个构造函数都有一个名为prototype的方法,既然是方法,那么就是一个对象,所以prototype同样带有__proto__属性

每个对象的__proto__指向它构造函数的prototype

计网

状态码

100:Continue 继续。客户端应继续其请求

200:OK 请求成功。一般用于 GET 与 POST 请求。

204:No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档

301:Moved Permanently 永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI 代替。

302:Found 临时移动,与 301 类似。但资源只是临时被移动。客户端应继续使用原有URI

304:Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。

400:Bad Request 客户端请求的语法错误,服务器无法理解。

401:Unauthorized 请求要求用户的身份认证

403:Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求

405:Method Not Allowed 客户端请求中的方法被禁止

500:Internal Server Error 服务器内部错误,无法完成请求。

cookie和session

当服务器收到 HTTP 请求时,服务器可以在响应头里面添加一个 Set-Cookie 选项。浏览器收到响应后通常会保存下 Cookie,之后对该服务器每一次请求中都通过  Cookie 请求头部将 Cookie 信息发送给服务器

请求头

GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

有两种方法可以确保 Cookie 被安全发送,并且不会被意外的参与者或脚本访问:Secure 属性和HttpOnly 属性。

JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie,此预防措施有助于缓解跨站点脚本(XSS) (en-US)攻击。

Domain 和 Path 标识定义了 Cookie 的作用域: 即允许 Cookie 应该发送给哪些 URL。Domain 指定了哪些主机可以接受 Cookie。如果不指定,默认为 origin不包含子域名。如果指定了Domain,则一般包含子域名,Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)

Cookie的优缺点:
  • 优点:

    1. 极高的扩展性和可用性
    2. 通过良好的编程,控制保存在cookie中的session对象的大小。
    3. 通过加密和安全传输技术(SSL),减少cookie被破解的可能性。
    4. 只在cookie中存放不敏感数据,即使被盗也不会有重大损失。
    5. 控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。
  • 缺点:

    1. cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。
    2. 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。
    3. 有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。
Session的优缺点:
  • 优点

    1. 可以是任何格式,存储量理论上是无限的,数据难以被获取、篡改、不易丢失
    2. 如果要在诸多Web页间传递一个变量,那么用Session变量要比通过QueryString传递变量可使问题简化
    3. 你可以在任何想要使用的时候直接使用session变量,而不必事先声明它
  • 缺点

    1. 占用服务器资源
    2. 当一个用户访问某页面时,每个Session变量的运行环境便自动生成,这些Session变量可在用户离开该页面后仍保留30分钟!(“timeout”的时间长短由Web服务器管理员设定。一些站点上的变量仅维持了3分钟,一些则为10分钟,还有一些则保留至默认值30分钟。)所以,如果在Session中置入了较大的对象,那就有麻烦了!随着站点访问量的增大,服务器将会因此而无法正常运行!
    3. 因为创建Session变量有很大的随意性,可随时调用,不需要开发者做精确地处理,所以,过度使用session变量将会导致代码不可读而且不好维护。

请求携带cookie的问题

axios默认是发送请求的时候不会带上cookie的,须要经过设置withCredentials: true来解决。

1.ajax同域请求下,ajax会自动带上同源的cookie;

2.ajax同域请求下,ajax添加自定义请求头(或原装)header,前端、后台不需要增加任何配置, 并且不会因为增加自定义请求头header,而引起预检查请求(options);

3.ajax跨域请求下,如果不需要携带cookie、请求头header,只需要在后台配置相应参数即可; 后台参数: (1).Access-Control-Allow-Origin:设置允许跨域的配置, 响应头指定了该响应的资源是否被允许与给定的origin共享;

4.ajax跨域请求下,ajax不会自动携带同源的cookie,需要通过前端配置相应参数才可以跨域携带同源cookie,后台配置相应参数才可以跨域返回同源cookie; 前端参数: withCredentials: true(发送Ajax时,Request header中会带上Cookie信息) 后台参数: (1).Access-Control-Allow-Origin:设置允许跨域的配置, 响应头指定了该响应的资源是否被允许与给定的origin共享; 特别说明:配置了Access-Control-Allow-Credentials:true则不能把Access-Control-Allow-Origin设置为通配符*; (2).Access-Control-Allow-Credentials:响应头表示是否可以将对请求的响应暴露给页面(cookie)。返回true则可以,其他值均不可以。

5.ajax请求任何时候都不会带上不同源的cookie(Cookie遵循同源策略);

6.ajax跨域请求下,ajax添加自定义或者原装的请求头,请求会发送两次,第一次预检查请求,第二次正常请求,详细描述: post(或GET)跨域请求时,分为简单请求和复杂请求,跨域携带自定义或者原装请求头头时是复杂请求。 复杂请求会先发送一个method 为option的请求,目的是试探服务器是否接受发起的请求. 如果服务器说可以,再进行post(或GET)请求。 对于java后台web应用,跨域需要添加一个过滤器(过滤器详见下面案例代码),这个过滤器做的事就是,加了几个http header在返回中, Access-Control-Allow-Origin 我能接受的跨域请求来源,配置主机名 Access-Control-Allow-Headers 表示能接受的http头部,别忘了加入你自己发明创造的头部 Access-Control-Allow-Methods 表示能接受的http mothed ,反正就那几种,全写上也无妨,猥琐点就只写 post, options 如果是OPTION返回空,设置返回码为202,202表示通过。 需要前端配置相应参数才可以跨域携带请求头,后台配置相应参数进行跨域携带请求头; 前端参数: crossDomain:true(发送Ajax时,Request header 中会包含跨域的额外信息,但不会含cookie(作用不明,不会影响请求头的携带)) 后台参数(配置预检查过滤器): (1)Access-Control-Allow-Origin:设置允许跨域的配置, 响应头指定了该响应的资源是否被允许与给定的origin共享; (2)Access-Control-Allow-Credentials:响应头表示是否可以将对请求的响应暴露给页面(cookie)。返回true则可以,其他值均不可以; (3)Access-Control-Allow-Headers:用于预检请求中,列出了将会在正式请求的 Access-Control-Request-Headers 字段中出现的首部信息。(自定义请求头); (4)Access-Control-Allow-Methods:在对预检请求的应答中明确了客户端所要访问的资源允许使用的方法或方法列表;

亲测小结论: 1.ajax跨域请求下,后台不配置跨域Access-Control-Allow-Origin,同样能够执行后台方法,但是无法执行ajax的success的方法,控制台报跨域错误; 2.ajax跨域请求下,前端配置withCredentials: false,同样能够执行后台方法,但是无法携带同源cookie,后台无法获取; 3.ajax跨域请求下,前端配置withCredentials: true,后端没有配置Access-Control-Allow-Credentials:true,同样能够执行后台方法,并能够生成cookie并返回浏览器,但是无法执行ajax的success的方法,控制台报跨域错误; 4.ajax跨域请求下,前端配置withCredentials: false或不配置withCredentials,后端配置Access-Control-Allow-Credentials:true或者false,同样能够执行后台方法,并能够生成cookie并返回浏览器,但是无法携带同源cookie,能够执行ajax的success的方法; 5.Cookie携带只区分域名,不区分端口; 6.jsonp可以携带cookie,但只能携带所属域名的cookie(同源策略); 7.jsonp可以跨域生成cookie,流程如下:跨域请求之后,在服务器端生成cookie,并在浏览器端记录相应的cookie; 8.静态资源同样会携带cookie(js和图片等),但是如果是和当前页面不同域只是在network中不显示cookie选项,但是后台能够获取到对应cookie; 9.ajax同域请求会自动带上同源的cookie,不会带上不同源的cookie; 10.这是MDN对withCredentials的解释: MDN-withCredentials ,我接着解释一下同源。 众所周知,ajax请求是有同源策略的,虽然可以应用CORS等手段来实现跨域,但是这并不是说这样就是“同源”了。ajax在请求时就会因为这个同源的问题而决定是否带上cookie,这样解释应该没有问题了吧,还不知道同源策略的,应该去谷歌一下看看。

总结:最好前端后台配置跨域,则同时配置相应的跨域配置,否则总会出现不可控的错误;

Vue

Object.definePropety和Proxy

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象 在vue初始化阶段,Object.defineProperty()进行数据绑定,当修改或调用数据时会触发getter和setter方法,对对象Object.keys(obj)进行遍历,通过递归对对象进行深度监听,但是数组的push等操作不会触发setter,vue2通过重写Array原型上的方法解决了这个问题

proxy

语法:const p = new Proxy(target, handler) 参数:

  1. target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  2. handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。 通过Proxy,我们可以对设置代理的对象上的一些操作进行拦截,外界对这个对象的各种操作,都要先通过这层拦截。(和defineProperty差不多)Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。

访问的对象的新增属性,但是我们访问它的时候,仍然们可以被get拦截到。

v-if v-show

区别

  • 1.手段:v-if是通过控制dom节点的存在与否来控制元素的显隐;v-show是通过设置DOM元素的display样式,block为显示,none为隐藏;
  • 2.编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  • 3.编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
  • 4.性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

使用场景

基于以上区别,因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好

computed和watch

  • 支持缓存,只有依赖数据发生改变,才会重新进行计算;

  • 不支持异步,当 computed 内有异步操作时无效,无法监听数据的变化;

  • computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的。也就是基 于 data 中声明过或者父组件传递的 props 中的数据通过计算得到的值;

  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性 是一个多对一或者一对一,一般用computed;

  • 如果 computed 属性值是函数,那么默认会走 get 方法,函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个 set 方法,当数据变化时,调用 set 方法;

watch

  • 不支持缓存,数据变化,直接会触发相应的操作;
  • watch 支持异步操作;
  • 监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
    当一个属性发生变化时,需要执行对应的操作,一对多;
  • 监听数据必须是 data 中声明过或者组合式api中声明的响应式值或者父组件传递过来的 props 中的数据。当数据变化时触发其他操作,函数有两个参数:
  • immediate:组件加载立即触发回调函数执行;
  • deep: 深度监听;为了发现对象内部值的变化,复杂类型的数据时使用,例如:数组中的对象内容的改变,注意:监听数组的变动不需要这么做。注意:deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到;

注:当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的,这是和 computed 最大的区别。

hash和history

hash:通过监听浏览器的onhashchange()事件变化,查找对应的路由规则

history:利用H5的history中新增的两个API pushState()和replaceState()和一个事件onpopstate监听URL变化

pushState()和replaceState()可以用于浏览器的历史记录栈,通过back、forward、go可以对当前浏览器进行修改,当他们发生修改的时候,尽管url变化了,但是不会立即向后端服务器发送请求,除非点击刷新

总结:1.history和hash一般来说都能用,history美观点 2.history修改更自由, 3.两个原理差不多都是通过事件监听,一个是onhashchange(),另一个onpopstate() 4.history必须要和后端保持一致,路由全覆盖,不然容易报404

vuex原理

虽然vue中提供了props(父传子)commit(子传父)兄弟间也可以用localstorage和sessionstorage。但是这种方式在项目开发中带来的问题比他解决的问题(难管理,难维护,代码复杂,安全性低)更多。vuex的诞生也是为了解决这些问题,从而大大提高我们vue项目的开发效率。

Vue.use(Vuex); // vue的插件机制,安装vuex插件

Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.store) {
                //找到根组件 main 上面挂一个$store
                this.$store = this.$options.store
                // console.log(this.$store);

            } else {
                //非根组件指向其父组件的$store
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })

store注入 vue的实例组件的方式,是通过vue的 mixin机制,借助vue组件的生命周期 钩子 beforeCreate 完成的。即 每个vue组件实例化过程中,会在 beforeCreate 钩子前调用 vuexInit 方法。

  1. new vue实现双向数据绑定:

**

        this._s = new Vue({ 
            data: {
                // 只有data中的数据才是响应式
                state: options.state
            }
        })
  1. getters
 //实现getters原理
        let getters = options.getters || {}
        // console.log(getters);
        // this.getters = getters; //不是直接挂载到 getters上 这样只会拿到整个 函数体
        this.getters = {};
        // console.log(Object.keys(getters))  // ["myAge","myName"]
        Object.keys(getters).forEach((getterName) => {
            // console.log(getterName)  // myAge
            // 将getterName 放到this.getters = {}中
            // console.log(this.state);
            Object.defineProperty(this.getters, getterName, {
                // 当你要获取getterName(myAge)会自动调用get方法
                // 箭头函数中没有this               
                get: () => {
                    return getters[getterName](this.state)
                }
            })
        })

Vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中,getters是借用了Object.defineProperty的getter方法

  1. vuex中,对于同步修改数据状态时,推荐使用mutations进行修改,不推荐直接使用this.$store.state.xxx = xxx进行修改,可以开启严格模式strict: true`进行处理

4.Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

后台API请求在这个模块进行请求。所有的操作最后都要到达mutation进行操作,更改state的状态。

还是上面一个例子,异步修改state的属性

算法

斐波那契动态规划解法(不会存在爆栈的问题)

function fibArr(prod) { if (prod < 2) { return prod; }

const numArr = [0, 1]; for (let i = 2; i <= prod; i++) { numArr.push(numArr[0] + numArr[1]); numArr.splice(0, 1); }

return numArr[1]; }

数组连续最大子数组和

动态规划

        let arr=[-2,1,-3,4,-1,2,1,-5,4]
        function maxVal(arr){
            let res=arr[0]
            for(let i=1;i<arr.length;i++){
                if(arr[i-1]>0){
                    arr[i]=arr[i]+arr[i-1]
                }
                res=Math.max(res,arr[i])
            }
            return res
        }
        console.log(maxVal(arr))

相当于满足条件累加积累在arr[i]上,条件就是之前的累加和>0

贪心

var maxSubArray = function(nums) {
    let maxSum = (sum = nums[0]);
    for (let i = 1; i < nums.length; ++i) {
        sum = Math.max(nums[i], sum + nums[i]);
        maxSum = Math.max(maxSum, sum);
    }
    return maxSum;
};

第k大数

数据结构

链表和数组增删

在查改方面数组可以通过索引迅速确定位置,而链表只能依次寻找,所以数组有着无法匹敌的优势。而在增删方面,数组每次插入元素都要将该元素后面的所有元素向后挪动一位,删除则将该元素之后所有元素向前移动一位。而链表在插入元素的时候,只需将该元素前驱的指针指向本身,将本身的指针指向后驱,删除则将该元素的前驱指向该元素的后驱。相比之下,链表确实优势挺大。

增删一定伴随查找,数组查找效率比链表高(因为数组可以随机访问,链表只能顺序访问),所以如果增删的是最后的元素,可能数组效率高

浏览器

不同页面传参

1.window.name保存数据, window.name='123';

2.localstorage,cookie,session存数据, window.localStorage.name="123"

3.使用url传递参数, ...?a=1&b=2