一、前端基础
JS:
1、Promise、Async有什么区别
- Async 更简洁,不需要用 then 连接
-
Promise 中不能自定义使用 try/catch 进行错误捕获,但是在 Async/await 中可以像处理同步代码处理错误
- 调试更方便
- Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因
2、介绍service worker
-
一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
-
一旦被 install,就永远存在,除非被 uninstall
-
需要的时候可以直接唤醒,不需要的时候自动睡眠(有效利用资源,此处有坑)
-
可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
-
能向客户端推送消息
-
不能直接操作 DOM,但是service worker可以通过postMessage与页面之间通信,把消息通知给页面,如果需要的话,让页面自己去操作DOM。
-
出于安全的考虑,必须在 HTTPS 环境下才能工作
-
异步实现,内部大都是通过 Promise 实现
3、列举Es6,常用的一些新特性
-
let, const:新增块级作用域,分别定义变量和常量
-
class, extends, super:用
class
定义了一个“类”,可以看到里面有一个constructor
方法,这就是构造方法,而this
关键字则代表实例对象;Class之间可以通过extends
关键字实现继承;super
关键字,它指代父类的实例(即父类的this对象)30分钟掌握ES6/ES2015核心内容(上) -
箭头函数:函数体内的this对象,就是定义时所在的对象,而不是运行时时所在的对象。
-
模板字符串(template string)
-
解构(Destructuring)
-
ES6模块:(import export):ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。30分钟掌握ES6/ES2015核心内容(下)
// 假设我们有两个js文件: index.js和content.js,现在我们想要在index.js中使用content.js返回的结果,我们要怎么做呢? //content.js define('content.js', function(){ return 'A cat'; }) // AMD //index.js require(['./content.js'], function(animal){ console.log(animal); //A cat }) // CMD //index.js var animal = require('./content.js') //content.js module.exports = 'A cat' // ES6 //index.js import animal from './content' //content.js export default 'A cat'
keys:获取对象键名,返回一个数组
values:获取对象值,返回一个数组
entries:把对象键值对转换成一个数组,返回一个包含键值对数组的新数组
includes:用于某个数组或字符串是否包含给定的值,返回一个布尔值。如果没找到符合条件的成员就返回 underfind
参考资料如下:
ECMAScript 6 入门
ES6 Promise 用法讲解
4、说下自己对模块化开发的理解,以及模块化开发的好处
- 提高开发效率,有利团队协同开发,
- 避免全局变量污染,命名冲突,
- 方便代码的复用维护等。
5、浏览器事件流向
- “DOM2级事件”规定事件流包括三个阶段,事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的事件捕获,为截获事件提供了机会,然后是实际的目标接收了事件,最后一个阶段是冒泡阶段
- 注意处于目标阶段的时候,既不是冒泡阶段、也不是捕获阶段,事件处理程序被调用的顺序是注册的顺序
6、类型转换规则
[] == ![]
- 逻辑非运算符使 ![] 转换为布尔值 false
- 如果有一方是布尔值,就把布尔值转换成 number 0 再比较 [] == 0
- 如果一方是 object ,且另一方为 string 或 number, object就先调用 valueOf [].valueOf() 返回 [] 不是基本类型,再调用 toString 返回 '' ,最后将 '' 转换为 0
- 转换顺序:object / 布尔值 -> string -> number
7、如何实现深拷贝?
- 很多人认为
Object.assign
是用来深拷贝的,其实并不是,Object.assign
只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。 - 展开运算符 ... 和
Object.assign()
行为一致, 执行的都是浅拷贝(只遍历一层) - JSON.parse(JSON.stringify(object)) 可以用来深拷贝,但它会会忽略掉函数和
undefined
- lodash 的深拷贝函数:
function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null } if (!isObject(obj)) { throw new Error('非对象') } let isArray = Array.isArray(obj) // 扩展运算符只拷贝一层 let newObj = isArray ? [...obj] : { ...obj } Reflect.ownKeys(newObj).forEach(key => { newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] }) return newObj }
8、如何理解原型、原型链?
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它- 函数的
prototype
是一个对象 - 对象的
__proto__
属性指向原型,__proto__
将对象和原型连接起来组成了原型链
9、基本类型判断,如怎么区分数组和对象?
- 用typeof运算符来判断
- 用Object的toString方法判断 :兼容性最好的方法
- 用Array对象的isArray方法判断 :最便捷安全的方法
if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; }
10、写一个通用的事件侦听器函数
// 视能力分别使用dom0||dom2||IE方式 来绑定事件 // 参数: 操作的元素,事件名称 ,事件处理程序 addEvent : function(element, type, handler) { if (element.addEventListener) { //事件类型、需要执行的函数、是否捕捉 element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent('on' + type, function() { handler.call(element); }); } else { element['on' + type] = handler; } }, // 移除事件 removeEvent : function(element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.datachEvent) { element.detachEvent('on' + type, handler); } else { element['on' + type] = null; } }, // 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获) stopPropagation : function(ev) { if (ev.stopPropagation) { ev.stopPropagation(); } else { ev.cancelBubble = true; } }, // 取消事件的默认行为 preventDefault : function(event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, // 获取事件目标 getTarget : function(event) { return event.target || event.srcElement; }, // 获取event对象的引用,取到事件的所有信息,确保随时能使用event; getEvent : function(e) { var ev = e || window.event; if (!ev) { var c = this.getEvent.caller; while (c) { ev = c.arguments[0]; if (ev && Event == ev.constructor) { break; } c = c.caller; } } return ev; }
10、用js实现千位分隔符
/(d)(?=(/d{3})+.)/g
num.toString() .replace(/(d)(?=(d{3})+.)/g, function($0, $1) { return $1 + ","; });
CSS:
1、介绍css3中position: sticky
absolute 生成绝对定位的元素,相对于值不为 static的第一个父元素进行定位。
fixed (老IE不支持) 生成绝对定位的元素,相对于浏览器窗口进行定位。
relative 生成相对定位的元素,相对于其正常位置进行定位。
static 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right z-index 声明)。
inherit 规定从父元素继承 position 属性的值。
- sticky 相对于最近的块级父元素定位
- 父级元素不能有任何
overflow:visible
以为的overflow设置,否则没有粘滞效果。因为改变了滚动容器(即使没有出现滚动条)。因此,如果你的position:sticky
无效,看看是不是某一个祖先元素设置了overflow:hidden
,移除之即可。 - 同一个父容器中的sticky元素,如果定位值相等,则会重叠;如果属于不同父元素,则会鸠占鹊巢,挤开原来的元素,形成依次占位的效果
2、浏览器的兼容性
png24位的图片在iE6浏览器上出现背景,解决方案是做成PNG8
浏览器默认的margin和padding不同。解决方案是加一个全局的*{margin:0;padding:0;}来统一
IE下,even对象有x,y属性,但是没有pageX,pageY属性; Firefox下,event对象有pageX,pageY属性,但是没有x,y属性
- 一些新特性如 flex ,需要加前缀兼容不同的浏览器
- even对象 Chrome 等浏览器用 e.target、而 IE 用 e.srcElement
- 给元素添加监听事件时,Chrome 等浏览器用的 addEventListener, 而IE 用 attachEvent
3、简述一下你对HTML语义化的理解?
用正确的标签做正确的事情。
html语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析;
即使在没有样式CSS情况下也以一种文档格式显示,并且是容易阅读的;
搜索引擎的爬虫也依赖于HTML标记来确定上下文和各个关键字的权重,利于SEO;
便于阅读维护理解。
4、请描述一下 cookies,sessionStorage 和 localStorage 的区别?
是否携带:cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密),会在 http 请求中携带(即使不需要)
存储大小: cookie数据大小不能超过4k。 sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
有期时间: localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage 数据在当前浏览器窗口关闭后自动删除。 cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
5、frame有那些缺点?
*iframe会阻塞主页面的加载(Onload事件)
*不利于搜索引擎优化;
如果需要使用iframe,最好是通过javascript 动态给iframe添加src属性值,这样可以绕开以上两个问题。
6、如何实现浏览器内多个标签页之间的通信? (阿里)
WebSocket、SharedWorker;
也可以调用localstorge、cookies等本地存储方式;
localstorge另一个浏览上下文里被添加、修改或删除时,它都会触发一个事件 storage
7、CSS3有哪些新特性?
圆角 (border-radius:8px)
阴影 (ShadowReflect)
文字特效 (text-shadow、)
线性渐变 (gradient)
旋转 (transform) 缩放,定位,动画等,例如:transform:scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg)Animation:
二、构建工具
1、Webpack热更新实现原理
- 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送消息给客户端
- 客户端获取到hash,成功后客户端构造hot-update.js script链接,然后插入主文档
- hot-update.js 插入成功后,执行hotAPI 的 createRecord 和 reload方法,获取到 Vue 组件的 render方法,重新 render 组件, 继而实现 UI 无刷新更新
2、减少 Webpack 打包时间
- 首先我们可以优化 Loader 的文件搜索范围,只在 src 文件夹下查找编译的 js 文件,排除
node_modules
中的文件,当然这样做还不够,我们还可以将 Babel 编译过的文件缓存起来
loader: 'babel-loader?cacheDirectory=true'
- HappyPack 可以将 Loader 的同步执行转换为并行的
- DllPlugin 可以将特定的类库提前打包然后引入
- 代码压缩
- 如果你确定一个文件下没有其他依赖,就可以使用该 module.noParse 让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
3、减少 Webpack 打包后的文件体积
- 按需加载
- Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去
- Tree Shaking 可以实现删除项目中未被引用的代码
三、框架原理
1、描述一下React 生命周期
渲染过程调用到的生命周期函数,主要几个要知道;
* constructor
* getInitialState
* getDefaultProps
* componentWillMount
* render
* componentDidMount
更新过程
* componentWillReceiveProps
* shouldComponentUpdate
* componentWillUpdate
* render
* componentDidUpdate
卸载过程
componentWillUnmount
2、实现组件有哪些方式?
React.createClass 使用API来定义组件 React ES6 class component 用 ES6 的class 来定义组件 Functional stateless component 通过函数定义无状态组件
3、应该在React生命周期的什么阶段发出ajax请求,为什么?
AJAX请求应在 componentDidMount函数 进行请求。因为此时组件已经挂载安装,保证了数据更新后对应的组件也能更新
4、当组件的setState函数被调用之后,发生了什么?
React 会将传入的参数与组件当前的状态合并,生成新的状态 一> 根据新的状态构建 React 元素树 一> 计算出新的树与老树的节点差异,进行最小化重渲染
5、为什么循环产生的组件中要利用上key这个特殊的prop?
Keys负责帮助React跟踪列表中哪些元素被改变/添加/移除。React利用子元素的key在比较新旧元素树的时候,快速得知一个元素是新的还是刚刚被移除。
6、介绍redux,主要解决什么问题
- 是管理应用程序状态的库,解决数据管理和数据通信的问题
- view 发出一个 action —> 派发器接收 action —> 让 store 进行数据更新:reducer 进行 state 操作 —> view 通过 store 提供的 getState 获取最新的数据
7、redux 有什么缺点
-
一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取。
-
当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的 shouldComponentUpdate 进行判断。
8、为什么虚拟 dom 会提高性能?(必考)
- 虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
9、react diff 原理(常考,大厂必考)
-
把树形结构按照层级分解,只比较同级元素。
-
给列表结构的每个单元添加唯一的 key 属性,方便比较。
-
合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
-
选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能
8、介绍react优化
用 shouldComponentUpdate 避免资源浪费
复杂的页面不要在一个组件里面写完。
- 请尽量使用
const element
。 - map里面添加key,并且key不要使用index(可变的)。具体可参考使用Perf工具研究React Key对渲染的影响
- 尽量少用
setTimeOut
或不可控的refs、DOM操作。 props
和state
的数据尽可能简单明了,扁平化。- 使用
return null
而不是CSS的display:none
来控制节点的显示隐藏。保证同一时间页面的DOM节点尽可能的少
四、性能优化
0、在开发项目上,知道那些优化的方式
- 性能优化:减少 http 请求数、避免跳转重定向、压缩代码图片、
减少DOM操作、避免全局变量、
将样式表放在顶部,将脚本放在底部、
小图片(<10kb)转换base64编码、资源按需加载等。 - 代码优化:有意义的命名,适当的注释,代码逻辑清晰,避免巨大函数,避免对象强耦合、使用事件代理、函数节流和防抖、禁止全局变量防止内存泄漏等。
1、函数节流和防抖
-
debounce:把触发非常频繁的事件(比如按键搜索)合并成一次执行。
-
throttle:保证每 X 毫秒恒定的执行次数,比如每200ms检查下滚动位置,并触发 CSS 动画或请求数据。
-
requestAnimationFrame:可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。
requestAnimationFrame优点
- 动画保持 60fps(每一帧 16 ms),浏览器内部决定渲染的最佳时机
- 简洁标准的 API,后期维护成本低
- 浏览器标签未激活时,一切都不会执行
2、介绍事件代理以及优缺点
在js中,当我们移除某个元素但没有将元素和监听函数进行解绑时,事件处理函数依旧会留在内存中,无法被当成垃圾回收。
- 减少事件注册,节省内存
- 减少了dom节点更新的操作,处理逻辑只需在委托元素上进行
- 缺点:事件委托基于冒泡,对于onfoucs和onblur等事件不支持;层级过多,冒泡过程中,可能会被某层阻止掉
3、手写原生js实现事件代理,并要求兼容浏览器
- 其实就是考核对事件对象e的了解程度,以及在IE下对应的属性名
- addEventListener、attachEvent
- e.target、e.srcElement
function delegateEvent(interfaceEle, selector, type, fn) { if(interfaceEle.addEventListener){ interfaceEle.addEventListener(type, eventfn); }else{ interfaceEle.attachEvent("on"+type, eventfn); } function eventfn(e){ var e = e || window.event; var target = e.target || e.srcElement; //如果目标元素与选择器匹配则执行函数 if (matchSelector(target, selector)) { if(fn) { //将fn内部的this指向target(在此之前this都是指向的绑定事件的元素即interfaceEle) fn.call(target, e); } } } }
4、React组件事件代理的原理、事件处理过程
- 并非 #child 和 #parent 的事件分别代理到 document 上,而是 React 在 document 上绑定了一个 dispatchEvent 函数(事件),在执行 dispatchEvent 的过程中,其内部会依次执行 #child 和 #parent 上绑定的事件。
- React 合成事件对象的
e.stopPropagation
,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡,更加不能阻止已经触发元素的多个事件的依次执行。在这种情况下,只有原生事件对象的stopImmediatePropagation
能做到。 - 事件处理过程:
- 示例:
5、什么是内存泄漏?
本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收
所以,确保用完以后把它设置为 null 或者重新定义
三种类型的常见 JavaScript 内存泄漏
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包里多余的没有被引用的变量
-
实例:使用 Chrome 发现内存泄漏
6、PWA:Workbox
五、算法、设计模式
前端架构主要解决的是高复用性,架构能力提升方向主要是组件库开发、前端框架实现
1、观察者和订阅-发布的区别,各自用在哪里
-
在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
-
在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
-
观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
- 观察者设计模式定义了对象间的一种一对多的组合关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新
- 示例
六、网络安全、通信协议
1、HTTP2.0和HTTP1.X相比的新特性
- 新的二进制格式,相对文本来说有更强的健壮性
- 多路复用(MultiPlexing),即连接共享。一个连接上可以有多个request,一个request对应一个id 。
- header压缩:HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送(server push)
2、通过什么做到并发请求
对请求并发数进行限制,并且使用排队机制让请求有序的发送出去
3、http1.1时如何复用tcp连接
使用 keep-alive
4、三次握手、四次挥手
- 客户端向服务器发送一个 SYN(synchronize:同步) 包,请求建立连接
- 服务器收到后会发一个对SYN包的确认包ACK (Acknowledgement)即是确认字符回去
- 客户端发送一个确认包(ACK),通知服务器连接已建立
客户端发送FIN报文,没有数据要发给你了
5、什么是 XSS 攻击?如何防范 XSS 攻击?什么是 CSP?
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过注入恶意脚本,使之在用户的浏览器上运行
解决方法:
- 转义字符
- CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行
什么是 CSP(Content-Security-Policy) ?
内容安全策略 (CSP) 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行
通常可以通过两种方式来开启 CSP:
- 设置 HTTP Header 中的
Content-Security-Policy
- 设置
meta
标签的方式<meta http-equiv="Content-Security-Policy">
示例:
-
只允许加载本站资源
Content-Security-Policy: default-src ‘self’
-
只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*
6、什么是 CSRF 攻击?如何防范 CSRF 攻击?
CSRF(Cross-site request forgery)跨站请求伪造:冒充用户执行某项操作的目的
- CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
- CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
- 保证页面的幂等性,后端接口不要在GET页面中做用户操作。
示例:
2007年Gmail的CSRF漏洞:点开一个黑客的链接,所有邮件都被窃取(攻击者诱导用户进入某个页面,在页面中通过表单提交 POST
请求,也说明了表单是可以进行跨域攻击的)
2008年YouTube上几乎所有用户可以操作的动作都存在CSRF漏洞
2012年WordPress发现了一个操作用户账户的CSRF漏洞:攻击者引导用户先进入目标的WordPress,然后点击其钓鱼站点上的某个按钮,该按钮实际上是表单提交按钮,会添加某个具有管理员权限的用户
7、什么是点击劫持?如何防范点击劫持?
点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过
iframe
嵌套的方式嵌入自己的网页中,并将iframe
设置为透明,在页面中透出一个按钮诱导用户点击。
X-FRAME-OPTIONS
是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe
嵌套的点击劫持攻击。
DENY
,表示页面不允许通过iframe
的方式展示SAMEORIGIN
,表示页面可以在相同域名下通过iframe
的方式展示ALLOW-FROM
,表示页面可以在指定来源的iframe
中展示
8、什么是中间人攻击?如何防范中间人攻击?
中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。如:不安全的公共 WIFI
9、URI 与 URL 的区别
- URI:统一资源标志符
- URL:统一资源定位符
- URN:URN 是另一种形式的 URI,它通过特定命名空间中的唯一名称来标识资源,比如 ISBN 版本号,
urn:isbn:9780141036144
- URI 包括很多协议,如 HTTPS、data、URN 等等,所以 URL 和 URN 都是 URI 的子集
六、监控
1、页面埋点
- 手动埋点:灵活性高,但工作量大
- 自动埋点:灵活性低,统计所有的事件并且定时上报,但工作量小
2、性能监控
只需要调用
performance.getEntriesByType('navigation') ,就可以获得页面中各种详细的性能相关信息
3、异常监控
- 通常的办法是使用
window.onerror
拦截报错 - 对于跨域的代码运行错误会显示
Script error.
对于这种情况我们需要给script
标签添加crossorigin
属性 - 对于异步代码来说,可以使用
catch
的方式捕获错误。比如Promise
可以直接使用catch
函数,async await
可以使用try catch
script标签的crossorigin属性
crossorigin的属性值可以是
anonymous
、use-credentials
,会被浏览器默认做anonymous
。crossorigin的作用有三个:
- crossorigin会让浏览器启用CORS访问检查,检查http相应头的Access-Control-Allow-Origin
- 对于传统script需要跨域获取的js资源,控制暴露出其报错的详细信息
- 对于
module script
,控制用于跨域请求的凭据模式script标签的integrity属性
<script crossorigin="anonymous" integrity="sha256-PJJrxrJLzT6CCz1jDfQXTRWOO9zmemDQbmLtSlFQluc=" src="https://assets-cdn.github.com/assets/frameworks-3c926bc6b24bcd3e820b3d630df4174d158e3bdce67a60d06e62ed4a515096e7.js"></script>这是github源码,
integrity
告诉浏览器,使用sha256签名算法对下载的js文件进行计算,并与intergrity
提供的摘要签名对比,如果二者不一致,就不会执行这个资源。
- 减少由【托管在CDN的资源被篡改】而引入的XSS 风险
- 减少通信过程资源被篡改而引入的XSS风险(同时使用https会更保险
script标签的
async
和defer
属性
async
仅适用于外链,规定脚本异步执行:不会按照出现的顺序执行,执行的时候,有可能页面还没解析完成,不会阻塞页面解析
defer
仅适用于外链,规定脚本延迟执行:会按照出现的顺序执行,在html解析完成后, DOMContentLoaded之前执行,不会阻塞页面解析