写点什么

大厂前端面试考什么?

作者:loveX001
  • 2022-11-17
    浙江
  • 本文字数:17326 字

    阅读完需:约 57 分钟

async/await 如何捕获异常

async function fn(){    try{        let a = await Promise.reject('error')    }catch(error){        console.log(error)    }}
复制代码

Promise.reject

Promise.reject = function(reason) {    return new Promise((resolve, reject) => reject(reason));}
复制代码

如何防御 XSS 攻击?

可以看到 XSS 危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:


  • 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。

  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。


  1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。

  2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式


  • 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

浏览器渲染进程的线程有哪些

浏览器的渲染进程的线程总共有五种: (1)GUI 渲染线程 负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。


注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。


(2)JS 引擎线程 JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序;


注意:GUI 渲染线程与 JS 引擎线程的互斥关系,所以如果 JS 执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。


(3)时间触发线程 时间触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS 引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;


注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行);


(4)定时器触发进程 定时器触发进程即 setInterval 与 setTimeout 所在线程;浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;


注意:W3C 在 HTML 标准中规定,定时器的定时时间不能小于 4ms,如果是小于 4ms,则默认为 4ms。


(5)异步 http 请求线程


  • XMLHttpRequest 连接后通过浏览器新开一个线程请求;

  • 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行;


参考:前端进阶面试题详细解答

Vue 通信

1.props和$emit2.中央事件总线 EventBus(基本不用)3.vuex(官方推荐状态管理器)4.$parent和$children当然还有一些其他办法,但基本不常用,或者用起来太复杂来。 介绍来通信的方式,还可以扩展说一下使用场景,如何使用,注意事项之类的。
复制代码

什么是原型什么是原型链?

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>
</body><script> function Person () { } var person = new Person(); person.name = 'Kevin'; console.log(person.name) // Kevin
// prototype function Person () { } Person.prototype.name = 'Kevin'; var person1 = new Person(); var person2 = new Person(); console.log(person1.name)// Kevin console.log(person2.name)// Kevin
// __proto__ function Person () { } var person = new Person(); console.log(person.__proto__ === Person.prototype) // true
//constructor function Person() { } console.log(Person === Person.prototype.constructor) // true
//综上所述 function Person () { } var person = new Person() console.log(person.__proto__ == Person.prototype) // true console.log(Person.prototype.constructor == Person) // true //顺便学习一下ES5得方法,可以获得对象得原型 console.log(Object.getPrototypeOf(person) === Person.prototype) // true
//实例与原型 function Person () { } Person.prototype.name = 'Kevin'; var person = new Person(); person.name = 'Daisy'; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin
//原型得原型 var obj = new Object(); obj.name = 'Kevin', console.log(obj.name) //Kevin
//原型链 console.log(Object.prototype.__proto__ === null) //true // null 表示"没用对象" 即该处不应该有值
// 补充 function Person() { } var person = new Person() console.log(person.constructor === Person) // true //当获取person.constructor时,其实person中并没有constructor属性,当不能读取到constructor属性时,会从person的原型 //也就是Person.prototype中读取时,正好原型中有该属性,所以 person.constructor === Person.prototype.constructor
//__proto__ //其次是__proto__,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Person.prototype中,实际上,它 // 是来自与Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了 // Object.getPrototypeOf(obj) 总结: 1、当一个对象查找属性和方法时会从自身查找,如果查找不到则会通过__proto__指向被实例化的构造函数的prototype 2、隐式原型也是一个对象,是指向我们构造函数的原型 3、除了最顶层的Object对象没有__proto_,其他所有的对象都有__proto__,这是隐式原型 4、隐式原型__proto__的作用是让对象通过它来一直往上查找属性或方法,直到找到最顶层的Object的__proto__属性,它的值是null,这个查找的过程就是原型链


</script></html>
复制代码

首屏和白屏时间如何计算

首屏时间的计算,可以由 Native WebView 提供的类似 onload 的方法实现,在 ios 下对应的是 webViewDidFinishLoad,在 android 下对应的是 onPageFinished 事件。


白屏的定义有多种。可以认为“没有任何内容”是白屏,可以认为“网络或服务异常”是白屏,可以认为“数据加载中”是白屏,可以认为“图片加载不出来”是白屏。场景不同,白屏的计算方式就不相同。


方法 1:当页面的元素数小于 x 时,则认为页面白屏。比如“没有任何内容”,可以获取页面的 DOM 节点数,判断 DOM 节点数少于某个阈值 X,则认为白屏。 方法 2:当页面出现业务定义的错误码时,则认为是白屏。比如“网络或服务异常”。 方法 3:当页面出现业务定义的特征值时,则认为是白屏。比如“数据加载中”。

如何提⾼webpack 的打包速度?

(1)优化 Loader

对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。


首先我们优化 Loader 的文件搜索范围


module.exports = {  module: {    rules: [      {        // js 文件才使用 babel        test: /\.js$/,        loader: 'babel-loader',        // 只在 src 文件夹下查找        include: [resolve('src')],        // 不会去查找的路径        exclude: /node_modules/      }    ]  }}
复制代码


对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。


当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间


loader: 'babel-loader?cacheDirectory=true'
复制代码

(2)HappyPack

受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。


HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了


module: {  loaders: [    {      test: /\.js$/,      include: [resolve('src')],      exclude: /node_modules/,      // id 后面的内容对应下面      loader: 'happypack/loader?id=happybabel'    }  ]},plugins: [  new HappyPack({    id: 'happybabel',    loaders: ['babel-loader?cacheDirectory'],    // 开启 4 个线程    threads: 4  })]
复制代码

(3)DllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:


// 单独配置在一个文件中// webpack.dll.conf.jsconst path = require('path')const webpack = require('webpack')module.exports = {  entry: {    // 想统一打包的类库    vendor: ['react']  },  output: {    path: path.join(__dirname, 'dist'),    filename: '[name].dll.js',    library: '[name]-[hash]'  },  plugins: [    new webpack.DllPlugin({      // name 必须和 output.library 一致      name: '[name]-[hash]',      // 该属性需要与 DllReferencePlugin 中一致      context: __dirname,      path: path.join(__dirname, 'dist', '[name]-manifest.json')    })  ]}
复制代码


然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中


// webpack.conf.jsmodule.exports = {  // ...省略其他配置  plugins: [    new webpack.DllReferencePlugin({      context: __dirname,      // manifest 就是之前打包出来的 json 文件      manifest: require('./dist/vendor-manifest.json'),    })  ]}
复制代码

(4)代码压缩

在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。


在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。

(5)其他

可以通过一些小的优化点来加快打包速度


  • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面

  • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径

  • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

setTimeout 模拟 setInterval

描述:使用setTimeout模拟实现setInterval的功能。


实现


const mySetInterval(fn, time) {    let timer = null;    const interval = () => {        timer = setTimeout(() => {            fn();  // time 时间之后会执行真正的函数fn            interval();  // 同时再次调用interval本身        }, time)    }    interval();  // 开始执行    // 返回用于关闭定时器的函数    return () => clearTimeout(timer);}
// 测试const cancel = mySetInterval(() => console.log(1), 400);setTimeout(() => { cancel();}, 1000); // 打印两次1
复制代码

Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。


Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。


(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。


(2)Pending callbacks:执行推迟到下一个循环迭代的 I / O 回调(系统调用相关的回调)。


(3)Idle/Prepare:仅供内部使用。


(4)Poll(轮询阶段)


  • 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。

  • 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval 和 setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的 I/O 操作完成,并马上执行相应的回调,直到所有回调执行完毕。


(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。


(6)Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。


下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机


setTimeout(() => {    console.log('setTimeout')}, 0)setImmediate(() => {    console.log('setImmediate')})
复制代码


对于以上代码来说,setTimeout 可能执行在前,也可能执行在后


  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的

  • 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

  • 那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了


当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:


const fs = require('fs')fs.readFile(__filename, () => {    setTimeout(() => {        console.log('timeout');    }, 0)    setImmediate(() => {        console.log('immediate')    })})
复制代码


在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。


上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,


setTimeout(() => {  console.log('timer21')}, 0)Promise.resolve().then(function() {  console.log('promise1')})
复制代码


对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。


最后来看 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。


setTimeout(() => { console.log('timer1') Promise.resolve().then(function() {   console.log('promise1') })}, 0)process.nextTick(() => { console.log('nextTick') process.nextTick(() => {   console.log('nextTick')   process.nextTick(() => {     console.log('nextTick')     process.nextTick(() => {       console.log('nextTick')     })   }) })})
复制代码


对于以上代码,永远都是先把 nextTick 全部打印出来。

什么是中间人攻击?如何防范中间人攻击?

中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。


攻击过程如下:


  • 客户端发送请求到服务端,请求被中间⼈截获

  • 服务器向客户端发送公钥

  • 中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个伪造的公钥,发给客户端

  • 客户端收到伪造的公钥后,⽣成加密 hash 值发给服务器

  • 中间⼈获得加密 hash 值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密 hash 值,发给服务器

  • 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

Promise.all

描述:所有 promise 的状态都变成 fulfilled,就会返回一个状态为 fulfilled 的数组(所有promisevalue)。只要有一个失败,就返回第一个状态为 rejectedpromise 实例的 reason


实现


Promise.all = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return resolve(promises);            let result = [];            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => {                        count++;                        result[index] = value;                        if(count === promises.length) resolve(result);                    },                     reason => reject(reason)                );            })        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

正向代理和反向代理的区别

  • 正向代理:


客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。


  • 反向代理:


服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。


正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。

说一下 web worker

在 HTML 页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。


如何创建 web worker:


  1. 检测浏览器对于 web worker 的支持性

  2. 创建 web worker 文件(js,回传函数等)

  3. 创建 web worker 对象

懒加载的概念

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。


如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

说一说 SessionStorage 和 localStorage 还有 cookie

共同点:都是保存在浏览器端、且同源的不同点:    1.cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。    cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下    sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。    2.存储大小限制也不同,cookie数据不能超过4K,sessionStorage和localStorage可以达到5M    3.sessionStorage:仅在当前浏览器窗口关闭之前有效;    localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;    cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭    4.作用域不同    sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;    localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在    cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在
复制代码

说一说你用过的 css 布局

gird布局,layout布局,flex布局,双飞翼,圣杯布局等
复制代码

如何解决跨越问题

(1)CORS

下面是 MDN 对于 CORS 的定义:


跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器  让运行在一个 origin (domain)上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。


CORS 需要浏览器和服务器同时支持,整个 CORS 过程都是浏览器完成的,无需用户参与。因此实现 CORS 的关键就是服务器,只要服务器实现了 CORS 请求,就可以跨源通信了。


浏览器将 CORS 分为简单请求非简单请求


简单请求不会触发 CORS 预检请求。若该请求满足以下两个条件,就可以看作是简单请求:


1)请求方法是以下三种方法之一:


  • HEAD

  • GET

  • POST


2)HTTP 的头信息不超出以下几种字段:


  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain


若不满足以上条件,就属于非简单请求了。


(1)简单请求过程:


对于简单请求,浏览器会直接发出 CORS 请求,它会在请求的头信息中增加一个 Orign 字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果 Orign 指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头:


Access-Control-Allow-Origin: http://api.bob.com  // 和Orign一直Access-Control-Allow-Credentials: true   // 表示是否允许发送CookieAccess-Control-Expose-Headers: FooBar   // 指定返回其他字段的值Content-Type: text/html; charset=utf-8   // 表示文档类型
复制代码


如果 Orign 指定的域名不在许可范围之内,服务器会返回一个正常的 HTTP 回应,浏览器发现没有上面的 Access-Control-Allow-Origin 头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是 200。


在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin


(2)非简单请求过程


非简单请求是对服务器有特殊要求的请求,比如请求方法为 DELETE 或者 PUT 等。非简单请求的 CORS 请求会在正式通信之前进行一次 HTTP 查询请求,称为预检请求


浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些 HTTP 请求方式和头信息字段,只有得到肯定的回复,才会进行正式的 HTTP 请求,否则就会报错。


预检请求使用的请求方法是 OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是 Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:


  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。

  • Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。


服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有 Access-Control-Allow-Origin 这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。


服务器回应的 CORS 的字段如下:


Access-Control-Allow-Origin: http://api.bob.com  // 允许跨域的源地址Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法Access-Control-Allow-Headers: X-Custom-Header  // 服务器支持的所有头信息字段Access-Control-Allow-Credentials: true   // 表示是否允许发送CookieAccess-Control-Max-Age: 1728000  // 用来指定本次预检请求的有效期,单位为秒
复制代码


只要服务器通过了预检请求,在以后每次的 CORS 请求都会自带一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


在非简单请求中,至少需要设置以下字段:


'Access-Control-Allow-Origin'  'Access-Control-Allow-Methods''Access-Control-Allow-Headers'
复制代码
减少 OPTIONS 请求次数:

OPTIONS 请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少 OPTIONS 请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的 URL 的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。

CORS 中 Cookie 相关问题:

在 CORS 请求中,如果想要传递 Cookie,就要满足以下三个条件:


  • 在请求中设置 withCredentials


默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.


// 原生 xml 的设置方式var xhr = new XMLHttpRequest();xhr.withCredentials = true;// axios 设置方式axios.defaults.withCredentials = true;
复制代码


  • Access-Control-Allow-Credentials 设置为 true

  • Access-Control-Allow-Origin 设置为非 *

(2)JSONP

jsonp 的原理就是利用<script>标签没有跨域限制,通过<script>标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。1)原生 JS 实现:


<script>    var script = document.createElement('script');    script.type = 'text/javascript';    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';    document.head.appendChild(script);    // 回调执行函数    function handleCallback(res) {        alert(JSON.stringify(res));    } </script>
复制代码


服务端返回如下(返回时即执行全局函数):


handleCallback({"success": true, "user": "admin"})
复制代码


2)Vue axios 实现:


this.$http = axios;this.$http.jsonp('http://www.domain2.com:8080/login', {    params: {},    jsonp: 'handleCallback'}).then((res) => {    console.log(res); })
复制代码


后端 node.js 代码:


var querystring = require('querystring');var http = require('http');var server = http.createServer();server.on('request', function(req, res) {    var params = querystring.parse(req.url.split('?')[1]);    var fn = params.callback;    // jsonp返回设置    res.writeHead(200, { 'Content-Type': 'text/javascript' });    res.write(fn + '(' + JSON.stringify(params) + ')');    res.end();});server.listen('8080');console.log('Server is running at port 8080...');
复制代码


JSONP 的缺点:


  • 具有局限性, 仅支持 get 方法

  • 不安全,可能会遭受 XSS 攻击

(3)postMessage 跨域

postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:


  • 页面和其打开的新窗口的数据传递

  • 多窗口之间消息传递

  • 页面与嵌套的 iframe 消息传递

  • 上面三个场景的跨域数据传递


用法:postMessage(data,origin)方法接受两个参数:


  • data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify()序列化。

  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。


1)a.html:(domain1.com/a.html)


<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script>           var iframe = document.getElementById('iframe');    iframe.onload = function() {        var data = {            name: 'aym'        };        // 向domain2传送跨域数据        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');    };    // 接受domain2返回数据    window.addEventListener('message', function(e) {        alert('data from domain2 ---> ' + e.data);    }, false);</script>
复制代码


2)b.html:(domain2.com/b.html)


<script>    // 接收domain1的数据    window.addEventListener('message', function(e) {        alert('data from domain1 ---> ' + e.data);        var data = JSON.parse(e.data);        if (data) {            data.number = 16;            // 处理后再发回domain1            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');        }    }, false);</script>
复制代码

(4)nginx 代理跨域

nginx 代理跨域,实质和 CORS 跨域原理一样,通过配置文件设置请求响应头 Access-Control-Allow-Origin…等字段。


1)nginx 配置解决 iconfont 跨域浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。


location / {  add_header Access-Control-Allow-Origin *;}
复制代码


2)nginx 反向代理接口跨域跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用 HTTP 接口只是使用 HTTP 协议,不需要同源策略,也就不存在跨域问题。实现思路:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域访问。


nginx 具体配置:


#proxy服务器server {    listen       81;    server_name  www.domain1.com;    location / {        proxy_pass   http://www.domain2.com:8080;  #反向代理        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名        index  index.html index.htm;        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*        add_header Access-Control-Allow-Credentials true;    }}
复制代码

(5)nodejs 中间件代理跨域

node 中间件实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登录认证。


1)非 vue 框架的跨域 使用 node + express + http-proxy-middleware 搭建一个 proxy 服务器。


  • 前端代码:


var xhr = new XMLHttpRequest();// 前端开关:浏览器是否读写cookiexhr.withCredentials = true;// 访问http-proxy-middleware代理服务器xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);xhr.send();
复制代码


  • 中间件服务器代码:


var express = require('express');var proxy = require('http-proxy-middleware');var app = express();app.use('/', proxy({    // 代理跨域目标接口    target: 'http://www.domain2.com:8080',    changeOrigin: true,    // 修改响应头信息,实现跨域并允许带cookie    onProxyRes: function(proxyRes, req, res) {        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');        res.header('Access-Control-Allow-Credentials', 'true');    },    // 修改响应信息中的cookie域名    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改}));app.listen(3000);console.log('Proxy server is listen at port 3000...');
复制代码


2)vue 框架的跨域


node + vue + webpack + webpack-dev-server 搭建的项目,跨域请求接口,直接修改 webpack.config.js 配置。开发环境下,vue 渲染服务和接口代理服务都是 webpack-dev-server 同一个,所以页面与代理接口之间不再跨域。


webpack.config.js 部分配置:


module.exports = {    entry: {},    module: {},    ...    devServer: {        historyApiFallback: true,        proxy: [{            context: '/login',            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口            changeOrigin: true,            secure: false,  // 当代理某些https服务报错时用            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改        }],        noInfo: true    }}
复制代码

(6)document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。1)父窗口:(domain.com/a.html)


<iframe id="iframe" src="http://child.domain.com/b.html"></iframe><script>    document.domain = 'domain.com';    var user = 'admin';</script>
复制代码


1)子窗口:(child.domain.com/a.html)


<script>    document.domain = 'domain.com';    // 获取父窗口中变量    console.log('get js data from parent ---> ' + window.parent.user);</script>
复制代码

(7)location.hash + iframe 跨域

实现原理:a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。


具体实现:A 域:a.html -> B 域:b.html -> A 域:c.html,a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信,但 c 与 a 同域,所以 c 可通过 parent.parent 访问 a 页面所有对象。


1)a.html:(domain1.com/a.html)


<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script>    var iframe = document.getElementById('iframe');    // 向b.html传hash值    setTimeout(function() {        iframe.src = iframe.src + '#user=admin';    }, 1000);        // 开放给同域c.html的回调方法    function onCallback(res) {        alert('data from c.html ---> ' + res);    }</script>
复制代码


2)b.html:(.domain2.com/b.html)


<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe><script>    var iframe = document.getElementById('iframe');    // 监听a.html传来的hash值,再传给c.html    window.onhashchange = function () {        iframe.src = iframe.src + location.hash;    };</script>
复制代码


<script>    // 监听b.html传来的hash值    window.onhashchange = function () {        // 再通过操作同域a.html的js回调,将结果传回        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));    };</script>
复制代码

(8)window.name + iframe 跨域

window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。


1)a.html:(domain1.com/a.html)


var proxy = function(url, callback) {    var state = 0;    var iframe = document.createElement('iframe');    // 加载跨域页面    iframe.src = url;    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name    iframe.onload = function() {        if (state === 1) {            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据            callback(iframe.contentWindow.name);            destoryFrame();        } else if (state === 0) {            // 第1次onload(跨域页)成功后,切换到同域代理页面            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';            state = 1;        }    };    document.body.appendChild(iframe);    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)    function destoryFrame() {        iframe.contentWindow.document.write('');        iframe.contentWindow.close();        document.body.removeChild(iframe);    }};// 请求跨域b页面数据proxy('http://www.domain2.com/b.html', function(data){    alert(data);});
复制代码


2)proxy.html:(domain1.com/proxy.html)


中间代理页,与 a.html 同域,内容为空即可。3)b.html:(domain2.com/b.html)


<script>        window.name = 'This is domain2 data!';</script>
复制代码


通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

(9)WebSocket 协议跨域

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。


原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。


1)前端代码:


<div>user input:<input type="text"></div><script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script><script>var socket = io('http://www.domain2.com:8080');// 连接成功处理socket.on('connect', function() {    // 监听服务端消息    socket.on('message', function(msg) {        console.log('data from server: ---> ' + msg);     });    // 监听服务端关闭    socket.on('disconnect', function() {         console.log('Server socket has closed.');     });});document.getElementsByTagName('input')[0].onblur = function() {    socket.send(this.value);};</script>
复制代码


2)Nodejs socket 后台:


var http = require('http');var socket = require('socket.io');// 启http服务var server = http.createServer(function(req, res) {    res.writeHead(200, {        'Content-type': 'text/html'    });    res.end();});server.listen('8080');console.log('Server is running at port 8080...');// 监听socket连接socket.listen(server).on('connection', function(client) {    // 接收信息    client.on('message', function(msg) {        client.send('hello:' + msg);        console.log('data from client: ---> ' + msg);    });    // 断开处理    client.on('disconnect', function() {        console.log('Client socket has closed.');     });});
复制代码

闭包

首先说明什么是闭包,闭包简单来说就是函数嵌套函数,内部函数引用来外部函数的变量,从而导致垃圾回收机制没有把当前变量回收掉,这样的操作带来了内存泄漏的影响,当内存泄漏到一定程度会影响你的项目运行变得卡顿等等问题。因此在项目中我们要尽量避免内存泄漏。
复制代码

图片懒加载

实现getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。


// scr 加载默认图片,data-src 保存实施懒加载后的图片// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />let imgs = [...document.querySelectorAll("img")];const len = imgs.length;
let lazyLoad = function() { let count = 0; let deleteImgs = []; // 获取当前可视区的高度 let viewHeight = document.documentElement.clientHeight; // 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop) let scrollTop = window.pageYOffset; imgs.forEach((img) => { // 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离 let bound = img.getBoundingClientRect(); // 当前图片距离网页顶部的距离 // let imgOffsetTop = img.offsetTop;
// 判断图片是否在可视区内,如果在就加载(两种判断方式) // if(imgOffsetTop < scrollTop + viewHeight) if (bound.top < viewHeight) { img.src = img.dataset.src; // 替换待加载的图片 src count++; deleteImgs.push(img); // 最后所有的图片加载完毕后需要解绑监听事件 if(count === len) { document.removeEventListener("scroll", imgThrottle); } } }); // 图片加载完会从 `img` 标签组成的 DOM 列表中删除 imgs = imgs.filter((img) => !deleteImgs.includes(img));}
window.onload = function () { lazyLoad();};// 使用 防抖/节流 优化一下滚动事件let imgThrottle = debounce(lazyLoad, 1000);// 监听 `scroll` 事件window.addEventListener("scroll", imgThrottle);
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
大厂前端面试考什么?_JavaScript_loveX001_InfoQ写作社区