1、说下浏览器缓存
缓存是用来进行性能优化的常用办法,能够有效减少延迟与网络阻塞。
缓存的优点:
- 减少不必要的数据传输,节省带宽
- 减少服务器负担,提示网站性能
- 加快客户端加载网页的速度,用户体验友好
缓存的缺点:
- 服务器资源更新后,客户端更新滞后
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置HTTP Header来实现的。
强缓存
不会向服务器发送请求,直接从缓存中读取资源,请求返回状态码为200。可以通过设置两种HTTP Header实现:Expires和Cache-Control。
Expires:Tue, Jun 30 2020 09:39:28 GMT+0800
表示资源会在Tue, Jun 30 2020 09:39:28 GMT+0800后过期,需要再次请求。并且Expires受限于本地时间,可能会造成缓存失效。
Cache-control:max-age=30
Cache-control的优先级高于Expires。该属性表示资源会在30秒后过期,需要再次请求。
协商缓存
会先向服务器发送请求,服务器会根据这个请求的Request Headers的一些参数来判断是否命中缓存,如果命中,则返回304状态码,并带上新的Request Headers通知浏览器从缓存中读取资源。可以通过设置两种HTTP Header实现:Last-Modified和ETag。
ETag即文件hash,每个文件都是唯一的,优先级要高于Last-Modified。Last-Modified即文件的修改时间,精确到秒。
Last-Modified存在以下弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源。
- 因为
Last-Modified只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源。
Response Headers 中的 etag、last-modified 在客户端重新向服务端发起请求时,会在 Request Headers 中换个key名:if-none-matched 和 if-modified-since
// Response Headers
etag: 65597c1615681857158408944e
last-modified: Tue, Jun 30 2020 09:39:28 GMT+0800
// Request Headers 变为
if-none-matched: 65597c1615681857158408944e
if-modified-since: Tue, Jun 30 2020 09:39:28 GMT+0800
2、JS中处理异步的方法有哪些
JS是单线程的,一次只能执行一个任务,有多个任务时需要排队依次执行。这种模式的好处就是实现起来比较简单,执行环境比较简单。坏处就是容易出现阻塞,必须执行完前一个任务才能执行下一个任务。
为了解决该问题,JS中分为同步任务和异步任务。同步任务就是必须执行完当前任务才能执行下一个任务,异步任务是在执行当前任务的同时可以执行下个任务,不阻塞下个任务的执行的
处理异步主要有以下几种方式:
- 回调函数
将A函数作为参数传递给B函数,等待B函数执行完成后再执行传递进来的A函数,此时,A函数就被称为回调函数。
fnB(fnA){
//执行B函数
...
//B函数执行完后执行A函数
fnA()
}
回调函数的好处就是简单易于理解,但是采用回调函数耦合比较严重,回调函数比较多的时候就会出现回调地狱
- Promise
Promise是异步编程的一种解决方案,可以有效的解决回调地狱的问题。Promise有三种状态:penging(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。它有以下几种方法
resolve 返回异步操作成功的结果
reject 返回异步操作失败的结果
then 执行Promise状态是成功的操作
catch 执行Promise状态是失败的操作
finally 不管Promise状态是成功或失败都执行的操作
示例如下:
//ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
const p = new Promise(function(resolve,reject){
if(success){
resolve('成功的结果')
}else{
reject('失败的结果')
}
})
p.then(function (res) {
// 接收resolve传来的数据
},function (err) {
// 接收reject传来的数据
})
p.catch(function (err) {
// 接收reject传来的数据或者捕捉到then()中的运行报错时
})
p.finally(function(){
// 不管什么状态都执行
})
Generator
Generator 最大的特点就是可以控制函数的执行。执行Generator函数会返回一个遍历器对象,每一次Generator函数里面的yield都相当执行一次遍历器对象的next()方法,并且可以通过next(value)方法传入自定义的value,来改变Generator函数的行为。
function* GeneratorFn() {
yield 'hello';
yield 'Generator';
return 'over';
}
let fn = GeneratorFn();
fn.next()//{value:"hello",done:false} done:false代表函数内的状态还没有执行结束
fn.next()//{value:"Generator",done:false}
fn.next()//{value:"over",done:true} done:true代表函数内的状态执行完毕
fn.next()//{value:undfined,done:true}
async函数
一个函数如果加上 async ,那么该函数就会返回一个 Promise,async 就是将函数返回值使用Promise.resolve()包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用。
async function test() {
let value = await sleep()
}
3、promise.all其中一个任务失败,怎么把其它正确执行的任务的数据返回?
Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。
它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。
若想其中一个任务失败时仍返回其它正确的数据,则可以在任务失败的时候直接捕获错误
var p1 = new Promise((resolve, reject) => {
resolve('p1');
});
var p2 = new Promise((resolve, reject) => {
resolve('p2');
});
var p3 = new Promise((resolve, reject) => {
reject('p3');
});
Promise.all([p1, p2, p3].map(p => p.catch(e => '出错后返回的值' )))
.then(values => {
console.log(values); //p1
}).catch(err => {
console.log(err);
})
4、vue怎么强制刷新某个子组件?
v-if
v-if可以控制一个DOM节点的存在与否,值改变时会重新挂载或卸载组件。
key
给组件绑定一个key值,key值改变时,组件会重新渲染。
this.$forceUpdate
强制重新渲染组件。
5、介绍一下vue的双向绑定机制
vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通javascript对象传给vue实例来作为它的data选项时,vue将遍历它的属性,用Object.defineProperty将它们转为getter/setter。用户看不到getter/setter,但是在内部它们让vue追踪依赖,在属性被访问和修改时通知变化。
vue的数据双向绑定将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observe来监听自己的Model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析{{}}),最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新,视图交互变化(input) ->数据Model变更双向绑定结果。
6、vue父子组件的生命周期的执行顺序是什么,兄弟组件的生命周期的执行顺序是什么?
<template>
<div class="father">
<component-A class="son_A"></component-A>
<component-B class="son_B"></component-B>
</div>
</template>
父组件beforeCreate -> 父组件created -> 父组件beforeMount -> 子组件AbeforeCreate -> 子组件Acreated -> 子组件AbeforeMount-> 子组件BbeforeCreate -> 子组件Bcreated -> 子组件BbeforeMount-> 子组件Amounted-> 子组件Bmounted -> 父组件mounted-> 父组件beforeDestroy-> 子组件AbeforeDestroy-> 子组件Adestroyed-> 子组件BbeforeDestroy-> 子组件Bdestroyed -> 父组件destroyed
子组件更新过程(props,store等变量,若仅仅是子元素变量更新那就是子beforeUpdate -> 子updated)
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
父组件更新过程
父beforeUpdat -> 父updated
7、介绍下vue的渲染函数
vue渲染的主要流程如下
new Vue,执行初始化- 挂载
$mount方法,通过自定义render方法、template、el等生成render函数 - 通过
Watcher监听数据的变化,当数据发生变化时,render函数执行生成VNode对象 - 通过
patch方法,对比新旧VNode对象,通过DOM Diff算法,添加、修改、删除真正的DOM元素
渲染过程中有三种渲染模式
自定义rende函数
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement (
'h' + this.level, // tag name标签名称
this.$slots.default // 子组件中的阵列
)
},
props: {
level: {
type: Number,
required: true
}
}
})
template写法
let app = new Vue({
template: `<div>{{ msg }}</div>`,
data () {
return {
msg: ''
}
}
})
el写法
let app = new Vue({
el: '#app',
data () {
return {
msg: 'Hello Vue!'
}
}
})
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
render函数是通过createElement建立一个虚拟 DOM,createElement接受以下三个参数:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// ....
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
8、vue组件通信的方法有哪些
- 父组件与子组件传值
父组件传给子组件:子组件通过props方法接受数据;子组件传给父组件:$emit方法传递参数
- 非父子组件间的数据传递,兄弟组件传值
eventBus(有被问到过eventBus的原理),就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
- 跨层级组件通信
$attrs和$listeners,eventBus,provider和inject
通用的 vuex
9、让你自己去实现一个双向绑定要怎么做?
首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发生变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
10、首页出现白屏的原因,怎么解决?
vue首页加载过慢,其原因是因为它是一个单页应用,需要将所有需要的资源都下载到浏览器并解析
解决办法:
- 使用首屏
SSR+跳转SPA方式来优化 - 改单页面应用为多页应用,需要修改
webpack的entry - 改成多页以后应该使用
prefetch的就使用 - 处理加载的时间片,合理安排加载顺序,尽量不要有大面积空隙
CDN资源还是很重要的,最好分开,也能减少一些不必要的资源损耗- 使用
quicklink,在网速好的时候,可以帮助你预加载页面资源
quicklink:通过空闲时间预加载视口中的links资源来让之后的页面加载更快
工作流程:
(1)、检测视口中的连接
(2)、等待浏览器空闲
(3)、判断用户不是使用慢网(2g)或者启用了数据缓存
(4)、预加载对应的url(使用<link rel=prefetch>或者XHR),优先级高的时候可以采用fetch - 骨架屏这种优化用户体验的东西一定要上,最好借助stream先将这部分输出给浏览器解析
- 合理使用
web worker优化一些计算 - 缓存一定要使用,但是请注意合理使用
11、什么工具可以用来判断页面中哪个文件加载慢?
- 通过浏览器开发者工具或浏览器插件、
Fiddler、Charles等查看页面加载情况。原理是通过追踪HTTP请求与响应的时间,以图形的方式列出所有资源的下载情况。缺点是人为操作,难以实现批量测试与统计。 - 在页面中引入额外的代码钩子来记录时间等相关数据。缺点是加重了开发者与测试人员的负担,还有可能因为检测代码本身的潜在问题影响页面的性能。如果好一点的话,会接入一个性能数据收集系统,采取并分析数据。
- 使用第三方的工具如
Page Speed、YSlow和WebPagetest,能够选择在不同浏览器和不同地域进行测试,并且给出各方面的评分以及提供一些优化建议。但某些服务需要排队等待,并且难以实现批量测试与统计。下面是使用WebPagetest测试京东首页的情况: W3C推出了一套性能API标准,目的是简化开发者对网站性能进行精确分析与控制的过程,最终实现性能的提高。比如通过Navigation Timing记录的关键时间点来统计页面完成所用的时间,部分使用方法:
var timing = window.performance.timing
timing.domLoading //浏览器开始解析 HTML 文档第一批收到的字节
timing.domInteractive // 浏览器完成解析并且所有 HTML 和 DOM 构建完毕
timing.domContentLoadedEventStart //DOM 解析完成后,网页内资源加载开始的时间
timing.domContentLoadedEventEnd // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
timing.domComplete //网页上所有资源(图片等)下载完成,且准备就绪的时间
12、谈谈你对nextTick的理解?
用来在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用该方法,获取更新后的DOM。
vue在修改数据后,视图不会立即更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
事件循环:
第一个tick(图例中第一个步骤,即“本次更新循环”)
- 首先修改数据,这是同步任务。同一事件循环的所有同步任务都在主线程上执行,形成一个执行栈,此时还未涉及
DOMvue开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。
第二个tick(图例中第二个步骤,即“下次更新循环”)
- 同步任务执行完毕,开始执行异
步watcher队列的任务,更新DOM。vue在内部尝试对异步队列使用原生的Promise.then和messageChannel方法,如果执行环境不支持,会采用setTimeout(fn,0)代替
第三个tick(图例中第三个步骤)
- 此时就是下次
dom更新循环结束之后,此时通过vue.nextTick获取改变后的DOM,通过setTimeout(fn,0)也可以同样获取到
13、页面渲染过程中哪些会阻塞解析?
浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制
CSSOM会阻塞渲染,只有当CSSOM构建完毕之后才会进入下一个阶段构建渲染树。
通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时恰巧浏览器尚未完成CSSOM的下载和构建,由于Javascript可以修改CSSOM,所以需要等CSSOM构建完毕后在执行JS,最后才重新进行DOM构建。
14、js加载会阻塞渲染,怎么解决?
给script标签添加async或defer属性
async:async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
defer: defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
具体可参考文章:你不知道的浏览器页面渲染机制
15、用async或defer的时候,什么生命周期钩子一定能够获取变量的值?
在onload的时候一定可以获取到变量的值,解释见上题。
16、window.onload 和 DOMContentLoaded 的区别
load:load 应该仅用于检测一个完全加载的页面 当一个资源及其依赖资源已完成加载时,将触发load事件。
DOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。
具体可参考文章:再谈 load 与 DOMContentLoaded
17、如何得知一个函数的执行时间?
- 函数执行前后分别执行
new Date().getTime(),计算时间差,假如是异步函数的话貌似就没法计算了。 console.time方法用于标记开始时间,console.timeEnd方法用于标记结束时间。- 使用
performance.now(),比使用new Date()得到的时间更准确。
18、同一个函数每次执行时间不一样,原因有哪些?
- 与操作系统的调度有关
- 现在的CPU支持动态调频
19、怎么算浏览器的localstroage的大小?
localStorage 中存储的是字符串,根据这一条件,我们可以不停的往localStorage塞值直到堆栈爆出,然后所有的localStorage的内容,而其长度就是大小。
(function() {
if(!window.localStorage) {
console.log('当前浏览器不支持localStorage!')
}
var test = '0123456789';
var add = function(num) {
num += num;
if(num.length == 10240) {
test = num;
return;
}
add(num);
}
add(test);
var sum = test;
var show = setInterval(function(){
sum += test;
try {
window.localStorage.removeItem('test');
window.localStorage.setItem('test', sum);
console.log(sum.length / 1024 + 'KB');
} catch(e) {
console.log(sum.length / 1024 + 'KB超出最大限制');
clearInterval(show);
}
}, 0.1)
})()
具体可参考文章:如何获得浏览器localStorage的剩余容量
20、筛选1-100之间的质数
质数:只能被1和自身整除的数
法一:
var countPrimes = function (n) {
let count = 0
for(let i = 2; i < n; i++){
if(isPrim(i)) count++
}
return count
};
function isPrim(n){
//遍历比比该数小并且大于1的数,如果能整除该数则为非质数
for(let i = 2; i < n; i++){
if(n % i === 0){
return false
}
}
return true
}
countPrimes(100)
法二:
// 每一个数的倍数肯定不是质数
var countPrimes = function(n){
let isPrim = new Array(n).fill(true)
for(let i = 2; i * i < n; i++){
if(isPrim[i]){
// i*i前面的数已经被之前的循环排除了
for(let j = i * i; j < n; j+= i){
isPrim[j] = false
}
}
}
let count = 0
for(let i = 2; i < n; i++){
if(isPrim[i]) count++
}
return count
}
countPrimes(100)
21、浏览器安全方面有了解吗?
XSS(Cross Site Script)跨站脚本攻击
指的是攻击者向网页注入恶意的客户端代码,通过恶意的脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
主要分为三种:
- 存储型:即攻击被存储在服务端,常见的有评论区插入攻击脚本,如果脚本被存储在服务端,那么看到对应评论的用户都会受到攻击。
- 反射型:攻击者将脚本混在
url里,服务端接收到url将恶意代码当作参数取出并拼接到html中返回,浏览器解析此html后即执行恶意代码。 DOM型:将攻击脚本写在url中,诱导用户点击该链接,如果该url被解析,那么攻击脚本就会被运行。DOM型与反射型攻击的区别就在于DOM型不经过服务器。
如何防御XSS攻击:
- 输入检查:对输入内容中的
script和iframe等标签进行转义或过滤。 - 设置
httpOnly:很多XSS攻击目标都是窃取用户cookie伪造用户身份认证,设置此属性可以防止js获取cookie。 - 开启
CSP,即开启白名单,可阻止白名单外的资源加载和运行。
CSRF攻击(Cross-site request forgery)跨站请求伪造
是一种劫持受信任用户向服务器发送非预期请求的攻击方式,通常情况下,它是攻击者借助受害者的cookie骗取服务器的信任,但是它不能拿到cookie,也看不到cookie的内容,它能做的就是给服务器发送请求,然后执行请求中描述的命令,以此来改变服务器中的数据,也就是不能窃取服务器中的数据。
防御主要有三种:
- 验证
token,浏览器请求服务器时,服务器返回一个token,每个请求需要同时带上token和cookie才会被认为是合法请求。 - 验证
referer:通过验证请求头中的referer来验证来源站点,但是请求头很容易仿造。 - 设置
sameSite:设置cookie的sameSite,可以让cookie不随跨站请求发出,但浏览器兼容不一。
点击劫持
诱使用户点击看似无害的按钮(实则点击了透明iframe中的按钮)
监听鼠标移动事件,让危险按钮始终在鼠标下面
使用html5拖拽技术执行敏感操作(例如deploy key)。
预防策略:
- 服务端添加
X-Frame-Options响应头,这个HTTP响应头是为了防御用iframe嵌套的点击劫持攻击。这样浏览器就会阻止嵌入网页的渲染 JS判断顶层视口的域名是不是和本页面的域名一致,不一致则不允许操作。top.location.hostname === self.location.hostname- 敏感操作使用更复杂的步骤(验证码)
中间人攻击
中间人(Man-in-the-middle attack,MITM)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都会攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
一般的过程如下:
- 客户端发送请求到服务器,请求被中间人截获
- 服务器向客户端发送公钥
- 中间人截获公钥,保留在自己手上,然后自己生成一个伪造的公钥,发给客户端
- 客户端收到伪造的公钥后,生成加密
hash值发给服务器 - 中间人获得加密
hash值,用自己的私钥解密获得真密钥,同时生成假的hash值发给服务器 - 服务器用私钥解密获得假密钥,然后加密数据传输给客户端
当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。
22、说一下vue生命周期
vue实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载dom->渲染,更新->渲染,销毁等一系列过程,称之为vue的生命周期。
生命周期中有多个事件钩子,让我们在控制整个vue实例的过程是更容易形成好的逻辑。
beforeCreate(创建前) 在数据观测和初始化事件还未开始。created(创建后)完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来。beforeMount(载入前)在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。mounted(载入后)在el被新创建的vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程进行ajax交互。beforeUpdate(更新前)在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步更改状态,不会触发附加的重渲染过程。updated(更新后)在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM得操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务端渲染期间不被调用。beforeDestory(销毁前)在实例销毁前调用。实例仍然完全可用。destoryed(销毁后)在实例销毁之后调用。调用后,所有的时间监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
23、在浏览器中输入url发生了什么?
DNS解析,通过域名查询到具体的ip,发生在TCP握手之前,例如当你在浏览器中想访问www.goole.com时,操作系统会首先在浏览器缓存中找,没有的话就去系统缓存中查询ip,没有的话就会系统配置的DNS服务器中查询,还没有的话就直接去DNS根服务器查询,这一步会先找出com这个一级域名的服务器,然后去该服务器查询goole这个二级域名,接下来三级域名的查询其实是我们自己配置的,你可以给www这个域名配置一个ip,然后还可以给别的三级域名配置一个ip。TCP握手,应用层会下发数据给传输层,这里TCP协议会指明两端的端口号,然后下发给网络层。网络层中的ip协议会确定ip地址,并且指示了数据如何跳转路由器。然后包会再次被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了。TLS握手,TLS握手后就开始正式的传输数据了- 数据在进入服务器之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这是假设服务端会响应一个
html文件。 - 首先浏览器会先判断状态码是什么,如果是200就继续解析,如果是400或500就报错,如果是300就会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。
- 浏览器开始解析文件,如果是
gzip格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。 - 文件解码成功后会正式开始渲染流程,先根据
HTML构建DOM树,有CSS的话会去构建CSSOM树,如果遇到script标签的话,会判断是否存在async或者defer,前者会并行下载并执行js,后者会先下载,然后等到html文件解析完成后顺序执行。如果以上都没有,就会阻塞渲染流程直到JS执行完毕。遇到文件下载的会去下载文件,这里如果使用HTTP/2协议的话会极大的提高多图的下载效率。 CSSOM树和DOM树构建完成后会开始生成Render树,这一步就是确定页面元素的布局,样式等等诸多方面的东西。生成render树的过程中,浏览器就开始调用GPU绘制,合成图层,将内容显示到屏幕上了。
24、介绍以下TCP三次握手
- 第一次握手:客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。
- 第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号。
- 第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。服务端接收到这个应答之后就代表连接成功。
25、项目路由权限控制怎么实现?纯前端实现还是后台取值?
这个就要根据自己的项目来回答了~
26、vuex,localstroge,sessionstroge的区别
vuex存储在内存中(页面刷新内存则清除),localstroage(本地存储)以文件的形式存储在本地(不主动删除会一直存在),sessionstroage(会话存储)是临时保存的(页面关闭则清除)。localstroage和sessionstroage只能存储字符串类型,对于复杂的对象可以使用JSON.stringify和parse来处理。- 应用场景不同,
vuex用于组件之间的传值,localstroage和sessionstroage则主要是用于不同页面的传值。 vuex的数据改变页面是自动更新的,localstroage和sessionstroage则不会,需要手动更新。
27、平时有对webpack做过哪些优化?
开启多进程打包,减少打包时间;合理利用缓存;开启多进程压缩,减少压缩时间;缩小文件搜索范围;减小没必要的编译工作;忽略对部分没采用模块化的文件的递归解析处理;按需加载,减小打包体积等等。
28、介绍下vuex
只用来读取的状态集中放在store中,改变状态的方式是提交mutataions,这是个同步的事物;异步逻辑应该封装在action中。
state:vuex使用单一状态树,即每个应用将仅仅包含一个store实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。mutations:mutations定义的方法动态修改vuex的store中的状态或数据getters:类似vue的计算属性,主要用来过滤一些数据。action:action可以理解为通过mutations里面处理数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据,view层通过store.dispatch来分发action。
29、你做过最有挑战性的项目,实现了什么优化
尽可能把自己做的事情介绍的比较复杂。。。。
30、说下你最近的项目中遇见的兼容性问题
这个压根不知道咋回答啊,平时遇到的都是一些比较小的点,然后百度解决了后就忘记了,大家有没有什么系统的回答请指教请指教。
31、介绍下vue路由的实现方法
主要有两种模式:hash模式和history模式
hash模式:在浏览器中符号#,#以及#后面的字符称之为hash,用window.location.hash读取;hash虽然在URL中,但不被包括在HTTP请求中,用来指导浏览器动作,对服务器端完全无用,hash不会重载页面。hash模式下,仅hash符号之前的内容会被包含在请求中,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误。history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。history模式下,前端的URL必须和实际向后端发起请求的`URL一致。
32、说一下vue的导航守卫
vue提供的导航守卫主要是用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的,单个路由独享的,或者组件级别的。
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。
- 全局前置守卫:
beforeEach - 全局解析守卫:
beforeResolve - 全局后置钩子:
afterEach - 路由独享守卫:
beforeEnter - 组件内的守卫:
beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
完整解析流程
- 导航被触发
- 在失活的组件里调用离开守卫
- 调用全局的
beforeEach守卫 - 在重用的组件里调用
beforeRouteUpdate守卫 - 在路由配置里调用
beforeEnter - 解析异步组件
- 在激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve - 导航被确认
- 调用全局的
afterEach钩子 - 触发
DOM更新 - 用创建好的实例调用
beforeRouteEnter守卫中传给next的回调函数
33、vue计算属性和watch有什么区别
computed是计算一个新的属性,并将该属性挂载到vm(vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有data,props)。computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问computed属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数 从使用场景上说,computed适用一个数据被多个数据影响,而watch适用于一个数据影响多个数据。
34、垂直居中布局有哪几种实现方式?
<div class="box">
<div class="box-center">
我是子元素
</div>
</div>
第一种方式:
.box {
display: flex;
width: 200px;
height: 200px;
background-color: red;
}
.box-center{
margin: auto;
}
第二种方式:
.box {
display: flex;
align-items: center;
justify-content: center;
width: 200px;
height: 200px;
background-color: red;
}
第三种方式:
.box {
position: relative;
width: 200px;
height: 200px;
background-color: red;
}
.box-center{
position: absolute;
top:50%;
left: 50%;
transform: translate(-50%,-50%);
/* 定宽的情况下可以用margin */
}
35、同时给一个标签设置两个class,取哪一个样式?id选择器和class选择器哪一个优先级高?各个选择器之间的优先级顺序是咋样的?
样式表中后定义的class的优先级高,id选择器的优先级要高于class选择器的优先级。
CSS 7 种基础的选择器:
ID选择器, 如#id{}- 类选择器, 如
.class{} - 属性选择器, 如
a[href="segmentfault.com"]{} - 伪类选择器, 如
:hover{} - 伪元素选择器, 如
::before{} - 标签选择器, 如
span{} - 通配选择器, 如
*{}</br>
CSS 优先规则1: 最近的祖先样式比其他祖先样式优先级高。(如果我们把一个标签从祖先那里继承来的而自身没有的属性叫做"祖先样式",那么"直接样式"就是一个标签直接拥有的属性。)
CSS 优先规则2:"直接样式"比"祖先样式"优先级高。
CSS 优先规则3:优先级关系:内联样式 > ID 选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器
如果外部样式表和内部样式表中的样式发生冲突会出现什么情况呢?这与样式表在 HTML 文件中所处的位置有关。样式被应用的位置越在下面则优先级越高,其实这仍然可以用规则 4 来解释。
CSS 优先规则4:计算选择符中 ID 选择器的个数(a),计算选择符中类选择器、属性选择器以及伪类选择器的个数之和(b),计算选择符中标签选择器和伪元素选择器的个数之和(c)。按 a、b、c 的顺序依次比较大小,大的则优先级高,相等则比较下一个。若最后两个的选择符中 a、b、c 都相等,则按照"就近原则"来判断。
CSS 优先规则5:属性后插有 !important 的属性拥有最高优先级。若同时插有 !important,则再利用规则 3、4 判断优先级。
具体可参考文章:CSS 样式优先级
36、ts是怎么定义接口的?有什么区别?
ts可以使用type和interface来定义接口。
相同点
- 都可以描述一个对象或者函数。
- 都允许扩展(extends),interface和type都可以拓展,并且两者并不是相互独立的,也就是说interface可以extends type,type也可以extends interface,虽然效果差不多,但是两者语法不同。
不同点
- type可以声明基本类型别名,联合类型,元组等类型,interface不行
- type 语句中还可以使用typeof获取实例的类型进行赋值,interface不行
- interface可以声明合并,type不行
37、let const var的区别
var声明的变量是全局作用域的,会被提升到作用域顶部,可以先使用后声明,可以重复声明和赋值。
let和const声明的变量都是块级作用域的,必须先声明后使用,不能重复声明,const声明的变量不能重复赋值。
38、介绍下原型和原型链
原型是一个可以被复制或者叫克隆的一个类,通过复制原型可以创建一个一模一样的新对象。通俗的说,原型就是一个模板,在设计语言中更准确的说是一个对象模板。原型的属性和方法总是被原型实例所共享。通过原型创建的新对象实例是相互独立的。
原型链记录了原型对象创建的整个过程,所有的对象都拥有一个_proto_属性指向该对象的原型(prototype),当查找一个对象的属性时,javascript会根据原型链向上遍历对象的原型,直到找到给定名称的属性为止,直到到达原型链的顶部任然没有找到指定的属性,就会返回undefined
39、个人规划,打算多久实现自己的目标?
这个就因人而异拉,一般都会被问到。
40、项目周期短,做的类型多,怎么做到优化代码进行平衡?
41、vue渲染大量数据时应该怎么优化
- 增加加载动画提升用户体验
- 采用分页
- 异步渲染组件
- 使用v-if最多显示上中(可视区域)下三屏
- 按需加载局部数据,虚拟列表,无线下拉刷新
- 采用ssr,服务端渲染
42、js数据类型有哪些?
- 原始类型:
null,undefined,symbol,string,boolean,number - 对象类型:
object,function - 原始类型进行拷贝的时候是值的拷贝,对象类型进行拷贝的时候是地址的拷贝
43、有没有用过nginx
44、说说React的生命周期
- 初始化阶段:
getDefaultProps:获取实例的默认属性 getInitialState:获取每个实例的初始化状态componentWillMount:组件即将被装载、渲染到页面上render:组件在这里生成虚拟的DOM节点componentDidMount:组件真正在被装载之后 运行中状态componentWillReceiveProps:组件将要接收到属性的时候调用shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了)componentWillUpdate:组件即将更新不能修改属性和状态render:组件重新描绘componentWillUnmount:组件即将销毁
45、dva有没有用过
react用的比较少,不咋熟悉
46、你觉得你有哪些优点?
可以开启自夸环节了,但是还是要稍微谦虚一点点拉。
47、做了两道变量提升的相关题目
var声明的变量只是声明会提升,函数是会被整体提升的,并且函数提升的优先级大于变量提升。
题目一
<script>
console.log(a) //function a(){}
var a = 1;
console.log(a) //1
function a(){}
console.log(a) //1
</script>
执行顺序如下:
<script>
var a;
a = function a(){}
console.log(a)//function a(){}
a=1;
console.log(a)//1
console.log(a)//1
</script>
题目二
<script>
a(); //1
console.log(a) // function a(){ console.log(1)}
var a = c = function() {
console.log(2)
};
a(); //2
function a() {
console.log(1)
}
a();//2
(function(b) {
b(); //2
c(); //2,c是全局变量
var b = c = function a() {
console.log(3)
}
b() //3
})(a);
c();
</script>
var a = b = 1相当于b = 1; var a = b
48、介绍下react的组件通信
react组件间通信方法:
- 父组件向子组件通讯:父组件可以向子组件通过传
props的方式,向子组件进行通讯。 - 子组件向父组件通讯:
props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中。 - 兄弟组件通信:找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信。
- 跨层级通信:
Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或者首选语言,对于跨越多层的全局数据通过Context通信。 - 发布订阅模式:发布者发布事件,订阅者监听事件并作出反应,我们可以通过引入
event模块进行通信。 - 全局状态管理工具:借助
redux或者mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产品新的状态。
49、原生小程序怎么获取页面的高度
- 获取系统信息
let that = this;
// 获取系统信息
wx.getSystemInfo({
success: function (res) {
that.setData({
widheight: res.screenHeight,
widwidth: res.screenWidth
});
}
});
- 获取元素宽高信息
//创建节点选择器
var query = wx.createSelectorQuery();
//选择id
query.select('#test').boundingClientRect()
query.exec(function (res) {
//res就是 所有id为test的元素的信息数组
console.log(res);
//取高度
console.log(res[0].height);
//取宽度
console.log(res[0].weight);
})
50、解释下防抖及节流,分别用在什么地方?
在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。
- 防抖
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
- 节流
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}
51、常用排序算法有哪些?
通用函数
function checkArray(array) {
if (!array) return
}
function swap(array, left, right) {
let rightValue = array[right]
array[right] = array[left]
array[left] = rightValue
}
- 冒泡排序
function bubble(array) {
checkArray(array);
for (let i = array.length - 1; i > 0; i--) {
// 从 0 到 `length - 1` 遍历
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) swap(array, j, j + 1)
}
}
return array;
}
- 选择排序
function selection(array) {
checkArray(array);
for (let i = 0; i < array.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < array.length; j++) {
minIndex = array[j] < array[minIndex] ? j : minIndex;
}
swap(array, i, minIndex);
}
return array;
}
还有快排,归并排序等等
52、webpack3和webpack4有哪些区别?
Webpack4版本增加mode配置,提供了三种构建模式可供选择development,production和none,默认为production, 不同模式下默认的配置也有所不同。- 在
webpack4版本中推荐使用mini-css-extract-plugin插件来替代extract-text-webpack-plugin抽取css到单独文件中,使用optimize-css-assets-webpack-plugin对css进行压缩处理 CommonChunksPlugin已经从webpack4中移除。可使用optimization.splitChunks进行模块划分(提取公用代码)。 但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对entry进行拆分,需要设置optimization.splitChunks.chunks = 'all'。webpack4使用动态import,而不是用system.import或者require.ensurewebpack也不需要使用UglifyJsPlugin这个plugin了,只需要使用optimization.minimize为true就行,production mode下面自动为true
具体可参考文章:Webpack4.x实践
53、vue渲染是同步的还是异步的
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。采用nextTick可以获取更新后的dom。
54、vue的普通插槽和作用域插槽有什么区别?
- 单个插槽,也就是默认插槽,单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(
name属性)不同就可以了。 - 具名插槽,就是加了名字的插槽。根据名字放在组件对应的地方
- 作用域插槽,就是带数据的插槽,因为单个插槽和具名插槽不绑定数据,所以父组件提供的模板一般要既包括样式又包括内容,而作用域插槽,父组件只需要提供一套样式(在确定用作用域插槽绑定的数据的前提下)。
具体可参考文章:深入理解vue中的slot与slot-scope
55、v-if和v-show有什么区别?
v-show仅仅控制元素的显示方式,将display属性在block和none来回切换;而v-if会控制这个DOM节点的存在与否。当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。
56、taro有什么优势,说下它的原理
一套代码可以多端运行,减少了工作量,增强了项目的可维护性。
原理大致就是利用babel将JSX转为各端的模板(采用穷举),然后定义一个统一的组件库和api,让各端用自己的组件库和api来实现这个组件库与api,同时还要为不同的端编写相应的运行时框架,负责初始化等等操作。
具体可参考文章:为何我们要用 React 来写小程序 - Taro 诞生记
57、介绍一下MVVM
MVVM分为Model、View、ViewModel三者。
Model代表数据模型,数据和业务逻辑都在Model层中定义;
View代表UI视图,负责数据的展示;
ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Modal中的数据改变时会出发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步
这种模式实现了Model和View的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作dom。
58、key的作用
key的特殊attribute主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用key时,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。有相同父元素的子元素必须有独特的key,重复的key会造成渲染错误。
不带key并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所有的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。
这种模式会带来一些隐藏的副作用,比如不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。Vue文档也说明了这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时DOM状态(例如:表单输入值)的列表渲染输出
key的作用:
- 更准确,因为带key就不是就地复用了,在sameNode函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确
- 更快,利用key的唯一性生成map对象来获取对应节点,比遍历方式更快
59、vue和react的区别
react和vue都是做组件化的,整体的功能都类似。但是他们的设计思路是有很多不同的,使用react和vue,主要是理解他们的设计思路的不同。
- 数据是不是可变的,
react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在react中,是单向数据流,推崇immutable来实现数据不可变。react在setState之后会重新走渲染的过程,如果shouldComponentUpdate返回的是true,就继续渲染,如果返回了false,就不会重新渲染,PureComponent就是重写了shouldComponentUpdate,然后在里面做了props和state的浅层对比
而vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立watcher来监听,当属性变化的时候,响应式的更新对应虚拟dom。总之,react的性能优化需要手动去做,而vue的性能优化是自动的。但是vue的响应式机制也有问题,就是当state特别多的时候,watcher也会很多,会导致卡顿,所以大型应用(状态特别多的),一般用react,更加可控 react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component,jss等vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html,js,css写到一个文件中,html提供了模板引擎来处理。- 在
vue中我们组合不同功能的方式是通过mixin,而在react中是通过高阶组件。 vuex和redux的区别,从表面来说,store注入和使用方式有一些区别
在vuex中,$store被直接注入到了组件实例中,因此可以比较灵活的使用:使用dispatch和commit提交更新,通过mapstate或者通过this.$store来读取数据
在redux中,我们每一个组件都需要显示的用connect把需要的props的dispatch连接起来,另外vuex更加灵活一些,组件中既可以dispatch action也可以commit updates,而redux中只能进行dispatch,并不能直接调用reducer进行修改- 从实现原理上来说,最大的区别是两点:
Redux使用的是不可变数据,而Vuex的数据是可变的。Redux每次都是用新的state替换旧的state,而Vuex是直接修改
Redux在检测数据变化的时候,是通过diff的方式比较差异的,而Vuex其实和Vue的原理一样,是通过getter/setter来比较的(如果看Vuex源码会知道,其实他内部直接创建一个Vue实例用来跟踪数据变化)
60、vue3有了解吗?
performance:性能比vue2.0更强tree shaking support:可以将无用模块“剪辑”,仅打包需要的(比如v-model,<transition>,用不到就不会打包)composition API:组合APIFragment,Teleport,Suspense:“碎片”,Teleport即Protal传送门,“悬念”fragment不在限于模板中的单个根节点Better TypeScript support:更优秀的Ts支持Custom Renderer API:暴露了自定义渲染API
具体可参考文章:抄笔记:尤雨溪在Vue3.0 Beta直播里聊到了这些…
61、说一下Event Loop
Javascript是单线程的,所有的同步任务都会在主线程中执行。异步任务会放入任务队列。- 当主线程中的任务都执行完后,系统会依次读取任务队列中的事件,与之对应的异步任务进入主线程开始执行
- 异步任务之间会存在差异,所以它们的执行的优先级也会有区别。大致分为微任务(
miscotask,如:Promise、MutaionObserver等)和宏任务(macrotask,如setTimeout、setInterval,I/O等) Promise执行器中的代码会被同步调用,但是回调是基于微任务的- 宏任务的优先级高于微任务
- 每一个宏任务执行完毕都必须将当前的微任务队列清空(即当前主线程的同步任务执行完之后会先执行异步任务中的微任务,然后再执行下一轮宏任务,即执行异步任务中的宏任务)
- 第一个
script标签的代码是第一个宏任务
主线程会不断重复上面的步骤,直到执行完所有任务
62、js继承了解吗?
继承主要有以下几种方式:原型链继承、构造函数继承、组合继承、实例继承、拷贝继承、寄生组合继承、es6--Class继承
具体可参考文章:JavaScript常用八种继承方案