写点什么

前端面试中小型公司都考些什么

作者:loveX001
  • 2022-10-28
    浙江
  • 本文字数:11176 字

    阅读完需:约 37 分钟

什么是 XSS 攻击?

(1)概念

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。


XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。


攻击者可以通过这种攻击方式可以进行以下操作:


  • 获取页面的数据,如 DOM、cookie、localStorage;

  • DOS 攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;

  • 破坏页面结构;

  • 流量劫持(将链接指向某网站);

(2)攻击类型

XSS 可以分为存储型、反射型和 DOM 型:


  • 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。

  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行,最终完成 XSS 攻击。 

  • DOM 型指的通过修改页面的 DOM 节点形成的 XSS。


1)存储型 XSS 的攻击步骤:


  1. 攻击者将恶意代码提交到⽬标⽹站的数据库中。

  2. ⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。

  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。


2)反射型 XSS 的攻击步骤:


  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. ⽤户打开带有恶意代码的 URL 时,⽹站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。

  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。


反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。


3)DOM 型 XSS 的攻击步骤:


  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. ⽤户打开带有恶意代码的 URL。

  3. ⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL 中的恶意代码并执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端 JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。

Nginx 的概念及其工作原理

Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。


传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。


Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和 Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的 HTTP 请求,而每个 Apache process 只能处理一个。

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

  • 正向代理:


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


  • 反向代理:


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


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

documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?

MDN 中对documentFragment的解释:


DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。


当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入 DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将 DocumentFragment 节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。

代码输出结果

const promise = new Promise((resolve, reject) => {  console.log(1);  setTimeout(() => {    console.log("timerStart");    resolve("success");    console.log("timerEnd");  }, 0);  console.log(2);});promise.then((res) => {  console.log(res);});console.log(4);
复制代码


输出结果如下:


124timerStarttimerEndsuccess
复制代码


代码执行过程如下:


  • 首先遇到 Promise 构造函数,会先执行里面的内容,打印1

  • 遇到定时器steTimeout,它是一个宏任务,放入宏任务队列;

  • 继续向下执行,打印出 2;

  • 由于Promise的状态此时还是pending,所以promise.then先不执行;

  • 继续执行下面的同步任务,打印出 4;

  • 此时微任务队列没有任务,继续执行下一轮宏任务,执行steTimeout

  • 首先执行timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列,再执行timerEnd

  • 执行完这个宏任务,就去执行微任务promise.then,打印出resolve的结果。

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。


  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。

  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

什么是 CSRF 攻击?

(1)概念

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。


CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。

(2)攻击类型

常见的 CSRF 攻击有三种:


  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。

  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。

  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。


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

setTimeout(fn, 0)多久才执行,Event Loop

setTimeout 按照顺序放到队列里面,然后等待函数调用栈清空之后才开始执行,而这些操作进入队列的顺序,则由设定的延迟时间来决定

代码输出结果

console.log(1);
setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3) });});
new Promise((resolve, reject) => { console.log(4) resolve(5)}).then((data) => { console.log(data);})
setTimeout(() => { console.log(6);})
console.log(7);
复制代码


代码输出结果如下:


1475236
复制代码


代码执行过程如下:


  1. 首先执行 scrip 代码,打印出 1;

  2. 遇到第一个定时器 setTimeout,将其加入到宏任务队列;

  3. 遇到 Promise,执行里面的同步代码,打印出 4,遇到 resolve,将其加入到微任务队列;

  4. 遇到第二个定时器 setTimeout,将其加入到红任务队列;

  5. 执行 script 代码,打印出 7,至此第一轮执行完成;

  6. 指定微任务队列中的代码,打印出 resolve 的结果:5;

  7. 执行宏任务中的第一个定时器 setTimeout,首先打印出 2,然后遇到 Promise.resolve().then(),将其加入到微任务队列;

  8. 执行完这个宏任务,就开始执行微任务队列,打印出 3;

  9. 继续执行宏任务队列中的第二个定时器,打印出 6。

IndexedDB 有哪些特点?

IndexedDB 具有以下特点:


  • 键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

  • 异步:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

  • 支持事务:IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源限制: IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

  • 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

  • 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

渲染过程中遇到 JS 文件如何处理?

JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。

代码输出结果

var length = 10;function fn() {    console.log(this.length);}
var obj = { length: 5, method: function(fn) { fn(); arguments[0](); }};
obj.method(fn, 1);
复制代码


输出结果: 10 2


解析:


  1. 第一次执行 fn(),this 指向 window 对象,输出 10。

  2. 第二次执行 arguments[0],相当于 arguments 调用方法,this 指向 arguments,而这里传了两个参数,故输出 arguments 长度为 2。

如何提⾼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 不扫描该文件,这种方式对于大型的类库很有帮助

浏览器本地存储方式及使用场景

(1)Cookie

Cookie 是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie 就出现了。Cookie 的大小只有 4kb,它是一种纯文本文件,每次发起 HTTP 请求都会携带 Cookie。


Cookie 的特性:


  • Cookie 一旦创建成功,名称就无法修改

  • Cookie 是无法跨域名的,也就是说 a 域名和 b 域名下的 cookie 是无法共享的,这也是由 Cookie 的隐私安全性决定的,这样就能够阻止非法获取其他网站的 Cookie

  • 每个域名下 Cookie 的数量不能超过 20 个,每个 Cookie 的大小不能超过 4kb

  • 有安全问题,如果 Cookie 被拦截了,那就可获得 session 的所有信息,即使加密也于事无补,无需知道 cookie 的意义,只要转发 cookie 就能达到目的

  • Cookie 在请求一个新的页面的时候都会被发送过去


如果需要域名之间跨域共享 Cookie,有两种方法:


  1. 使用 Nginx 反向代理

  2. 在一个站点登陆之后,往其他网站写 Cookie。服务端的 Session 存储到一个节点,Cookie 存储 sessionId


Cookie 的使用场景:


  • 最常见的使用场景就是 Cookie 和 session 结合使用,我们将 sessionId 存储到 Cookie 中,每次发请求都会携带这个 sessionId,这样服务端就知道是谁发起的请求,从而响应相应的信息。

  • 可以用来统计页面的点击次数

(2)LocalStorage

LocalStorage 是 HTML5 新引入的特性,由于有的时候我们存储的信息较大,Cookie 就不能满足我们的需求,这时候 LocalStorage 就派上用场了。


LocalStorage 的优点:


  • 在大小方面,LocalStorage 的大小一般为 5MB,可以储存更多的信息

  • LocalStorage 是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在

  • 仅储存在本地,不像 Cookie 那样每次 HTTP 请求都会被携带


LocalStorage 的缺点:


  • 存在浏览器兼容问题,IE8 以下版本的浏览器不支持

  • 如果浏览器设置为隐私模式,那我们将无法读取到 LocalStorage

  • LocalStorage 受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问


LocalStorage 的常用 API:


// 保存数据到 localStoragelocalStorage.setItem('key', 'value');
// 从 localStorage 获取数据let data = localStorage.getItem('key');
// 从 localStorage 删除保存的数据localStorage.removeItem('key');
// 从 localStorage 删除所有保存的数据localStorage.clear();
// 获取某个索引的KeylocalStorage.key(index)
复制代码


LocalStorage 的使用场景:


  • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的 LocalStorage 中,当需要换肤的时候,直接操作 LocalStorage 即可

  • 在网站中的用户浏览信息也会存储在 LocalStorage 中,还有网站的一些不常变动的个人信息等也可以存储在本地的 LocalStorage 中

(3)SessionStorage

SessionStorage 和 LocalStorage 都是在 HTML5 才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。


SessionStorage 与 LocalStorage 对比:


  • SessionStorage 和 LocalStorage 都在本地进行数据存储

  • SessionStorage 也有同源策略的限制,但是 SessionStorage 有一条更加严格的限制,SessionStorage 只有在同一浏览器的同一窗口下才能够共享

  • LocalStorage 和 SessionStorage 都不能被爬虫爬取


SessionStorage 的常用 API:


// 保存数据到 sessionStoragesessionStorage.setItem('key', 'value');
// 从 sessionStorage 获取数据let data = sessionStorage.getItem('key');
// 从 sessionStorage 删除保存的数据sessionStorage.removeItem('key');
// 从 sessionStorage 删除所有保存的数据sessionStorage.clear();
// 获取某个索引的KeysessionStorage.key(index)
复制代码


SessionStorage 的使用场景


  • 由于 SessionStorage 具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了。

如何实现浏览器内多个标签页之间的通信?

实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:


  • 使用 websocket 协议,因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。

  • 使用 ShareWorker 的方式,shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。

  • 使用 localStorage 的方式,我们可以在一个标签页对 localStorage 的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候 localStorage 对象就是充当的中介者的角色。

  • 使用 postMessage 方法,如果我们能够获得对应标签页的引用,就可以使用 postMessage 方法,进行通信。

代码输出问题

function A(){}function B(a){  this.a = a;}function C(a){  if(a){this.a = a;  }}A.prototype.a = 1;B.prototype.a = 1;C.prototype.a = 1;
console.log(new A().a);console.log(new B().a);console.log(new C(2).a);
复制代码


输出结果:1 undefined 2


解析:


  1. console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;

  2. console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;

  3. console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。

CSS 如何阻塞文档解析?

理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。然而,存在一个问题,JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后再继续文档的解析。

代码输出结果

function Foo(){    Foo.a = function(){        console.log(1);    }    this.a = function(){        console.log(2)    }}
Foo.prototype.a = function(){ console.log(3);}
Foo.a = function(){ console.log(4);}
Foo.a();let obj = new Foo();obj.a();Foo.a();
复制代码


输出结果:4 2 1


解析:


  1. Foo.a() 这个是调用 Foo 函数的静态方法 a,虽然 Foo 中有优先级更高的属性方法 a,但 Foo 此时没有被调用,所以此时输出 Foo 的静态方法 a 的结果:4

  2. let obj = new Foo(); 使用了 new 方法调用了函数,返回了函数实例对象,此时 Foo 函数内部的属性方法初始化,原型链建立。

  3. obj.a() ; 调用 obj 实例上的方法 a,该实例上目前有两个 a 方法:一个是内部属性方法,另一个是原型上的方法。当这两者都存在时,首先查找 ownProperty ,如果没有才去原型链上找,所以调用实例上的 a 输出:2

  4. Foo.a() ; 根据第 2 步可知 Foo 函数内部的属性方法已初始化,覆盖了同名的静态方法,所以输出:1

代码输出结果

console.log(1)
setTimeout(() => { console.log(2)})
new Promise(resolve => { console.log(3) resolve(4)}).then(d => console.log(d))
setTimeout(() => { console.log(5) new Promise(resolve => { resolve(6) }).then(d => console.log(d))})
setTimeout(() => { console.log(7)})
console.log(8)
复制代码


输出结果如下:


13842567
复制代码


代码执行过程如下:


  1. 首先执行 script 代码,打印出 1;

  2. 遇到第一个定时器,加入到宏任务队列;

  3. 遇到 Promise,执行代码,打印出 3,遇到 resolve,将其加入到微任务队列;

  4. 遇到第二个定时器,加入到宏任务队列;

  5. 遇到第三个定时器,加入到宏任务队列;

  6. 继续执行 script 代码,打印出 8,第一轮执行结束;

  7. 执行微任务队列,打印出第一个 Promise 的 resolve 结果:4;

  8. 开始执行宏任务队列,执行第一个定时器,打印出 2;

  9. 此时没有微任务,继续执行宏任务中的第二个定时器,首先打印出 5,遇到 Promise,首选打印出 6,遇到 resolve,将其加入到微任务队列;

  10. 执行微任务队列,打印出 6;

  11. 执行宏任务队列中的最后一个定时器,打印出 7。

浏览器渲染优化

(1)针对 JavaScript: JavaScript 既会阻塞 HTML 的解析,也会阻塞 CSS 的解析。因此我们可以对 JavaScript 的加载方式进行改变,来进行优化:


(1)尽量将 JavaScript 文件放在 body 的最后


(2) body 中间尽量不要写<script>标签


(3)<script>标签的引入资源方式有三种,有一种就是我们常用的直接引入,还有两种就是使用 async 属性和 defer 属性来异步引入,两者都是去异步加载外部的 JS 文件,不会阻塞 DOM 的解析(尽量使用异步加载)。三者的区别如下:


  • script 立即停止页面渲染去加载资源文件,当资源加载完毕后立即执行 js 代码,js 代码执行完毕后继续渲染页面;

  • async 是在下载完成之后,立即异步加载,加载好后立即执行,多个带 async 属性的标签,不能保证加载的顺序;

  • defer 是在下载完成之后,立即异步加载。加载好后,如果 DOM 树还没构建好,则先等 DOM 树解析好再执行;如果 DOM 树已经准备好,则立即执行。多个带 defer 属性的标签,按照顺序执行。


(2)针对 CSS:使用 CSS 有三种方式:使用 link、@import、内联样式,其中 link 和 @import 都是导入外部样式。它们之间的区别:


  • link:浏览器会派发一个新等线程(HTTP 线程)去加载资源文件,与此同时 GUI 渲染线程会继续向下渲染代码

  • @import:GUI 渲染线程会暂时停止渲染,去服务器加载资源文件,资源文件没有返回之前不会继续渲染(阻碍浏览器渲染)

  • style:GUI 直接渲染


外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以 CSS 一般写在 headr 中,让浏览器尽快发送请求去获取 css 样式。


所以,在开发过程中,导入外部样式使用 link,而不用 @import。如果 css 少,尽可能采用内嵌样式,直接写在 style 标签中。


(3)针对 DOM 树、CSSOM 树: 可以通过以下几种方式来减少渲染的时间:


  • HTML 文件的代码层级尽量不要太深

  • 使用语义化的标签,来避免不标准语义化的特殊处理

  • 减少 CSSD 代码的层级,因为选择器是从左向右进行解析的


(4)减少回流与重绘:


  • 操作 DOM 时,尽量在低层级的 DOM 节点进行操作

  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局

  • 使用 CSS 的表达式

  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。

  • 使用 absolute 或者 fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素

  • 避免频繁操作 DOM,可以创建一个文档片段documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中

  • 将元素先设置display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。

  • 将 DOM 的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制


浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列


浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。


将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
前端面试中小型公司都考些什么_JavaScript_loveX001_InfoQ写作社区