关于前端:有哪些前端面试题是必须要掌握的

33次阅读

共计 26500 个字符,预计需要花费 67 分钟才能阅读完成。

对浏览器的缓存机制的了解

浏览器缓存的全过程:

  • 浏览器第一次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header,以供下次加载时比照应用;
  • 下一次加载资源时,因为强制缓存优先级较高,先比拟以后工夫与上一次返回 200 时的时间差,如果没有超过 cache-control 设置的 max-age,则没有过期,并命中强缓存,间接从本地读取资源。如果浏览器不反对 HTTP1.1,则应用 expires 头判断是否过期;
  • 如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 If-None-Match 和 If-Modified-Since 的申请;
  • 服务器收到申请后,优先依据 Etag 的值判断被申请的文件有没有做批改,Etag 值统一则没有批改,命中协商缓存,返回 304;如果不统一则有改变,间接返回新的资源文件带上新的 Etag 值并返回 200;
  • 如果服务器收到的申请没有 Etag 值,则将 If-Modified-Since 和被申请文件的最初批改工夫做比对,统一则命中协商缓存,返回 304;不统一则返回新的 last-modified 和文件并返回 200;

    很多网站的资源前面都加了版本号,这样做的目标是:每次降级了 JS 或 CSS 文件后,为了避免浏览器进行缓存,强制扭转版本号,客户端浏览器就会从新下载新的 JS 或 CSS 文件,以保障用户可能及时取得网站的最新更新。

浏览器存储

咱们常常须要对业务中的一些数据进行存储,通常能够分为 短暂性存储 和 持久性贮存。

  • 短暂性的时候,咱们只须要将数据存在内存中,只在运行时可用
  • 持久性存储,能够分为 浏览器端 与 服务器端

    • 浏览器:

      • cookie: 通常用于存储用户身份,登录状态等

        • http 中主动携带,体积下限为 4K,可自行设置过期工夫
      • localStorage / sessionStorage: 短暂贮存 / 窗口敞开删除,体积限度为 4~5M
      • indexDB
    • 服务器:

      • 分布式缓存 redis
      • 数据库

cookie 和 localSrorage、session、indexDB 的区别

个性 cookie localStorage sessionStorage indexDB
数据生命周期 个别由服务器生成,能够设置过期工夫 除非被清理,否则始终存在 页面敞开就清理 除非被清理,否则始终存在
数据存储大小 4K 5M 5M 有限
与服务端通信 每次都会携带在 header 中,对于申请性能影响 不参加 不参加 不参加

从上表能够看到,cookie 曾经不倡议用于存储。如果没有大量数据存储需要的话,能够应用 localStoragesessionStorage。对于不怎么扭转的数据尽量应用 localStorage 存储,否则能够用 sessionStorage 存储。

对于 cookie,咱们还须要留神安全性

属性 作用
value 如果用于保留用户登录态,应该将该值加密,不能应用明文的用户标识
http-only 不能通过 JS拜访 Cookie,缩小 XSS攻打
secure 只能在协定为 HTTPS 的申请中携带
same-site 规定浏览器不能在跨域申请中携带 Cookie,缩小 CSRF 攻打
  • Name,即该 Cookie 的名称。Cookie 一旦创立,名称便不可更改。
  • Value,即该 Cookie 的值。如果值为 Unicode 字符,须要为字符编码。如果值为二进制数据,则须要应用 BASE64 编码。
  • Max Age,即该 Cookie 生效的工夫,单位秒,也常和 Expires 一起应用,通过它能够计算出其无效工夫。Max Age如果为负数,则该 CookieMax Age 秒之后生效。如果为正数,则敞开浏览器时 Cookie 即生效,浏览器也不会以任何模式保留该 Cookie
  • Path,即该 Cookie 的应用门路。如果设置为 /path/,则只有门路为 /path/ 的页面能够拜访该 Cookie。如果设置为 /,则本域名下的所有页面都能够拜访该 Cookie
  • Domain,即能够拜访该 Cookie 的域名。例如如果设置为 .zhihu.com,则所有以 zhihu.com,结尾的域名都能够拜访该 CookieSize 字段,即此 Cookie 的大小。
  • Http 字段,即 Cookiehttponly 属性。若此属性为 true,则只有在 HTTP Headers 中会带有此 Cookie 的信息,而不能通过 document.cookie 来拜访此 Cookie。
  • Secure,即该 Cookie 是否仅被应用平安协定传输。平安协定。平安协定有 HTTPS、SSL 等,在网络上传输数据之前先将数据加密。默认为 false

TCP 和 UDP 的区别

UDP TCP
是否连贯 无连贯 面向连贯
是否牢靠 不牢靠传输,不应用流量管制和拥塞管制 牢靠传输(数据程序和正确性),应用流量管制和拥塞管制
连贯对象个数 反对一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅 8 字节 首部最小 20 字节,最大 60 字节
实用场景 实用于实时利用,例如视频会议、直播 实用于要求牢靠传输的利用,例如文件传输

TCP 和 UDP 的概念及特点

TCP 和 UDP 都是传输层协定,他们都属于 TCP/IP 协定族:

(1)UDP

UDP 的全称是 用户数据报协定,在网络中它与 TCP 协定一样用于解决数据包,是一种无连贯的协定。在 OSI 模型中,在传输层,处于 IP 协定的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的毛病,也就是说,当报文发送之后,是无奈得悉其是否平安残缺达到的。

它的特点如下:

1)面向无连贯

首先 UDP 是不须要和 TCP 一样在发送数据前进行三次握手建设连贯的,想发数据就能够开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协定,UDP 只会给数据减少一个 UDP 头标识下是 UDP 协定,而后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2)有单播,多播,播送的性能

UDP 不止反对一对一的传输方式,同样反对一对多,多对多,多对一的形式,也就是说 UDP 提供了单播,多播,播送的性能。

3)面向报文

发送方的 UDP 对应用程序交下来的报文,在增加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因而,应用程序必须抉择适合大小的报文

4)不可靠性

首先不可靠性体现在无连贯上,通信都不须要建设连贯,想发就发,这样的状况必定不牢靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关怀对方是否曾经正确接管到数据了。

再者网络环境时好时坏,然而 UDP 因为没有拥塞管制,始终会以恒定的速度发送数据。即便网络条件不好,也不会对发送速率进行调整。这样实现的弊病就是在网络条件不好的状况下可能会导致丢包,然而长处也很显著,在某些实时性要求高的场景(比方电话会议)就须要应用 UDP 而不是 TCP。

5)头部开销小,传输数据报文时是很高效的。

UDP 头部蕴含了以下几个数据:

  • 两个十六位的端口号,别离为源端口(可选字段)和指标端口
  • 整个数据报文的长度
  • 整个数据报文的测验和(IPv4 可选字段),该字段用于发现头部信息和数据中的谬误

因而 UDP 的头部开销小,只有 8 字节,相比 TCP 的至多 20 字节要少得多,在传输数据报文时是很高效的。

(2)TCP TCP 的全称是传输控制协议是一种面向连贯的、牢靠的、基于字节流的传输层通信协议。TCP 是面向连贯的、牢靠的流协定(流就是指不间断的数据结构)。

它有以下几个特点:

1)面向连贯

面向连贯,是指发送数据之前必须在两端建设连贯。建设连贯的办法是“三次握手”,这样能建设牢靠的连贯。建设连贯,是为数据的牢靠传输打下了根底。

2)仅反对单播传输

每条 TCP 传输连贯只能有两个端点,只能进行点对点的数据传输,不反对多播和播送传输方式。

3)面向字节流

TCP 不像 UDP 一样那样一个个报文独立地传输,而是在不保留报文边界的状况下以字节流形式进行传输。

4)牢靠传输

对于牢靠传输,判断丢包、误码靠的是 TCP 的段编号以及确认号。TCP 为了保障报文传输的牢靠,就给每个包一个序号,同时序号也保障了传送到接收端实体的包的按序接管。而后接收端实体对已胜利收到的字节发回一个相应的确认 (ACK);如果发送端实体在正当的往返时延(RTT) 内未收到确认,那么对应的数据(假如失落了)将会被重传。

5)提供拥塞管制

当网络呈现拥塞的时候,TCP 可能减小向网络注入数据的速率和数量,缓解拥塞。

6)提供全双工通信

TCP 容许通信单方的应用程序在任何时候都能发送数据,因为 TCP 连贯的两端都设有缓存,用来长期寄存双向通信的数据。当然,TCP 能够立刻发送一个数据段,也能够缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于 MSS)

事件循环

  • 默认代码从上到下执行,执行环境通过 script 来执行(宏工作)
  • 在代码执行过程中,调用定时器 promise click事件 … 不会立刻执行,须要期待以后代码全副执行结束
  • 给异步办法划分队列,别离寄存到微工作(立刻寄存)和宏工作(工夫到了或事件产生了才寄存)到队列中
  • script执行结束后,会清空所有的微工作
  • 微工作执行结束后,会渲染页面(不是每次都调用)
  • 再去宏工作队列中看有没有达到工夫的,拿进去其中一个执行
  • 执行结束后,依照上述步骤不停的循环

例子

主动执行的状况 会输入 listener1 listener2 task1 task2

如果手动点击 click 会一个宏工作取出来一个个执行,先执行 click 的宏工作,取出微工作去执行。会输入 listener1 task1 listener2 task2

console.log(1)

async function asyncFunc(){console.log(2)
  // await xx ==> promise.resolve(()=>{console.log(3)}).then()
  // console.log(3) 放到 promise.resolve 或立刻执行
  await console.log(3) 
  // 相当于把 console.log(4)放到了 then promise.resolve(()=>{console.log(3)}).then(()=>{//   console.log(4)
  // })
  // 微工作谁先注册谁先执行
  console.log(4)
}

setTimeout(()=>{console.log(5)})

const promise = new Promise((resolve,reject)=>{console.log(6)
  resolve(7)
})

promise.then(d=>{console.log(d)})

asyncFunc()

console.log(8)

// 输入 1 6 2 3 8 7 4 5

1. 浏览器事件循环

涉及面试题:异步代码执行程序?解释一下什么是 Event Loop

JavaScript 的单线程,与它的用处无关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很简单的同步问题。比方,假设 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上增加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了防止复杂性,从一诞生,JavaScript 就是单线程,这曾经成了这门语言的外围特色,未来也不会扭转

js 代码执行过程中会有很多工作,这些工作总的分成两类:

  • 同步工作
  • 异步工作

当咱们关上网站时,网页的渲染过程就是一大堆同步工作,比方页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的工作,就是异步工作。,咱们用导图来阐明:

咱们解释一下这张图:

  • 同步和异步工作别离进入不同的执行 ” 场合 ”,同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事件实现时,Event Table 会将这个函数移入 Event Queue。
  • 主线程内的工作执行结束为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会一直反复,也就是常说的 Event Loop(事件循环)。

那主线程执行栈何时为空呢?js 引擎存在 monitoring process 过程,会继续一直的查看主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里查看是否有期待被调用的函数

以上就是 js 运行的整体流程

面试中该如何答复呢?上面是我集体举荐的答复:

  • 首先 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保障代码的有序执行
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会始终期待其返回后果,而是会将这个事件挂起,继续执行执行栈中的其余工作
  • 当同步事件执行结束后,再将异步事件对应的回调退出到与以后执行栈中不同的另一个工作队列中期待执行
  • 工作队列能够分为宏工作对列和微工作对列,当以后执行栈中的事件执行结束后,js 引擎首先会判断微工作对列中是否有工作能够执行,如果有就将微工作队首的事件压入栈中执行
  • 当微工作对列中的工作都执行实现后再去判断宏工作对列中的工作。
setTimeout(function() {console.log(1)
}, 0);
new Promise(function(resolve, reject) {console.log(2);
  resolve()}).then(function() {console.log(3)
});
process.nextTick(function () {console.log(4)
})
console.log(5)
  • 第一轮:主线程开始执行,遇到 setTimeout,将 setTimeout 的回调函数丢到宏工作队列中,在往下执行new Promise 立刻执行,输入 2,then 的回调函数丢到微工作队列中,再继续执行,遇到 process.nextTick,同样将回调函数扔到微工作队列,再继续执行,输入 5,当所有同步工作执行实现后看有没有能够执行的微工作,发现有 then 函数和nextTick 两个微工作,先执行哪个呢?process.nextTick指定的异步工作总是产生在所有异步工作之前,因而先执行 process.nextTick 输入 4 而后执行 then 函数输入 3,第一轮执行完结。
  • 第二轮:从宏工作队列开始,发现 setTimeout 回调,输入 1 执行结束,因而后果是 25431

JS 在执行的过程中会产生执行环境,这些执行环境会被程序的退出到执行栈中。如果遇到异步的代码,会被挂起并退出到 Task(有多种 task)队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,所以实质上来说 JS 中的异步还是同步行为

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

console.log('script end');

不同的工作源会被调配到不同的 Task 队列中,工作源能够分为 微工作(microtask)和 宏工作(macrotask)。在 ES6 标准中,microtask 称为 jobsmacrotask 称为 task

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

new Promise((resolve) => {console.log('Promise')
    resolve()}).then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码尽管 setTimeout 写在 Promise 之前,然而因为 Promise 属于微工作而 setTimeout 属于宏工作

微工作

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

宏工作

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O 网络申请实现、文件读写实现事件
  • UI rendering
  • 用户交互事件(比方鼠标点击、滚动页面、放大放大等)

宏工作中包含了 script,浏览器会先执行一个宏工作,接下来有异步代码的话就先执行微工作

所以正确的一次 Event loop 程序是这样的

  • 执行同步代码,这属于宏工作
  • 执行栈为空,查问是否有微工作须要执行
  • 执行所有微工作
  • 必要的话渲染 UI
  • 而后开始下一轮 Event loop,执行宏工作中的异步代码

通过上述的 Event loop 程序可知,如果宏工作中的异步代码有大量的计算并且须要操作 DOM 的话,为了更快的响应界面响应,咱们能够把操作 DOM 放入微工作中

  • JavaScript 引擎首先从宏工作队列(macrotask queue)中取出第一个工作
  • 执行结束后,再将微工作(microtask queue)中的所有工作取出,依照程序别离全副执行(这里包含不仅指开始执行时队列里的微工作),如果在这一步过程中产生新的微工作,也须要执行;
  • 而后再从宏工作队列中取下一个,执行结束后,再次将 microtask queue 中的全副取出,周而复始,直到两个 queue 中的工作都取完。

总结起来就是:一次 Eventloop 循环会解决一个宏工作和所有这次循环中产生的微工作

2. Node 中的 Event loop

当 Node.js 开始启动时,会初始化一个 Eventloop,解决输出的代码脚本,这些脚本会进行 API 异步调用,process.nextTick() 办法会开始处理事件循环。上面就是 Node.js 官网提供的 Eventloop 事件循环参考流程

  • Node 中的 Event loop 和浏览器中的不雷同。
  • NodeEvent loop 分为 6 个阶段,它们会依照程序重复运行
  • 每次执行执行一个宏工作后会清空微工作(执行程序和浏览器统一,在 node11 版本以上)
  • process.nextTick node 中的微工作,以后执行栈的底部,优先级比 promise 要高

整个流程分为六个阶段,当这六个阶段执行完一次之后,才能够算得上执行了一次 Eventloop 的循环过程。咱们来别离看下这六个阶段都做了哪些事件。

  • Timers 阶段 :这个阶段执行 setTimeoutsetInterval 的回调函数,简略了解就是由这两个函数启动的回调函数。
  • I/O callbacks 阶段:这个阶段次要执行零碎级别的回调函数,比方 TCP 连贯失败的回调。
  • idle,prepare 阶段:仅零碎外部应用,你只须要晓得有这 2 个阶段就能够。
  • poll 阶段 poll 阶段是一个重要且简单的阶段,简直所有 I/O 相干的回调,都在这个阶段执行(除了setTimeoutsetIntervalsetImmediate 以及一些因为 exception 意外敞开产生的回调)。 检索新的 I/O 事件,执行与 I/O 相干的回调,其余状况 Node.js 将在适当的时候在此阻塞。这也是最简单的一个阶段,所有的事件循环以及回调解决都在这个阶段执行。这个阶段的次要流程如下图所示。
  • check 阶段setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该局部,如下代码所示。
const fs = require('fs');
setTimeout(() => { // 新的事件循环的终点
    console.log('1'); 
}, 0);
setImmediate(() => {console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {if (err) throw err;
    console.log('read file success');
});
/// 该局部将会在首次事件循环中执行
Promise.resolve().then(()=>{console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在这一代码中有一个十分奇异的中央,就是 setImmediate 会在 setTimeout 之后输入。有以下几点起因:

  • setTimeout 如果不设置工夫或者设置工夫为 0,则会默认为 1ms
  • 主流程执行实现后,超过 1ms 时,会将 setTimeout 回调函数逻辑插入到待执行回调函数 poll 队列中;
  • 因为以后 poll 队列中存在可执行回调函数,因而须要先执行完,待齐全执行实现后,才会执行check:setImmediate

因而这也验证了这句话,先执行回调函数,再执行 setImmediate

  • close callbacks 阶段:执行一些敞开的回调函数,如 socket.on('close', ...)

除了把 Eventloop 的宏工作细分到不同阶段外。node 还引入了一个新的工作队列 Process.nextTick()

能够认为,Process.nextTick() 会在上述各个阶段完结时,在 进入下一个阶段之前立刻执行(优先级甚至超过 microtask 队列)

事件循环的次要蕴含微工作和宏工作。具体是怎么进行循环的呢

  • 微工作 :在 Node.js 中微工作蕴含 2 种——process.nextTickPromise 微工作在事件循环中优先级是最高的 ,因而在同一个事件循环中有其余工作存在时,优先执行微工作队列。并且process.nextTick 和 Promise 也存在优先级,process.nextTick 高于 Promise
  • 宏工作:在 Node.js 中宏工作蕴含 4 种——setTimeoutsetIntervalsetImmediateI/O。宏工作在微工作执行之后执行,因而在同一个事件循环周期内,如果既存在微工作队列又存在宏工作队列,那么优先将微工作队列清空,再执行宏工作队列

咱们能够看到有一个外围的主线程,它的执行阶段次要解决三个外围逻辑。

  • 同步代码。
  • 将异步工作插入到微工作队列或者宏工作队列中。
  • 执行微工作或者宏工作的回调函数。在主线程解决回调函数的同时,也须要判断是否插入微工作和宏工作。依据优先级,先判断微工作队列是否存在工作,存在则先执行微工作,不存在则判断在宏工作队列是否有工作,有则执行。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {if (err) throw err;
    console.log('read file success');
});
setTimeout(() => { // 新的事件循环的终点
    console.log('setTimeout'); 
}, 0);
/// 该局部将会在首次事件循环中执行
Promise.resolve().then(()=>{console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');

剖析下下面代码的执行过程

  • 第一个事件循环主线程发动,因而先执行同步代码,所以先输入 start,而后输入 end
  • 第一个事件循环主线程发动,因而先执行同步代码,所以先输入 start,而后输入 end;
  • 再从上往下剖析,遇到微工作,插入微工作队列,遇到宏工作,插入宏工作队列,剖析实现后,微工作队列蕴含:Promise.resolve 和 process.nextTick,宏工作队列蕴含:fs.readFile 和 setTimeout
  • 先执行微工作队列,然而依据优先级,先执行 process.nextTick 再执行 Promise.resolve,所以先输入 nextTick callback 再输入 Promise callback
  • 再执行宏工作队列,依据 宏工作插入先后顺序执行 setTimeout 再执行 fs.readFile,这里须要留神,先执行 setTimeout 因为其回调工夫较短,因而回调也先执行,并非是 setTimeout 先执行所以才先执行回调函数,然而它执行须要工夫必定大于 1ms,所以尽管 fs.readFile 先于setTimeout 执行,然而 setTimeout 执行更快,所以先输入 setTimeout,最初输入 read file success
// 输入后果
start
end
nextTick callback
Promise callback
setTimeout
read file success

当微工作和宏工作又产生新的微工作和宏工作时,又应该如何解决呢?如下代码所示:

const fs = require('fs');
setTimeout(() => { // 新的事件循环的终点
    console.log('1'); 
    fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {if (err) throw err;
        console.log('read file sync success');
    });
}, 0);
/// 回调将会在新的事件循环之前
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {if (err) throw err;
    console.log('read file success');
});
/// 该局部将会在首次事件循环中执行
Promise.resolve().then(()=>{console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在下面代码中,有 2 个宏工作和 1 个微工作,宏工作是 setTimeout 和 fs.readFile,微工作是 Promise.resolve

  • 整个过程优先执行主线程的第一个事件循环过程,所以先执行同步逻辑,先输入 2。
  • 接下来执行微工作,输入 poll callback
  • 再执行宏工作中的 fs.readFile 和 setTimeout,因为 fs.readFile 优先级高,先执行 fs.readFile。然而解决工夫长于 1ms,因而会先执行 setTimeout 的回调函数,输入 1。这个阶段在执行过程中又会产生新的宏工作 fs.readFile,因而又将该 fs.readFile 插入宏工作队列
  • 最初因为只剩下宏工作了 fs.readFile,因而执行该宏工作,并期待解决实现后的回调,输入 read file sync success
// 后果
2
poll callback
1
read file success
read file sync success

Process.nextick() 和 Vue 的 nextick

Node.js 和浏览器端宏工作队列的另一个很重要的不同点是,浏览器端工作队列每轮事件循环仅出队一个回调函数接着去执行微工作队列;而 Node.js 端只有轮到执行某个宏工作队列,则会执行完队列中所有的当前任务,然而以后轮次新增加到队尾的工作则会等到下一轮次才会执行。

setTimeout(() => {console.log('setTimeout');
}, 0);
setImmediate(() => {console.log('setImmediate');
})
// 这里可能会输入 setTimeout,setImmediate
// 可能也会相同的输入,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

下面介绍的都是 macrotask 的执行状况,microtask 会在以上每个阶段实现后立刻执行

setTimeout(()=>{console.log('timer1')

    Promise.resolve().then(function() {console.log('promise1')
    })
}, 0)

setTimeout(()=>{console.log('timer2')

    Promise.resolve().then(function() {console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印状况是不同的
// 浏览器中肯定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其余 microtask 执行

setTimeout(() => {console.log("timer1");

 Promise.resolve().then(function() {console.log("promise1");
 });
}, 0);

// poll 阶段执行
fs.readFile('./test',()=>{
  // 在 poll 阶段外面 如果有 setImmediate 优先执行,setTimeout 处于事件循环顶端 poll 上面就是 setImmediate
  setTimeout(()=>console.log('setTimeout'),0)
  setImmediate(()=>console.log('setImmediate'),0)
})

process.nextTick(() => {console.log("nextTick");
});
// nextTick, timer1, promise1,setImmediate,setTimeout

对于 microtask 来说,它会在以上每个阶段实现前清空 microtask 队列,下图中的 Tick 就代表了 microtask

谁来启动这个循环过程,循环条件是什么?

当 Node.js 启动后,会初始化事件循环,解决已提供的输出脚本,它可能会先调用一些异步的 API、调度定时器,或者 process.nextTick(),而后再开始处理事件循环。因而能够这样了解,Node.js 过程启动后,就发动了一个新的事件循环,也就是事件循环的终点。

总结来说,Node.js 事件循环的发动点有 4 个:

  • Node.js 启动后;
  • setTimeout 回调函数;
  • setInterval 回调函数;
  • 也可能是一次 I/O 后的回调函数。

有限循环有没有起点

当所有的微工作和宏工作都清空的时候,尽管以后没有工作可执行了,然而也并不能代表循环完结了。因为可能存在以后还未回调的异步 I/O,所以这个循环是没有起点的,只有过程在,并且有新的工作存在,就会去执行

Node.js 是单线程的还是多线程的?

主线程是单线程执行的 ,然而 Node.js 存在多线程执行 ,多线程包含 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其余的线程,包含 垃圾回收、内存优化

EventLoop 对渲染的影响

  • 想必你之前在业务开发中也遇到过 requestIdlecallback 和 requestAnimationFrame,这两个函数在咱们之前的内容中没有讲过,然而当你开始思考它们在 Eventloop 的生命周期的哪一步触发,或者这两个办法的回调会在微工作队列还是宏工作队列执行的时候,才发现如同没有设想中那么简略。这两个办法其实也并不属于 JS 的原生办法,而是浏览器宿主环境提供的办法,因为它们牵扯到另一个问题:渲染。
  • 咱们晓得浏览器作为一个简单的利用是多线程工作的,除了运行 JS 的线程外,还有渲染线程、定时器触发线程、HTTP 申请线程,等等。JS 线程能够读取并且批改 DOM,而渲染线程也须要读取 DOM,这是一个典型的多线程竞争临界资源的问题。所以浏览器就把这两个线程设计成互斥的,即同时只能有一个线程在执行
  • 渲染本来就不应该呈现在 Eventloop 相干的常识体系里,然而因为 Eventloop 显然是在探讨 JS 如何运行的问题,而渲染则是浏览器另外一个线程的工作。然而 requestAnimationFrame的呈现却把这两件事件给关联起来
  • 通过调用 requestAnimationFrame 咱们能够在下次渲染之前执行回调函数。那下次渲染具体是哪个工夫点呢?渲染和 Eventloop 有什么关系呢?

    • 简略来说,就是在每一次 Eventloop 的开端,判断以后页面是否处于渲染机会,就是从新渲染
  • 有屏幕的硬件限度,比方 60Hz 刷新率,简而言之就是 1 秒刷新了 60 次,16.6ms 刷新一次。这个时候浏览器的渲染间隔时间就没必要小于 16.6ms,因为就算渲染了屏幕上也看不到。当然浏览器也不能保障肯定会每 16.6ms 会渲染一次,因为还会受到处理器的性能、JavaScript 执行效率等其余因素影响。
  • 回到 requestAnimationFrame,这个 API 保障在下次浏览器渲染之前肯定会被调用,实际上咱们齐全能够把它看成是一个高级版的 setInterval。它们都是在一段时间后执行回调,然而前者的间隔时间是由浏览器本人一直调整的,而后者只能由用户指定。这样的个性也决定了 requestAnimationFrame 更适宜用来做针对每一帧来批改的动画成果
  • 当然 requestAnimationFrame 不是 Eventloop 里的宏工作,或者说它并不在 Eventloop 的生命周期里,只是浏览器又凋谢的一个在渲染之前产生的新的 hook。另外须要留神的是微工作的认知概念也须要更新,在执行 animation callback 时也有可能产生微工作(比方 promise 的 callback),会放到 animation queue 解决完后再执行。所以微工作并不是像之前说的那样在每一轮 Eventloop 后处理,而是在 JS 的函数调用栈清空后处理

然而 requestIdlecallback 却是一个更好了解的概念。当宏工作队列中没有工作能够解决时,浏览器可能存在“闲暇状态”。这段闲暇工夫能够被 requestIdlecallback 利用起来执行一些优先级不高、不用立刻执行的工作,如下图所示:

模块化

js 中当初比拟成熟的有四种模块加载计划:

  • 第一种是 CommonJS 计划,它通过 require 来引入模块,通过 module.exports 定义模块的输入接口。这种模块加载计划是服务器端的解决方案,它是以同步的形式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取十分快,所以以同步的形式加载没有问题。但如果是在浏览器端,因为模块的加载是应用网络申请,因而应用异步加载的形式更加适合。
  • 第二种是 AMD 计划,这种计划采纳异步加载的形式来加载模块,模块的加载不影响前面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载实现后再执行回调函数。require.js 实现了 AMD 标准
  • 第三种是 CMD 计划,这种计划和 AMD 计划都是为了解决异步模块加载的问题,sea.js 实现了 CMD 标准。它和 require.js 的区别在于模块定义时对依赖的解决不同和对依赖模块的执行机会的解决不同。
  • 第四种计划是 ES6 提出的计划,应用 import 和 export 的模式来导入导出模块

在有 Babel 的状况下,咱们能够间接应用 ES6的模块化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

CommonJsNode 独有的标准,浏览器中应用就须要用到 Browserify解析了。

// a.js
module.exports = {a: 1}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

在上述代码中,module.exportsexports 很容易混同,让咱们来看看大抵外部实现

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立刻执行函数,这样就不会净化全局变量了,// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {a: 1}
// 根本实现
var module = {exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法类似的起因
var exports = module.exports
var load = function (module) {
    // 导出的货色
    var a = 1
    module.exports = a
    return module.exports
};

再来说说 module.exportsexports,用法其实是类似的,然而不能对 exports 间接赋值,不会有任何成果。

对于 CommonJSES6 中的模块化的两者区别是:

  • 前者反对动静导入,也就是 require(${path}/xx.js),后者目前不反对,然而已有提案, 前者是同步导入,因为用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。
  • 而后者是异步导入,因为用于浏览器,须要下载文件,如果也采纳同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会扭转,所以如果想更新值,必须从新导入一次。
  • 然而后者采纳实时绑定的形式,导入导出的值都指向同一个内存地址,所以导入值会追随导出值变动
  • 后者会编译成 require/exports 来执行的

AMD

AMD 是由 RequireJS 提出的

AMD 和 CMD 标准的区别?

  • 第一个方面是在模块定义时对依赖的解决不同。AMD 推崇依赖前置,在定义模块的时候就要申明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
  • 第二个方面是对依赖模块的执行机会解决不同。首先 AMD 和 CMD 对于模块的加载形式都是异步加载,不过它们的区别在于模块的执行机会,AMD 在依赖模块加载实现后就间接执行依赖模块,依赖模块的执行程序和咱们书写的程序不肯定统一。而 CMD 在依赖模块加载实现后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行程序就和咱们书写的程序保持一致了。
// CMD
define(function(require, exports, module) {var a = require("./a");
  a.doSomething();
  // 此处略去 100 行
  var b = require("./b"); // 依赖能够就近书写
  b.doSomething();
  // ...
});

// AMD 默认举荐
define(["./a", "./b"], function(a, b) {
  // 依赖必须一开始就写好
  a.doSomething();
  // 此处略去 100 行
  b.doSomething();
  // ...
})
  • AMDrequirejs 在推广过程中对模块定义的规范化产出,提前执行,推崇依赖前置
  • CMDseajs 在推广过程中对模块定义的规范化产出,提早执行,推崇依赖就近
  • CommonJs:模块输入的是一个值的 copy,运行时加载,加载的是一个对象(module.exports 属性),该对象只有在脚本运行完才会生成
  • ES6 Module:模块输入的是一个值的援用,编译时输入接口,ES6模块不是对象,它对外接口只是一种动态定义,在代码动态解析阶段就会生成。

谈谈对模块化开发的了解

  • 我对模块的了解是,一个模块是实现一个特定性能的一组办法。在最开始的时候,js 只实现一些简略的性能,所以并没有模块的概念,但随着程序越来越简单,代码的模块化开发变得越来越重要。
  • 因为函数具备独立作用域的特点,最原始的写法是应用函数来作为模块,几个函数作为一个模块,然而这种形式容易造成全局变量的净化,并且模块间没有分割。
  • 前面提出了对象写法,通过将函数作为一个对象的办法来实现,这样解决了间接应用函数作为模块的一些毛病,然而这种方法会裸露所有的所有的模块成员,内部代码能够批改外部属性的值。
  • 当初最罕用的是立刻执行函数的写法,通过利用闭包来实现模块公有作用域的建设,同时不会对全局作用域造成净化。

参考 前端进阶面试题具体解答

变量晋升

当执行 JS 代码时,会生成执行环境,只有代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {console.log('call b')
}

想必以上的输入大家必定都曾经明确了,这是因为函数和变量晋升的起因。通常晋升的解释是说将申明的代码移动到了顶部,这其实没有什么谬误,便于大家了解。然而更精确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创立的阶段,JS 解释器会找出须要晋升的变量和函数,并且给他们提前在内存中开拓好空间,函数的话会将整个函数存入内存中,变量只申明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,咱们能够间接提前应用

  • 在晋升的过程中,雷同的函数会笼罩上一个函数,并且函数优先于变量晋升
b() // call b second

function b() {console.log('call b fist')
}
function b() {console.log('call b second')
}
var b = 'Hello world'

var 会产生很多谬误,所以在 ES6 中引入了 letlet不能在申明前应用,然而这并不是常说的 let 不会晋升,let晋升了,在第一阶段内存也曾经为他开拓好了空间,然而因为这个申明的个性导致了并不能在申明前应用

层叠上下文

元素晋升为一个比拟非凡的图层,在三维空间中 (z 轴) 高出一般元素一等。

触发条件

  • 根层叠上下文(html)
  • position
  • css3属性

    • flex
    • transform
    • opacity
    • filter
    • will-change
    • webkit-overflow-scrolling

层叠等级:层叠上下文在 z 轴上的排序

  • 在同一层叠上下文中,层叠等级才有意义
  • z-index的优先级最高

Ajax

它是一种异步通信的办法,通过间接由 js 脚本向服务器发动 http 通信,而后依据服务器返回的数据,更新网页的相应局部,而不必刷新整个页面的一种办法。

面试手写(原生):

//1:创立 Ajax 对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容 IE6 及以下版本
//2:配置 Ajax 申请地址
xhr.open('get','index.xml',true);
//3:发送申请
xhr.send(null); // 谨严写法
//4: 监听申请,承受响应
xhr.onreadysatechange=function(){if(xhr.readySate==4&&xhr.status==200 || xhr.status==304)
          console.log(xhr.responsetXML)
}

jQuery 写法

$.ajax({
  type:'post',
  url:'',
  async:ture,//async 异步  sync  同步
  data:data,// 针对 post 申请
  dataType:'jsonp',
  success:function (msg) { },
  error:function (error) {}})

promise 封装实现:

// promise 封装实现:function getJSON(url) {
  // 创立一个 promise 对象
  let promise = new Promise(function(resolve, reject) {let xhr = new XMLHttpRequest();

    // 新建一个 http 申请
    xhr.open("GET", url, true);

    // 设置状态的监听函数
    xhr.onreadystatechange = function() {if (this.readyState !== 4) return;

      // 当申请胜利或失败时,扭转 promise 的状态
      if (this.status === 200) {resolve(this.response);
      } else {reject(new Error(this.statusText));
      }
    };

    // 设置谬误监听函数
    xhr.onerror = function() {reject(new Error(this.statusText));
    };

    // 设置响应的数据类型
    xhr.responseType = "json";

    // 设置申请头信息
    xhr.setRequestHeader("Accept", "application/json");

    // 发送 http 申请
    xhr.send(null);
  });

  return promise;
}

Chrome 关上一个页面须要启动多少过程?别离有哪些过程?

关上 1 个页面至多须要 1 个网络过程、1 个浏览器过程、1 个 GPU 过程以及 1 个渲染过程,共 4 个;最新的 Chrome 浏览器包含:1 个浏览器(Browser)主过程、1 个 GPU 过程、1 个网络(NetWork)过程、多个渲染过程和多个插件过程。

  • 浏览器过程:次要负责界面显示、用户交互、子过程治理,同时提供存储等性能。
  • 渲染过程:外围工作是将 HTML、CSS 和 JavaScript 转换为用户能够与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该过程中,默认状况下,Chrome 会为每个 Tab 标签创立一个渲染过程。出于平安思考,渲染过程都是运行在沙箱模式下。
  • GPU 过程:其实,Chrome 刚开始公布的时候是没有 GPU 过程的。而 GPU 的应用初衷是为了实现 3D CSS 的成果,只是随后网页、Chrome 的 UI 界面都抉择采纳 GPU 来绘制,这使得 GPU 成为浏览器广泛的需要。最初,Chrome 在其多过程架构上也引入了 GPU 过程。
  • 网络过程:次要负责页面的网络资源加载,之前是作为一个模块运行在浏览器过程外面的,直至最近才独立进去,成为一个独自的过程。
  • 插件过程:次要是负责插件的运行,因插件易解体,所以须要通过插件过程来隔离,以保障插件过程解体不会对浏览器和页面造成影响。

迭代查问与递归查问

实际上,DNS 解析是一个蕴含迭代查问和递归查问的过程。

  • 递归查问 指的是查问申请收回后,域名服务器代为向下一级域名服务器发出请求,最初向用户返回查问的最终后果。应用递归 查问,用户只须要收回一次查问申请。
  • 迭代查问 指的是查问申请后,域名服务器返回单次查问的后果。下一级的查问由用户本人申请。应用迭代查问,用户须要收回 屡次的查问申请。

个别咱们向本地 DNS 服务器发送申请的形式就是递归查问,因为咱们只须要收回一次申请,而后本地 DNS 服务器返回给我 们最终的申请后果。而本地 DNS 服务器向其余域名服务器申请的过程是迭代查问的过程,因为每一次域名服务器只返回单次 查问的后果,下一级的查问由本地 DNS 服务器本人进行。

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

  • 正向代理:

客户端想取得一个服务器的数据,然而因为种种原因无奈间接获取。于是客户端设置了一个代理服务器,并且指定指标服务器,之后代理服务器向指标服务器转交申请并将取得的内容发送给客户端。这样实质上起到了对实在服务器暗藏实在客户端的目标。实现正向代理须要批改客户端,比方批改浏览器配置。

  • 反向代理:

服务器为了可能将工作负载分不到多个服务器来进步网站性能 (负载平衡)等目标,当其受到申请后,会首先依据转发规定来确定申请应该被转发到哪个服务器上,而后将申请转发到对应的实在服务器上。这样实质上起到了对客户端暗藏实在服务器的作用。
个别应用反向代理后,须要通过批改 DNS 让域名解析到代理服务器 IP,这时浏览器无奈察觉到真正服务器的存在,当然也就不须要批改配置了。

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

Iterator 迭代器

Iterator(迭代器)是一种接口,也能够说是一种标准。为各种不同的数据结构提供对立的拜访机制。任何数据结构只有部署 Iterator 接口,就能够实现遍历操作(即顺次解决该数据结构的所有成员)。

Iterator 语法:

const obj = {[Symbol.iterator]:function(){}
}

[Symbol.iterator] 属性名是固定的写法,只有领有了该属性的对象,就可能用迭代器的形式进行遍历。

  • 迭代器的遍历办法是首先取得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 办法,扭转指针的指向,让其指向下一条数据
  • 每一次的 next 都会返回一个对象,该对象有两个属性

    • value 代表想要获取的数据
    • done 布尔值,false 示意以后指针指向的数据有值,true 示意遍历曾经完结

Iterator 的作用有三个:

  • 创立一个指针对象,指向以后数据结构的起始地位。也就是说,遍历器对象实质上,就是一个指针对象。
  • 第一次调用指针对象的 next 办法,能够将指针指向数据结构的第一个成员。
  • 第二次调用指针对象的 next 办法,指针就指向数据结构的第二个成员。
  • 一直调用指针对象的 next 办法,直到它指向数据结构的完结地位。

每一次调用 next 办法,都会返回数据结构的以后成员的信息。具体来说,就是返回一个蕴含 value 和 done 两个属性的对象。其中,value 属性是以后成员的值,done 属性是一个布尔值,示意遍历是否完结。

let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next())  // {value: Object { num: 1}, done: false }
console.log(it.next())  // {value: 2, done: false}
console.log(it.next())  // {value: 3, done: false}
console.log(it.next())  // {value: undefined, done: true}

对象没有布局 Iterator 接口,无奈应用for of 遍历。上面使得对象具备 Iterator 接口

  • 一个数据结构只有有 Symbol.iterator 属性,就能够认为是“可遍历的”
  • 原型部署了 Iterator 接口的数据结构有三种,具体蕴含四种,别离是数组,相似数组的对象,Set 和 Map 构造

为什么对象(Object)没有部署 Iterator 接口呢?

  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,须要开发者手动指定。然而遍历遍历器是一种线性解决,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换
  • 对对象部署 Iterator 接口并不是很必要,因为 Map 补救了它的缺点,又正好有 Iteraotr 接口
let obj = {
    id: '123',
    name: '张三',
    age: 18,
    gender: '男',
    hobbie: '睡觉'
}

obj[Symbol.iterator] = function () {let keyArr = Object.keys(obj)
    let index = 0
    return {next() {
            return index < keyArr.length ? {
                value: {key: keyArr[index],
                    val: obj[keyArr[index++]]
                }
            } : {done: true}
        }
    }
}

for (let key of obj) {console.log(key)
}

CDN 的原理

CDN 和 DNS 有着密不可分的分割,先来看一下 DNS 的解析域名过程,在浏览器输出的解析过程如下:
(1)查看浏览器缓存
(2)查看操作系统缓存,常见的如 hosts 文件
(3)查看路由器缓存
(4)如果前几步都没没找到,会向 ISP(网络服务提供商) 的 LDNS 服务器查问
(5)如果 LDNS 服务器没找到,会向根域名服务器(Root Server) 申请解析,分为以下几步:

  • 根服务器返回顶级域名 (TLD) 服务器如 .com.cn.org 等的地址,该例子中会返回 .com 的地址
  • 接着向顶级域名服务器发送申请,而后会返回次级域名 (SLD) 服务器的地址,本例子会返回 .test 的地址
  • 接着向次级域名服务器发送申请,而后会返回通过域名查问到的指标 IP,本例子会返回 www.test.com 的地址
  • Local DNS Server 会缓存后果,并返回给用户,缓存在零碎中

CDN 的工作原理:(1)用户未应用 CDN 缓存资源的过程:

  1. 浏览器通过 DNS 对域名进行解析(就是下面的 DNS 解析过程),顺次失去此域名对应的 IP 地址
  2. 浏览器依据失去的 IP 地址,向域名的服务主机发送数据申请
  3. 服务器向浏览器返回响应数据

(2)用户应用 CDN 缓存资源的过程:

  1. 对于点击的数据的 URL,通过本地 DNS 零碎的解析,发现该 URL 对应的是一个 CDN 专用的 DNS 服务器,DNS 零碎就会将域名解析权交给 CNAME 指向的 CDN 专用的 DNS 服务器。
  2. CND 专用 DNS 服务器将 CND 的全局负载平衡设施 IP 地址返回给用户
  3. 用户向 CDN 的全局负载平衡设施发动数据申请
  4. CDN 的全局负载平衡设施依据用户的 IP 地址,以及用户申请的内容 URL,抉择一台用户所属区域的区域负载平衡设施,通知用户向这台设施发动申请
  5. 区域负载平衡设施抉择一台适合的缓存服务器来提供服务,将该缓存服务器的 IP 地址返回给全局负载平衡设施
  6. 全局负载平衡设施把服务器的 IP 地址返回给用户
  7. 用户向该缓存服务器发动申请,缓存服务器响应用户的申请,将用户所需内容发送至用户终端。

如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器申请内容,以此类推,直到获取到须要的资源。最初如果还是没有,就会回到本人的服务器去获取资源。

CNAME(意为:别名):在域名解析中,实际上解析进去的指定域名对应的 IP 地址,或者该域名的一个 CNAME,而后再依据这个 CNAME 来查找对应的 IP 地址。

This

不同状况的调用,this指向别离如何。顺带能够提一下 es6 中箭头函数没有 this, arguments, super 等,这些只依赖蕴含箭头函数最靠近的函数

咱们先来看几个函数调用的场景

function foo() {console.log(this.a)
}
var a = 1
foo()

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()
  • 对于间接调用 foo 来说,不论 foo 函数被放在了什么中央,this 肯定是window
  • 对于 obj.foo() 来说,咱们只须要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 对于 new 的形式来说,this 被永远绑定在了 c 下面,不会被任何形式扭转 this

说完了以上几种状况,其实很多代码中的 this 应该就没什么问题了,上面让咱们看看箭头函数中的 this

function a() {return () => {return () => {console.log(this)
    }
  }
}
console.log(a()()())
  • 首先箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个一般函数的 this。在这个例子中,因为包裹箭头函数的第一个一般函数是 a,所以此时的 thiswindow。另外对箭头函数应用 bind这类函数是有效的。
  • 最初种状况也就是 bind 这些扭转上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window
  • 那么说到 bind,不晓得大家是否思考过,如果对一个函数进行屡次 bind,那么上下文会是什么呢?
let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你认为输入后果是 a,那么你就错了,其实咱们能够把上述代码转换成另一种模式

// fn.bind().bind(a) 等于
let fn2 = function fn1() {return function() {return fn.apply()
  }.apply(a)
}
fn2()

能够从上述代码中发现,不论咱们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以后果永远是 window

let a = {name: 'poetries'}
function foo() {console.log(this.name)
}
foo.bind(a)() // => 'poetries'

以上就是 this 的规定了,然而可能会产生多个规定同时呈现的状况,这时候不同的规定之间会依据优先级最高的来决定 this 最终指向哪里。

首先,new 的形式优先级最高,接下来是 bind 这些函数,而后是 obj.foo() 这种调用形式,最初是 foo 这种调用形式,同时,箭头函数的 this 一旦被绑定,就不会再被任何形式所扭转。

函数执行扭转 this

  • 因为 JS 的设计原理: 在函数中,能够援用运行环境中的变量。因而就须要一个机制来让咱们能够在函数体外部获取以后的运行环境,这便是this

因而要明确 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如

  • obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
  • fn(),这里能够看成 window.fn(),因而 this === window

但这种机制并不齐全能满足咱们的业务需要,因而提供了三种形式能够手动批改 this 的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

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

MDN 中对 documentFragment 的解释:

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

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

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

(1)回流

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

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

  • 页面的首次渲染
  • 浏览器的窗口大小发生变化
  • 元素的内容发生变化
  • 元素的尺寸或者地位发生变化
  • 元素的字体大小发生变化
  • 激活 CSS 伪类
  • 查问某些属性或者调用某些办法
  • 增加或者删除可见的 DOM 元素

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

  • 全局范畴:从根节点开始,对整个渲染树进行从新布局
  • 部分范畴:对渲染树的某局部或者一个渲染对象进行从新布局

(2)重绘

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

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

  • color、background 相干属性:background-color、background-image 等
  • outline 相干属性:outline-color、outline-width、text-decoration
  • border-radius、visibility、box-shadow

留神:当触发回流时,肯定会触发重绘,然而重绘不肯定会引发回流。

什么是回调函数?回调函数有什么毛病?如何解决回调天堂问题?

以下代码就是一个回调函数的例子:

ajax(url, () => {// 解决逻辑})

回调函数有一个致命的弱点,就是容易写出回调天堂(Callback hell)。假如多个申请存在依赖性,可能会有如下代码:

ajax(url, () => {
    // 解决逻辑
    ajax(url1, () => {
        // 解决逻辑
        ajax(url2, () => {// 解决逻辑})
    })
})

以上代码看起来不利于浏览和保护,当然,也能够把函数离开来写:

function firstAjax() {ajax(url1, () => {
    // 解决逻辑
    secondAjax()})
}
function secondAjax() {ajax(url2, () => {// 解决逻辑})
}
ajax(url, () => {
  // 解决逻辑
  firstAjax()})

以上的代码尽管看上去利于浏览了,然而还是没有解决基本问题。回调天堂的基本问题就是:

  1. 嵌套函数存在耦合性,一旦有所改变,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个毛病,比方不能应用 try catch 捕捉谬误,不能间接 return

懒加载的实现原理

图片的加载是由 src 引起的,当对 src 赋值时,浏览器就会申请图片资源。依据这个原理,咱们应用 HTML5 的 data-xxx 属性来贮存图片的门路,在须要加载图片的时候,将 data-xxx 中图片的门路赋值给src,这样就实现了图片的按需加载,即懒加载。

留神:data-xxx 中的 xxx 能够自定义,这里咱们应用 data-src 来定义。

懒加载的实现重点在于确定用户须要加载哪张图片,在浏览器中,可视区域内的资源就是用户须要的资源。所以当图片呈现在可视区域时,获取图片的实在地址并赋值给图片即可。

应用原生 JavaScript 实现懒加载:

知识点:

(1)window.innerHeight 是浏览器可视区的高度

(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的间隔

(3)imgs.offsetTop 是元素顶部间隔文档顶部的高度(包含滚动条的间隔)

(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;

图示: 代码实现:

<div class="container">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img');
function lozyLoad(){
        var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
        var winHeight= window.innerHeight;
        for(var i=0;i < imgs.length;i++){if(imgs[i].offsetTop < scrollTop + winHeight ){imgs[i].src = imgs[i].getAttribute('data-src');
            }
        }
    }
  window.onscroll = lozyLoad();
</script>

display 的属性值及其作用

属性值 作用
none 元素不显示,并且会从文档流中移除。
block 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。
inline 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
inline-block 默认宽度为内容宽度,能够设置宽高,同行显示。
list-item 像块类型元素一样显示,并增加款式列表标记。
table 此元素会作为块级表格来显示。
inherit 规定应该从父元素继承 display 属性的值。

正文完
 0