写点什么

前端二面常考面试题(必备)

作者:loveX001
  • 2022-12-12
    浙江
  • 本文字数:13260 字

    阅读完需:约 44 分钟

与缓存相关的 HTTP 请求头有哪些

强缓存:


  • Expires

  • Cache-Control


协商缓存:


  • Etag、If-None-Match

  • Last-Modified、If-Modified-Since

代码输出结果

function foo(something){    this.a = something}
var obj1 = { foo: foo}
var obj2 = {}
obj1.foo(2); console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);console.log(obj2.a); // 3
var bar = new obj1.foo(4)console.log(obj1.a); // 2console.log(bar.a); // 4
复制代码


输出结果: 2 3 2 4


解析:


  1. 首先执行 obj1.foo(2); 会在 obj 中添加 a 属性,其值为 2。之后执行 obj1.a,a 是右 obj1 调用的,所以 this 指向 obj,打印出 2;

  2. 执行 obj1.foo.call(obj2, 3) 时,会将 foo 的 this 指向 obj2,后面就和上面一样了,所以会打印出 3;

  3. obj1.a 会打印出 2;

  4. 最后就是考察 this 绑定的优先级了,new 绑定是比隐式绑定优先级高,所以会输出 4。

代码输出结果

Promise.resolve(1)  .then(2)  .then(Promise.resolve(3))  .then(console.log)
复制代码


输出结果如下:


1
复制代码


看到这个题目,好多的 then,实际上只需要记住一个原则:.then.catch 的参数期望是函数,传入非函数则会发生值透传


第一个 then 和第二个 then 中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个 then 里,直接打印出 1。

代码输出结果

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。

Number() 的存储空间是多大?如果后台发送了一个超过最大自己的数字怎么办

Math.pow(2, 53) ,53 为有效数字,会发生截断,等于 JS 能支持的最大数字。

常见的浏览器内核比较

  • Trident: 这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。

  • Gecko: 这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。

  • Presto: Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快 3 倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。

  • Webkit: Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。

  • Blink: 谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是 KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。


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

闭包产生的本质

当前环境中存在指向父级作用域的引用

如何解决跨越问题

(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.');     });});
复制代码

一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。


而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

懒加载与预加载的区别

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


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

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

IE 兼容

  • attchEvent('on' + type, handler)

  • detachEvent('on' + type, handler)

回流与重绘的概念及触发条件

(1)回流

当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流


下面这些操作会导致回流:


  • 页面的首次渲染

  • 浏览器的窗口大小发生变化

  • 元素的内容发生变化

  • 元素的尺寸或者位置发生变化

  • 元素的字体大小发生变化

  • 激活 CSS 伪类

  • 查询某些属性或者调用某些方法

  • 添加或者删除可见的 DOM 元素


在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:


  • 全局范围:从根节点开始,对整个渲染树进行重新布局

  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局

(2)重绘

当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘


下面这些操作会导致回流:


  • color、background 相关属性:background-color、background-image 等

  • outline 相关属性:outline-color、outline-width 、text-decoration

  • border-radius、visibility、box-shadow


注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。

如何提⾼webpack 的构建速度?

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码

  2. 通过 externals 配置来提取常⽤库

  3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。

  4. 使⽤ Happypack 实现多线程加速编译

  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度

  6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

代码输出结果

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的结果。

代码输出结果

const async1 = async () => {  console.log('async1');  setTimeout(() => {    console.log('timer1')  }, 2000)  await new Promise(resolve => {    console.log('promise1')  })  console.log('async1 end')  return 'async1 success'} console.log('script start');async1().then(res => console.log(res));console.log('script end');Promise.resolve(1)  .then(2)  .then(Promise.resolve(3))  .catch(4)  .then(res => console.log(res))setTimeout(() => {  console.log('timer2')}, 1000)
复制代码


输出结果如下:


script startasync1promise1script end1timer2timer1
复制代码


代码的执行过程如下:


  1. 首先执行同步带吗,打印出 script start;

  2. 遇到定时器 timer1 将其加入宏任务队列;

  3. 之后是执行 Promise,打印出 promise1,由于 Promise 没有返回值,所以后面的代码不会执行;

  4. 然后执行同步代码,打印出 script end;

  5. 继续执行下面的 Promise,.then 和.catch 期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将 resolve(1)的值传到最后一个 then,直接打印出 1;

  6. 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出 timer2,在四秒后打印出 timer1。

字符串模板

function render(template, data) {    const reg = /\{\{(\w+)\}\}/; // 模板字符串正则    if (reg.test(template)) { // 判断模板里是否有模板字符串        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染        return render(template, data); // 递归的渲染并返回渲染后的结构    }    return template; // 如果模板没有模板字符串直接返回}
复制代码


测试:


let template = '我是{{name}},年龄{{age}},性别{{sex}}';let person = {    name: '布兰',    age: 12}render(template, person); // 我是布兰,年龄12,性别undefined
复制代码

归并排序--时间复杂度 nlog(n)

题目描述:实现一个时间复杂度为 nlog(n)的排序算法


实现代码如下:


function merge(left, right) {  let res = [];  let i = 0;  let j = 0;  while (i < left.length && j < right.length) {    if (left[i] < right[j]) {      res.push(left[i]);      i++;    } else {      res.push(right[j]);      j++;    }  }  if (i < left.length) {    res.push(...left.slice(i));  } else {    res.push(...right.slice(j));  }  return res;}
function mergeSort(arr) { if (arr.length < 2) { return arr; } const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); return merge(left, right);}// console.log(mergeSort([3, 6, 2, 4, 1]));
复制代码

浏览器的渲染过程

浏览器渲染主要有以下步骤:


  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。

  • 然后对 CSS 进行解析,生成 CSSOM 规则树。

  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。

  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。


大致过程如图所示:


注意: 这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

代码输出结果

function a() {  console.log(this);}a.call(null);
复制代码


打印结果:window 对象


根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是 null 或者 undefined,call 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined,其 this 都是全局对象 window。所以,在浏览器上答案是输出 window 对象。


要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined:


'use strict';
function a() { console.log(this);}a.call(null); // nulla.call(undefined); // undefined
复制代码


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
前端二面常考面试题(必备)_JavaScript_loveX001_InfoQ写作社区