HTML
1. <script> 标签到底放在那里好
JS代码加载完成之后会立即执行,执行时会阻塞页面的内容,阻塞DOM树创建,所以一般将所有的JS放到<body>标签底部。
DOMContentLoaded 文档DOM结构加载完成时触发的事件,在load事件之前
<script > async和defer 属性使JS的加载不会阻塞DOM的渲染
(1) 区别:
defer 浏览器会异步下载JS文件不会影响后续DOM的渲染,但当有多个script设置了defer属性时,会按照顺序进行加载。defer 脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行
async 不会按照script页面顺序,而是谁先下载完谁执行,DOMContentLoaded事件的触发并不受async脚本加载的影响
(2) 应用场景:
defer 当脚本依赖于页面中的DOM元素时,或者被其它JS脚本所依赖时。async 当脚本并不关心页面DOM元素,并且也不会产生其它脚本所需要的数据。当不确定时,用defer会比用async更稳定。
PS: script 放在头部,会阻塞DOM的渲染,如果JS会执行时间很长,会导致网页看起来很卡;放在尾部,当有很多DOM元素需要依赖JS进行修改时,就会导致多次重绘或重排,性能很差,所以最好是使用defer或async,一边解析页面,一边下载js
Head 先加载 再调用(事件) 才执行
Body 一遍加载一遍执行 在加载的时候执行
HTML5
1. 拖拽Drag(字节曾经的面试题)
将一个对象(A)拖拽到另一个对象(B)范围内。
拖拽对象(A)
- 设置draggable=”true”,设置对象为可拖拽的
- 设置οndragstart=”drag(event)”, 拖拽开始时触发事件
- οndragend=”func(event)”, 拖拽结束出发事件
目标对象(B)
- οndragenter=”dragenter()” 拖拽对象刚进入容器范围时触发
- οndragοver=”allowDrog(event)” 拖拽对象在容器内移动时触发
- οndragleave=”dragleave()” 拖拽对象离开容器时触发
- οndrοp=”drop(event)” 放下拖拽对象时触发(松开鼠标按键)
fuction drag(ev){
ev.dataTransfer.setData(“Text”,ev.target.id);
}
function allowDrog(ev){
ev.preventDefault();
}
function drog(ev){
ev.preventDefault();
var dataId=ev.dataTransfer.getData(“Text”);
ev.target.appendChild(document.getElementById(dataId));
}
2. HTML5与HTML相比有什么改进
A、 新增了一些语义化标签,如article,header,footer,dialog等
B、 新的全局属性:id, tabindex, repeat
C、 DOCTYPE更简洁
D、新增了一些高级标签,如<video>,<audio>,<canvas>
3. 语义化标签
如 header footer nav section (节)
语义化标签,顾名思义,可以直接从标签名中看出标签的作用,防止滥用div,代码结构更加清晰,更加便于阅读。
<audio controls="controls" height="100" width="100">
<source src="song.mp3" type="audio/mp3" />
<source src="song.ogg" type="audio/ogg" />
<embed height="100" width="100" src="song.mp3" />
</audio>
<video width="320" height="240" controls="controls">
<source src="movie.mp4" type="video/mp4" />
<source src="movie.ogg" type="video/ogg" />
<source src="movie.webm" type="video/webm" />
<object data="movie.mp4" width="320" height="240">
<embed src="movie.swf" width="320" height="240" />
</object>
</video
CSS
1. flex布局——弹性盒子(常用,常考)
容器属性 | flex-direction 项目排列方式 | row row-reverse column column-reverse |
flex-wrap 项目换行方式 | nowrap wrap wrap-reverse | |
justify-content 水平轴对齐方式 | flex-start flex-end center space-around space-between | |
align-items 垂直轴对齐方式 | flex-start flex-end center baseline stretch | |
项目属性 | order | 项目排序,值越小越靠前 |
flex-grow | 默认值0,等比例放大,0不放大 | |
flex-shrink | 默认值1,等比例缩小 | |
flex-basis | 默认值 auto,项目大小,优先级高于width | |
align-self | 项目自身的对齐方式 |
flex:1 === flex: 1 1 auto
2. Grid布局(网格布局)
注意,设为网格布局以后,容器子元素(项目)的float、display: inline-block、display: table-cell、vertical-align和column-*等设置都将失效
a) 容器与项目
容器是外层的父元素,项目是顶层的子元素,grid布局只对项目生效。
默认情况下,容器元素都是块元素,但也可以设置为行内元素,使用inline-grid
b) 容器属性
display: gird /inline-grid
grid-template-columns:100 100px 100px;定义每一列的列宽
grid-template-rows:100px 100px 100px; 定义每一行的行高
A. 绝对单位px
B. 百分比 grid-template-columns: 33.33% 33.33% 33.33%;
C. repeat 简化重复值 repeat(3, 33.33%);
D. auto-fill 填充,每一行尽可能容纳多的单元格
repeat(auto-fill, 100px); 每个单元格大小固定为100px, 容器大小不固定。
E. fr 表示倍数关系
150px 1fr 2fr; 第一列为150px, 第2列宽度是第3列的一半
F. minmax(最小值,最大值)
1fr 1fr minmax(100px, 1fr);
G.auto 关键字指定单元格的最大宽度,100px auto 100px;
H. 网格线的名称:grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4];
三列有四个网格线
grid-row-gap 行与行的间隔
grid-column-gap 列与列的间隔
grid-gap: <grid-row-gap> <grid-column-gap> 缩写
PS: 根据最新标准,上面三个属性名的grid-前缀已经删除,grid-column-gap和grid-row-gap写成column-gap和row-gap,grid-gap写成gap。
容器属性 | display: grid / inline-grid | grid: 容器元素为块元素 inline-grid:容器元素为行内元素 |
grid-template-rows 各行的行高 grid-template-column各列的列宽 grid-row-gap 行距 grid-column-gap 列距 | 固定单位100px 100px 100px 百分比33.33% 33.33% 33.33% repeat(3,33.33%) repeat(auto-fill,100px) 倍数 fr minmax(最小值,最大值) 100px auto 100px | |
grid-auto-flow 容器内项目排列方式 | row 先行后列 column 先列后行 row dense 先行后列,紧密排列 column dense 先列后行,紧密排列 | |
justify-items 项目内容的水平对齐方式 align-items 项目内容的垂直对齐方式 | start 左对齐 end 右对齐 center: 居中 stretch 填满(默认) | |
justify-content 项目的水平对齐方式 align-content 项目的垂直对齐方式 | start end center stretch space-between 两端对齐 space-around 项目两侧间隔相等 space-evenly | |
grid-auto-rows 额外行行高 grid-auto-columns 额外列列宽 | 与grid-template-rows属性相同 | |
项目属性 | grid-column-start grid-column-end grid-row-start grid-row-end | 网格线名 span 2 ——设定网格跨行/列 |
justify-self align-self |
3. 选择器
a)选择器
- 后代选择器: div p{} 选择所有div下的p标签子元素。
- 子元素选择器 div>p{} 只选择div下的直接p标签子元素
- 相邻兄弟选择器 #pp1+p{} 选择id=pp1的相邻兄弟,且这个兄弟必须是p
- 后继兄弟 #pp1~p{} 之后所有的p兄弟
b) 伪元素选择器
nth-of-type(num) 匹配同类型的第num个同级兄弟,num可以为数字、关键字、公式。
数字下标从1开始,
关键字:odd 奇数 even 偶数
公式:an+b, 注意此时n为从0开始,如偶数2n,奇数2n+1
p:nth-of-type(2) 选择每个p是其父级的第二个p元素。
p:first-of-type 选择每个p是父级的第一个p元素
p:last-of-type 选择每个p是父级的最后一个p元素
nth-child(num)
p:nth-child(2) 选择每个p是其父级的第二个子元素。
p:first-child 选择每个p是父级的第一个子元素
p:last-child 选择每个p是父级的最后一个子元素
<style>
#app p:nth-of-type(3){
background-color:red
}
#app h2:nth-child(3){
background-color:yellow
}
</style>
<div id="app">
<div>
<h1>标题</h1>
<p>标题下文字。。</p>
</div>
<p id="pp1">一段话1。。。</p>
<h2>小标题</h2> -------------------- 变黄
<p>一段话2。。。</p>
<p>一段话3。。。</p>---------------- 变红
<p>一段话。。。</p>
</div>
后伪元素
p:before{ //在每个p元素之前插入内容
content:”” //既可以为文本,也可以为图片url(“”)
}
p:after{content:” ”} //在每个p元素之后插入内容
4. BFC
BFC块级格式上下文, 是一个独立的渲染区域,与世隔绝,与外部元素无关。
触发BFC:
- body根元素
- 绝对定位position: fixed/absolute
- 浮动元素:float: right/left
- 滚动:overflow: hidden/auto/scroll
- display: inline-block/flex/table-cell
BFC可以解决什么问题
- 垂直塌陷问题
<style>
.bfc{
overflow:hidden;
}
#div1{
background-color:red;
width:100px;
height:100px;
margin-bottom:100px;
}
#div2{
margin-top:100px;
background-color:yellow;
width:100px;
height:100px;
}
</style>
</head>
<div class="bfc">
<div id="div1"></div>
</div>
<div class="bfc">
<div id="div2"></div>
<div>
- 包含塌陷问题
<style>
.parent{
width:200px;
height:200px;
background-color:gray;
overflow:hidden
}
#div1{
width:100px;
height:100px;
background-color:red;
margin-top:50px; //会带动父元素
}
</style>
</head>
<body>
<div class="parent">
<div id="div1"></div>
</div>
- 包含浮动元素
<style>
.parent{
background-color:gray;
overflow:hidden
}
#div1{
width:100px;
height:100px;
background-color:red;
float:left
}
</style>
</head>
<body>
<div class="parent"> //父元素由子元素撑起,当子元素浮动时,父元素就会消失。
<div id="div1"></div>
</div>
- 使标准流元素不被浮动元素覆盖
5. 清除浮动的影响
a) 额外标签法 clear
clear:left;//清除左浮动,即float:left
clear:right;//清除右浮动,即float:right
clear:both;//清除左右浮动
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.fahter{
width: 400px;
border: 1px solid deeppink;
}
.big{
width: 200px;
height: 200px;
background: darkorange;
float: left;
}
.small{
width: 120px;
height: 120px;
background: darkmagenta;
float: left;
}
.clear{
clear:both;
}
</style>
</head>
<body>
<div class="fahter">
<div class="big">big</div>
<div class="small">small</div>
<div class="clear"></div> //额外标签法,清除前面两个div设置的浮动
</div>
</body>
b)设置父元素为BFC,清除浮动影响。
.fahter{
width: 400px;
border: 1px solid deeppink;
overflow: hidden;
}
6. 定位position
a) relative 相对定位
相对于自己现在的位置进行定位
top: 10px; 向下移动10px
left: 10px; 向右移动10px
b) absolute 绝对定位
相对于已经定位的父元素进行定位,会脱离文档流,可能与其它元素发生重叠。
父元素一般采用相对定位
top: 10px; 距离父元素顶部10px; 向下为正
left: 10px; 距离父元素左边框10px; 向右为正
如果找不到这样的父元素,就相对body进行定位。
c) fixed 固定定位
脱离文档流,相当于浏览器左上角定位
d) static 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)
e) sticky 粘性定位
relative与fixed的结合。元素在跨域特定阈值前为相对定位,跨域特定阈值后为固定定位。常用于:导航栏.
正常情况下,元素会随着页面滚动,当到达屏幕的特定位置时(设置值),如果用户继续滚动,它就会“锁定”在这个位置。
7. 设置元素居中
水平居中:
- text-align:center 文本水平居中
- margin:auto 左右均匀占用可用空间
- 父元素:{ position:relative; width: 400px;height:300px;}
子元素:{
position:absolute;
top:150px;
left:200px;
transform:translateY(-50,-50); } 先X后Y。
- display:flex;
justify-content:center;
align-items:center;
垂直居中:
- line-height: 100px 高度 文本垂直居中
- box-sizing:border-box;
padding: 99px 0;
水平垂直居中
- 只要让子容器开启绝对定位,并且left、right、top、bottom全为0,margin:auto就能实现div水平垂直居中了。
- 使用flex布局
display:flex
justify-content:center
align-items:center
- 父元素相对定位,子元素绝对定位,同时设置
top:50%
left:50%
transform:translate(-50%,-50%)
/* div在body居中 */
.center {
width: 100px;
height:100px;
background-color: #000;
position: absolute;
top:50%;
left:50%;
/* margin:-50px 0 0 -50px; */ /* 前提:必须知道div的宽度和高度 */
transform:translate(-50%,-50%) 使用transform就不需要了
}
7. CSS3 动画 animation
@keyframes 动画名{ 动画时间百分比{ CSS属性; } } | 定义动画的CSS属性。 | from,动画开始,等价于0% to, 动画结束,等价于100% |
animation-name | 动画名 | |
animation-duration | 动画完成一个周期所花费的时间 | 1s或1000ms |
animation-iteration-count | 动画迭代次数 | 数字/infinite循环 |
animation-delay | 动画延迟开始的时间 | |
animation-timing-function | 动画速度 | linear 匀速 ease 默认,动画以低速开始,然后加速,以低速结束。 ease-in 以低速开始 ease-out 以低速结束 ease-in-out 以低速开始和结束 |
animation-direction | 动画播放方向 | normal 正常 reverse 逆向 alternate 奇数次正向,偶数次逆向 alternate-reverse 偶数次正向,奇数次逆向 |
animation-play-state | 动画播放状态 | running 运行 paused 暂停 |
#app{
width:50px;
height:50px;
background:red;
border-radius:50%;
animation-name:mymove;
animation-duration:1000ms;
animation-timing-function:ease-out;
animation-delay:2s;
animation-iteration-count:2;
animation-direction:reverse;
}
#app:hover{
animation-play-state:paused;
}
@keyframes mymove{
0% {width:50px;height:50px;}
50% {width:100px;height:100px;}
100% {width:50px;height:50px;}
}
8. CSS画圆形和三角形,0.5px的线
// 三角形
#app{
width:0;
height:0;
border-width:100px;
border-style:solid;
border-color:yellow blue red transparent;
//transparent 默认值,透明色。 上右下左顺时针
}
// 圆形
#app{
width:100px;
height:100px;
background-color:red;
border-radius:50%;
}
// 画一条0.5px的线
.half-hr {
width: 300px;
height:1px;
background-color: #000;
transform: scaleY(0.5); /* 延Y轴缩小1倍 */
}
9. CSS选择器优先级
口诀:个十百千
1000: 内联样式style=””
100:ID选择器 #app
10: 类选择器、伪类选择器、属性选择器
1:标签选择器、伪元素选择器
0:通配符* 、子选择器(空格 >)、兄弟选择器(+ ~)
继承的样式没有权值
可继承样式:font-size font-family color text-indent
不可继承样式:height width margin padding border
div > #title{color: blue;} //标签+id = 100+1 = 101
div > h3#title.title{color: red;} //标签+标签+id+类 = 1+1+100+10=112
[lang=”en”] h3.title{color: green;} //属性+标签+类 = 10+1+10 =21
[lang=”en”] #title{color: gray;} //属性+id = 10+100 = 110
10. 伪类选择器与伪元素选择器区别
a)伪类选择器在c3中使用一个冒号,而伪元素选择器可使用两个冒号或一个冒号
b) 伪类选择器只是相当于给标签加上了个名字,让我们能够找到精准的位置,不改变DOM结构;
c) 伪元素选择器修改了他原本的结构,创造出了一个虚拟的内容来辅助我们找到元素的位置,就像例子的span标签一样(可能改变DOM结构,创建了虚拟的DOM)。
常见伪元素选择器:
::after
::before
::first-letter
::first-line
::selection
::placeholder
常见伪类选择器:
(1)结构伪类选择器:
:first-child
:last-child
:nth-child(n)
:nth-last-child(n)
:first-of-type
:last-of-type
:nth-of-type(n)
:nth-last-of-type(n)
:only-child
:only-of-type
:root
:empty
(2)反选伪类选择器:
父元素 (空格) :not(标签名)
(3)目标伪类选择器:
:target
(4)UI元素状态选择器:
:enabled
:disabled
:checked
(5)动态伪类选择器:
:link 鼠标点击之前
:hover 鼠标悬停
:active 鼠标点击
:visited 鼠标点击之后
(6)用户行为伪类选择器
:focus
// 设置网页除了input和textarea里面的文字不能被选中拷贝
*:not(input):not(textarea){
user-select:none;
}
11. :before VS ::before
相同点: 效果是一样的
a) 都可以用来表示伪类对象,在对象前设置某些内容
b) 与content属性配合使用
c) 伪类对象不会出现在DOM树中,不能用JS进行操作, 仅仅实现渲染层加入的
不同点:
:before是css2语法,::before是css3语法,:before兼容性更好。
12. DOM事件流
DOM (文档对象模型)是一个树形结构,当一个HTML元素产生事件时,事件会在当前元素节点与根节点之间的路径传播,路径所经过的节点都会收到该事件,这个事件传播过程称为DOM事件流。
事件流分为两类:
- 捕获型事件流(从上到下: window->document->html->body->...->某个具体元素)
- 冒泡型事件流(从下到上: 某个具体元素->...->body->html->document->window)
三个阶段
- 捕获阶段:从window对象依次向下传播,到达目标节点称为捕获阶段,该阶段不会触发任何事件
- 目标阶段:在目标节点触发事件
- 冒泡阶段:从目标节点依次向上传播,到达window对象
<div id="box1">
<div id="box2"></div>
</div>
这时点击子元素box2则,
document.getElementById("box1").onclick=function(e){
var ev=e||window.event;
console.log(ev.eventPhase); // 3 //事件所处的阶段 1捕获阶段 2目标阶段 3冒泡阶段
var target=ev.target||ev.srcElement;
console.log(target); //当前点击的DOM元素,即box2
console.log(ev.currentTarget); //真正绑定事件的元素,即box1
console.log(ev.type) //事件类型,没有on, click
}
13. link VS @import
都可以用于引入外部的CSS样式
<link href=”” rel=””/>
<style>
@import url(CSS文件路径地址);
</style>
不同:
- link是html标签,不只可以加载CSS, 还可以设定rel等连接属性;@import是CSS样式,只能加载CSS.
- link 没有兼容性问题,@import是CSS2.1之后才存在的,老式浏览器可能不支持
- link 会和页面一起加载,@import会在页面加载完成后再加载
- JS可以操作DOM, 可以通过插入link标签操作样式,@import则不行
14. 字体大小em
元素字体大小font-size 用em是相对于父元素字体的,除了这个,元素width\height\margin\padding等用em是相对于自己的字体大小的。
15. CSS3新增属性
a) border-radius 圆角
简写 border-top-left-radius border-top-right-radius border-bottom-left-radius
border-bottom-right-radius
b). box-shadow 阴影
offset-x水平阴影, 正值,阴影在右边,负值,阴影在左边
offset-y垂直阴影,正值,阴影在下边,负值,阴影在上边
blur模糊距离(值越大阴影边缘越模糊),
spread向四周扩展的阴影的尺寸
color 阴影颜色
inset/outset内部投影或外部投影
JavaScript
1. JS事件
a) 鼠标事件
onclick 点击
ondblclick 双击
onmousedown 鼠标按下
onmouseup 松开鼠标
onmouseover 鼠标进入
onmouseout 鼠标离开
onmousemove 鼠标移动
PS. ev.type的作用:因为添加一个函数,就会占用更多的内存,使用ev.type可以多个事件处理函数封装成一个函数。
box1.onmouseout=fn;
box1.onmousemove=fn;
function fn(e){
var ev=e||window.event;
switch (ev.type){
case "mouseout":
break;
case: "mousemove":
break;
}
}
b) 键盘事件
onkeydown 按下任意键触发,如果按下不动,会重复触发
onkeyup 松开键盘
onkeypress 按下字符键触发(不会触发alt, ctrl shift esc 等),如果按住不动会一直触发。
// 键盘事件一般绑定在document或者一些可以获得焦点的对象
document.onkeydown=function(e){
console.log(e.keyCode); //编码
console.log(e.ctrlKey); //是否按下ctrl键 true/false
}
c)表单事件
onfocus
onblur
oninput 输入时触发,每按下一个键,就会触发一次input事件
onchange 表单内容改变,且失去焦点时触发
onsubmit 提交表单
<form id='form1' action='url' method='get/post' omsubmit='return checkForm()'>
<input type='text' name='username' value=''/>
<input type='password' name='pwd' value =''/>
<input type='password' name='pwd2' value =''/>
<button type='submit'>提交<button/>
<form/>
function checkForm(){
if($(“#form1”).valid()){ //验证表单
// 表单验证通过
return true;
}else{
return false; 阻止表单提交
}
}
// action=”” 或action=”#” 提交到当前页面
d) 页面相关事件
onload 页面内容加载完成 window.onload
onresize
onscroll
e) JS绑定事件的三种方式:
一、行内元素绑定
<input type="button" value="按钮" id="btn" οnclick="alert(2)">
二、动态绑定
var btn = document.getElementById('btn');
btn.onclick = function(){
alert(4);
}
三、事件监听addEventListener
document.querySelector(“.a”) css选择器 单一选择器,选择第一个
document.querySelectorAll(“”) 返回一个数组,选择多个
2. 数组
a) 访问:索引
var a = [0,1,2,3,4];
索引:0 1 2 3 4
-5 -4 -3 -2 -1
二维:var a=[[1,2,3],[1,2],[5,6,7]]
a.toString() /a.join() "1,2,3,1,2,5,6,7"
a[1] //[1,2]
a[1][1] //2
a.length 属性 返回数组长度,不加括号
b) 数组常用方法
push() | 结尾插入元素 |
unshift() | 开头插入元素 |
pop() | 删除结尾元素,并返回 |
shift() | 删除开头元素,并返回 |
delete arr[index] | 将index索引处元素置为空(undefined),但不会改变数组长度. |
splice(start,num,items1,items2,…) | 在指定位置处删除/添加项目,并返回删除的元素数组,start开始位置,num要删除的元素数量,如果为0则不删除任何项目,items1可选从start开始要添加到数组中的元素(其余元素依次向后移动)。 var arr=[1,2,3] arr.splice(-1,0,"a") // [1,2,”a”,3] |
indexOf() lastIndexOf() | 从开头开始寻找,找到返回元素所在下标,找不到返回-1. |
includes() | 判断是否包含某个元素,返回ture/false |
join() toString() | 连接为字符串,默认连接字符为逗号 [1,2,3]-> “1,2,3” (相当于toString) arr=[2,3,3] arr.join(‘’) [2,3,3]->’233’ |
Slice(start,end) | 返回数组片段,包头不包尾 |
Concat(arr1,arr2) | 数组连接,不改变原数组 |
Reverse() | 逆转(注意这是数组的方法不是字符串的方法) |
Sort(func) | 按照function中的方式对数组进行排序,默认按照将值作为字符串按照字母和升序进行排序(当排序数字25和100时会出错因为“25”>”100”) |
Map(function(value,index,arr), thisvalue) | 数组中的每个元素执行func函数,并用返回值生成新数组。thisvalue 改变方法内的this指向,默认this是指向window对象的 |
array.reduce(function(accumulator, currentValue, currentIndex,array),initalValue) | 累积操作。function必须,若initalValue有值,accumulator =initalValue,currentValue=arr[0]; 若initalValue没值,accumulator=arr[0], currentValue=arr[1],currentIndex永远对应currentValue的下标,也从1开始. accumulator 表示上一次循环返回值(必须有返回值) currentValue 当前值 currentIndex 当前值下标 array 当前操作的数组 |
forEach((value,index)=>{}) | 与map使用方法类似 |
fill(value,start,end) | 给数组填充固定值,start开始下标,默认为0,end结束下标,默认为arr.length(不包含) var arr1 = new Array(5) console.log(arr1.fill(1)); var arr3 = [0, 1, 2, 3, 4, 5, 6] console.log (arr3.fill(1, 3)); //[0,1,2,1,1,1,1] 也可用于替换值, 改变源数组, 包头不包尾 可以视为替换。 |
改变源数组的方法:pop push shift unshift reverse sort splice fill
不改变源数组的方法: concat replace split join filter
数组元素替换:
arr.splice(0,1,"a") 将0下标元素,替换为“a”
arr.fill(10,0,1) 将0下标元素,替换为10
常见数组操作:
c) 排序 sort
升序:points.sort(function(a, b){return a-b});
降序:points.sort(function(a, b){return b-a});
d)map
var a=Array(100).join(",").split(",").map(function(items,index){
console.log(index+":"+items);
})
var obj={
name:"xiaoming",
age:20
}
var arr=[1,2,3,4];
arr.map(function(value,index,arr){
// this.name="xiaoming111"; //obj对象的name也会改变
console.log(this); // obj对象
},obj);
e) 合并数组,不改变源数组
var a=[…arr1,…arr2]; var b=[].concat(arr1,arr2);
f)申请二维数组,20行10列
console.log(new Array(20).fill(new Array(10).fill(0)));
有问题,arr[0][1] 纵坐标会一起改变,fill返回的是对象的引用
Array.from(new Array(row),() => new Array(col).fill(0))
Array.from(arrayLike,fun,thisArg)
--arrayLike:必选,想要转换成数组的伪数组对象或可迭代对象
--fun:可选,如果指定了该参数,新数组中的每个元素会执行该函数
--thisArg:可选,执行回调函数mapFn时this对象, 类似于map里的thisvalue
可迭代的对象包括ES6新增的数据结构Set和Map
g)数组去重
function quchong1(arr){
return arr.reduce(function(accumalator,currentValue,currentIndex,array){
if (accumalator.includes(currentValue)){ // 注意reduce每次循环必须有返回值,因为acc等于上次循环的返回值。
return accumalator
}else{
return accumalator.concat(currentValue); //必须是concat数组连接,不能用push
}
},[]);
}
function quchong2(arr){
var newarr=[];
arr.forEach((value,index)=>{
if(!newarr.includes(value)){
newarr.push(value);
}
});
return newarr;
}
function quchong3(arr){
return Array.from(new Set(arr));
}
function quchong4(arr){
return arr.filter(function(value,index,arr){
return arr.indexOf(value) = = = index; //返回true/false
});
}
h)数组降维,将多维数组转为一维
arr=[[1,2,3],[1,2,3]];
方法一: arr.flat() //[1,2,3,1,2,3] flat(n) n为扁平化深度
arr.flat(Infinity); //Infinity 无穷
方法二:str=arr.toString(); str.split(",");
方法三:str=JSON.stringify(arr); str.match(/\d+/g);
function myFlat(arr,n){
if(n==0){
return arr;
}
return arr.reduce(function(acc,value){
if(Array.isArray(value)){
acc=acc.concat(myFlat(value,n-1)); //concat 不会改源数组
}else{
acc=acc.concat(value);
}
return acc;
},[]);
}
i)统计字符串中每个元素出现次数
function count(s){
var arr=s.split("");
return arr.reduce(function(acc,current){
if(acc[current]!=undefined){
acc[current]++;
}else{
acc[current]=1;
}
return acc;
},{})
}
var a=Array(100).join(",").split(",").map(function(items,index){
console.log(index+":"+items);
})
3. 字符串 String
a)访问:索引
下标从0开始,没有负值索引(-1)
.length 获取长度
b) 方法
charAt(index) | 返回指定索引处字符,相当于str[index] |
charCodeAt(index) fromCharCode() | 返回指定索引处字符的Unicode编码 将Unicode编码转为字符串 |
indexOf(str) lastIndexOf(str) | 返回下标 |
slice(start,end) substring(start,end) substr(start,length) | 包头不包尾,不改变原数组 包头不包尾,不改变原数组 不改变原数组 |
startsWith(str) endsWith(str) includes(str) | 返回true/false |
toUpperCase() toLowerCase() | 小写—>大写 大写—>小写 |
split(seg) | 转为字符数组 "1 2 3 4" -> ["1", "2", "3", "4"] |
trim() | 移除头尾空格 |
match(regexp) | 匹配正则表达式。 a.match(/\d+/g) /g全局匹配,返回匹配的字符数组 ["1", "3", "4"] b=a.match(/\d+/) 只返回第一个匹配 b.index var a="*1*"; a.match(/[*]+/g) |
search(regexp) | 返回第一个匹配的下标,找不到返回-1 |
replace(old,new) replaceAll(old,new) | 不改变源字符串 |
concat(str1,str2) | 字符连接,不改变源字符串,+会改变源字符串 |
repeat(n) | “aa”.repeat(2); //”aaaa” |
PS. 标红的为数组和字符串共有的方法
c)字符串 数字转化
"123.222"-'0' // 123.222
parseInt("1234blue"); //returns 1234
parseInt("0xA"); //returns 10
parseInt("22.5"); //returns 22
parseInt("blue"); //returns NaN
false-0 // 0 false+0 // 0
true-0 // 1 true+0 // 1
4. 正则表达式
一般形式:模式+量词,如果模式后面没有加量词,就是匹配一次。
a)模式
[abc]—— 匹配a或b或c
[^abc]—— ^取反,不是a不是b不是c
[a-zA-Z] ——[]表示范围,匹配a-zA-Z范围内任一字母
预定义类 | 含义 | 等价于 |
. | 匹配除回车和换行之外的任意单个字符 | [^\r\n] |
\d | 数字 | [0-9] |
\D | 非数字 | [^0-9] |
\w | 数字、字母、下划线 | [0-9a-zA-Z_] |
\W | 非数字、字母、下划线 | [^0-9a-zA-Z_] |
\s | 空白符 | [\t\n\x0B\f\r] |
\S | 非空白符 |
边界匹配:
^开始
$结束
\b 单词边界
\B 非单词边界
b) 量词
? | 0次或1次 |
+ | 1次或多次 |
* | 0次或多次 |
{n} | 匹配n次 |
{n,m} | 匹配最少n次,最多m次 |
{n,} | 最少匹配n次 |
c) 贪婪模式和非贪婪模式
正则表达式,默认是贪婪模式的,即尽可能多得匹配
可以在量词后加?,改为非贪婪模式
"12345678".replace(/\d{3,6}/g, "X"); //控制台输出"X78",即贪婪模式
"12345678".replace(/\d{3,6}?/g, "X"); //控制台输出"XX78",非贪婪模式
d)分组
使用()进行分组,会自动为分组进行编号,从1开始,可以使用$1,$2,..来捕获分组。
如果不希望捕获某些分组,只需要在分组内加上(?: )
"a1b2c3d4".replace(/([a-z]\d){3}/g, "X"); //控制台输出"Xd4"
"2017-12-25".replace(/(\d{4})-(\d{2})-(\d{2})/g, "$2/$3/$1"); // "12/25/2017"
"2017-12-25".replace(/(\d{4})-(?:\d{2})-(\d{2})/g, "$2/$3/$1"); // "25/$3/2017"
\n 代表匹配第n个分组 n从1开始
/([0-5]6\1)/g 060 565
e) 向后查找与向前查找,也被称为断言
肯定式向后查找(?<= xxx)必须匹配但不在结果中返回的模式
去匹配到xxx的字符的前面找。(字符串的字符顺序肯定与正则式一致的)
肯定式向前查找(?= xxx)
否定式向前查找(?! xxx)
否定式向后查找(?<! xxx)
练习:求URL= https://music.163.com/#/discover/toplist?id=3778678 的域名?
var str=” https://music.163.com/#/discover/toplist?id=3778678”;
首先,匹配开始//和结束/
str.match(/\/\/.*\//g) ["//music.163.com/#/discover/"] 匹配特殊符号,使用\转义
改为非贪婪模式?
str.match(/\/\/.*?\//g) ["//music.163.com/"]
使用向前查找、向后查找过滤域名前后的斜杠
str.match(/(?>=\/\/).*?(?=\/)/g) ["music.163.com"]
var url = "http://www.baidu.com " +
"https://google.com " +
http://taobao.com
console.log(url.match(/\w+(?=:)/g)); ["http", "https", "http"]
var jg = "iphone:$88.88 " +
"oppo: $10.23" +
"honor:$25.56";
console.log(jg.match(/(?<=\$)[0-9.]+/g)) ["88.88", "10.23", "25.56"] 去它的后面找
千分分隔:
"1000000".replace(/\d(?=(\d{3})+$)/g,'$&,') $&匹配所有分组结果
f) new RegExp() 给正则表达式添加变量
new RegExp(patter,flag)
Patter: 字符串,开头结尾不需要加/…/, 注意转义 \\d
Flag: “g” 全局, “i” 忽略大小写,m多行
"a1a2z3".match(/a\d/g) 等价于=>
var a="a"
"a1a2z3".match(new RegExp(a+"\\d+","g"))
['a1', 'a2']
function strcount(str,s){ //计算字符串中某个字符一共出现多少次
let array = str.match(new RegExp(s,"g"));
let count = array ? array.length: 0;
console.log(s,count)
return count;
}
5. 对象
a) 方法
obj[key]=value | 新增/修改 |
delete(obj[key]) | 删除成功返回ture, 失败返回false |
for (let key in obj) | 遍历对象 |
‘name’ in obj | 判断某个属性是否在对象实例或原型中 |
Object.hasOwnProperty(‘name’) | 判段某个属性是否在对象实例中 |
Object.keys(obj) | 返回key值组成的数组 |
Object.values(obj) | 返回value值组成的数组 |
Object.getOwnPropertyNames(obj) | 同Object.keys(obj) |
Object.definePropery(obj,key,descriptor) | 为对象定义属性,默认不可删除或修改 |
PS. Object.getOwnPropertyNames(obj) 得到对象的所有key值组成的数组,不能返回Symbol的属性,需要用Object.getOwnPropertySymbols()
b) 创建对象的三种方式
1. new Object()
var obj=new Object();
obj.name="aaa";
obj.say=function(){
console.log(this.name);
}
2. 构造器
function People(name,fn){
this.name=name;
this.fn=function(){
console.log(this.name);
};
}
var obj2=new People("aaa");
3. 字面量
var obj={
name:”黎明”,
say:function(){
console.log(this.name); //黎明
}
}
obj.constructor.prototype.name="黑土"
obj.constructor.prototype.say2=function(){
console.log(this.name) //黎明
}
// 注意prototype 是给构造器用的
// obj.__proto__===obj.constructor.prototype true
//先去实例属性和方法找,找不到,就去原型中找,再去原型的原型中找,如果最后也没有找到,
如果是属性返回undefined, 是方法就报错。
// 创建对象的方法
// 1.字面量
var obj1={
name:"aaa",
age:20,
say(){
console.log(this.name);
},
sayA:function(){
console.log(this.age);
}
}
// obj1.say();
// obj1.sayA();
//2.构造器
function Person(name,age){
this.nnm=name;
this.age=age;
this.say=function(){
console.log(this.nnm);
}
}
var obj2=new Person("ccc",10);
obj2.say();
var obj3=Person("bbb",10);
console.log(obj3); //undefined
console.log(window.nnm); //bbb
// 构造器函数如果不用new调用, 返回的就是undefined,并不是对象实列,而参数会被加到window全局对象上。
// 作用域安全的构造函数
function Person(name,age){
if(!(this instanceof Person)){
return new Person(name,age);
}
this.nnm=name;
this.age=age;
this.say=function(){
console.log(this.nnm);
}
}
//3.new Object
var obj4=new Object();
obj4[name]="emm";
obj4[age]=10;
obj4.say=function(){
console.log(this.name);
}
console.log(obj4);
c) Object.defineProperty
Object.defineProperty(obj, key, descriptor) 在一个对象上,定义一个新的属性,或修改整个属性。默认使用defineProperty定义的属性是不可删除的
descriptor:描述符对象,具有以下几个参数:
configurable: 仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false
enumerable: 仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false
value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
writable: 仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false
get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。undefined
set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。
d)对象的增删改查
var obj= {0: "aa", 1: "cc", 2: "dd"}
注意:obj.1="mm" 会报错,只能是 obj["1"]="mm"
obj[key]=value 如果key已经存在则覆盖,不存在则新增
obj[key] 返回value值
遍历:for(let key in obj){ console.log(key+":"+obj[key]) }
删除:delete(obj["0"]) // { 1: "cc", 2: "dd"}
PS. JS 对象的键名只能是字符串,而ES6中Map类型就是为了弥补这个问题 。
d) 判断对象类型
typeof typeof null/[]/{}/Date->object 开头小写
instanceof 判断对象是否在目标对象的原型链上,不能判断字面量
null instanceof Object ——>false
undefined instanceof Object ——>false
// instanceof 底层实现代码
d._proto_===Date.prototype true 对象.__proto__ 构造器.prototype
function myInstrance(L,R){
RP=R.prototype; //是一个对象,原型对象
LP=L.__proto__;
while(true){
if(LP==null){
return false;
}
if(RP == LP){
return true;
}
LP=LP.__proto__;
}
obj.constructor.name ——>Array Date Function Object RegExp开头大写
Object.prototype.toString.call(123)——> "[object Number]"
6. Map
new Map() —— 创建 map。
map.set(key, value) —— 根据键存储值。
map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined。
map.has(key) —— 如果 key 存在则返回 true,否则返回 false。
map.delete(key) —— 删除指定键的值。
map.clear() —— 清空 map。
map.size —— 返回当前元素个数。
迭代
map.keys //获得所有的键
map.values//获得所有的值
map.entries//获取所有的map键值对
map.forEach(fun(value,key,map){})//这也是一种迭代
obj与map转化:
obj->map new Map(Object.entries(obj))
map->obj Object.fromEntries(map.entries());
普通对象与map的区别:普通对象key只能使用字符串,map可以使用任意类型作为key(NaN也可为键值)
7. Set
new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
set.add(value) —— 添加一个值,返回 set 本身
set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
set.clear() —— 清空
set.size —— 返回元素个数
map与set迭代时按照值插入的顺序进行的
WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。
WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。
8. 类型转换
0 null undefined ‘’空字符串 ->false
[] {} ->true
[]==[] // false 对象判断的是地址
null==false //false, if(null)相当于if(false)
undefined==false //false
NaN==NaN //false NaN表示的是一个范围,不是数字
“”==false //true
“0”==false //true
0==false //true
[]==false //true
0===0.0 //true
Infinity===Infinity //ture
{}==false //Uncaught SyntaxError: Unexpected token '=='
typeof NaN //number
typeof Infinity //number
null===null true
null==undefined true null===undefined false
undefined===undefined true
obj = new String("0"); obj==false true
9. JS 继承
a) 原型链继承
Child.prototype=new Parent();
特点:
优点:会继承父类所有的实例的属性与方法和原型的属性与方法
缺点:创建子类实例时无法向父类构造器传参;无法实现多继承;
b) 构造器继承
parent.call(this,arg1,arg2,…) /apply(this,[argsArr])
只能继承父类实例属性与方法,不能继承父类原型属性与方法;
可以实现多继承;可以在创建子类对象时向父类构造器传参
每个子类都有一个父类构造器副本,浪费性能
c) 组合继承
原型链继承+构造器继承,会调用两遍父类构造器,影响性能。
d) Parent.call()
Child.prototype=Parent.prototype
子类原型于父类原型指向同一对象(constructor指向也相同)
e) Parent.call()
Child.prototype=Object.create(Parent.prototype);
Child.prototype.construstor=Child;
10. 基本数据类型
JS 六个基本数据类型:String Number Boolean Undefined Null Symbol
引用类型:Object Array Date Function RegExp
Symbol :ES6 新增,创建一个全局独一无二的值,这个值不是字符串
var a1=Symbol(‘aa’) ,aa用于Symbol描述,与Symbol的值无关
最大安全整数 Number.MAX_SAFE_INTEGER 2^53-1
最小安全整数 Number.MIN_SAFE_INTEGER –(2^53-1)
JS 中所有的Number都是由双精度64位存储
11. Array
Array.from() 将伪数组对象或可迭代对象,转为数组 , 比如set arguments “foo”
Array.isArray()
12. this的指向
在事件中,this指向当前dom元素,即this===e.target // true
在函数里,this指向函数的调用对象(全局函数默认是全局对象window调用的)
对象方法里,this指向当前对象
inner = 0
function say(){
console.log(inner) 、
console.log(this.inner)
}
let obj1 = {
inner:'1-1',
say(){
let inner = '1-2'
console.log(inner)
console.log(this.inner)
}
}
let obj2 = {
inner:'2-1',
say(){
let inner = '2-2'
console.log(inner)
console.log(this.inner)
}
}
say() // 0 0
obj1.say() //1-2 1-1
obj2.say() // 2-2 2-1
obj1.say = say
obj1.say() // 0 1-1 先局部找,局部找不到,全局找,如果都找不到则报错。
obj2.say = obj1.say
obj2.say() //0 2-1
13. call apply bind
都是原型对象上的默认方法, 改变this指向,必须用函数调用
Parent.call() Parent为构造器函数
fun.call(obj,arg1,arg2,...) 用于改变fun函数内this的指向,指向第一个参数,后续为向fun传递的参数
fun.apply(obj,[argsArr]) 用法同call ,只是传参方式不同
fun.bind(obj,arg1,arg2,...) () 返回值是一个函数,必须进行调用才会执行
var obj={
age:10,
name:"xiaoming",
say:function(place,to){
console.log(this.name+"的年龄是"+this.age+",来自"+place+",去往"+to);
}
}
var obj2={
age:20,
name:"honghong",
}
obj.say("英国","中国"); //this指向obj
obj.say.call(obj2,"美国","日本") //this指向obj2
obj.say.apply(obj2,["美国","瑞典"])
obj.say.bind(obj2,"美国","西班牙")();
14. 浅拷贝 VS 深拷贝
浅拷贝 :
obj2=Object.assign(obj); 对obj2的修改会影响obj
obj2=Object.create(obj); 以obj1为原型创建obj2
1.循环
function shallowCopy(obj){
var obj2={};
for(let key in obj){
obj2[key]=obj[key];
}
return obj2;
}
2.var obj_copy=Object.assign({},obj);
3.var obj_copy={...obj};
深拷贝:obj2=JSON.parse(JSON.stringify(obj));
// 1. 循环+递归
加上 数组判断,循环引用判断。
function deepCopy(obj,hash=[]){
if (typeof obj=="object"){
if(hash.indexOf(obj)>-1) return {};
hash.push(obj);
var obj2=Array.isArray(obj)?[]:{};
for(let key in obj){
if(typeof obj[key] == "object"){
obj2[key]=deepCopy(obj[key],hash);
}else{
obj2[key]=obj[key];
}
}
return obj2;
}
return obj;
}
2. JSON 字符串
function deepCopy2(obj){
try{
var obj2=JSON.parse(JSON.stringify(obj));
return obj2;
}catch(e){
return e.message.indexOf("circular")>-1?"exists circular":"error";
}
}
15. 判断是否存在循环引用(面试题)
function isCircular(obj,hash=[]){
if(Object.prototype.toString.call(obj)!==”[object Object]”) return false;
if(hash.indexOf(obj)>-1) return true;
hash.push(obj);
for(let key in obj){
if(Object.prototype.toString.call(obj[key])===”[object Object]”){
if(obj[key]===obj || isCircular(obj[key],hash)){
return true; //有循环引用
}
}
}
return false; }
16. new 一个对象的原理
- 创建一个新对象。
- 将对象的__proto__赋值为函数的prototype
- 修改this的指向,将this指向当前新对象,执行构造器方法,得到返回结果
- 判断返回结果是否也是一个对象,如果是返回改结果,否则返回新对象(空对象)
function myNew(fn,...args){
var obj={};
obj.__proto__=fn.prototype;
var result=fn.apply(obj,args);
return result instanceof Object ? result :obj;
}
17. 闭包
闭包是什么?
- 一个函数中嵌套一个函数
- 内部函数可以访问外部函数的参数和变量
- 并且内部函数使用外部函数的参数和变量,不会被垃圾回收机制回收,可能会造成内存泄露,解决办法就是在使用完之后手动为其赋值为null。
好处:实现封装,避免命名冲突;在内存中维持一个变量,以用作缓存,匿名自执行函数可以减少内存消耗
作用:可以作为返回值,也可以作为参数
// 闭包作为返回值
function fn(){
var num=3;
return function(){
var n=0;
console.log(++num);
console.log(++n);
}
}
var fn1=fn();
fn1(); // 4 1
fn1(); // 5 1 num最终没有被垃圾回收机制回收
// 闭包作为参数
var num=10;
function fn1(x){
if (x>num){
console.log(x);
}
}
void function(fn){ //匿名自执行函数
var num=100;
fn(30);
}(fn1) //30,fn1执行使用的是自己作用域下可以访问到的num=10
18. 防抖与节流
防抖:在1s内再次出发滚动事件,则清空计时器,重新计时;在1s内没有触发事件,则执行函数
节流:如果在短时间内大量触发该事件,在函数执行一次后,时间期限内该函数不再生效(冷却时间),过了这段时间才生效
防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行一次
//防抖函数:在某个时间期限内,事件处理函数只执行一次
var div=document.getElementById("pp");
function showTop(){
var top=document.body.scrollTop || document.documentElement.scrollTop;
div.innerHTML="top:"+top;
}
window.onscroll=debounce(showTop,1000);
function debounce(fn,delay){
var timer=null; //使用闭包,timer不会被垃圾回收
return function(){
if(timer){
clearTimeout(timer);
timer=setTimeout(fn,delay);
}else{
timer=setTimeout(fn,delay);
}
}
}
// 节流
function throttle(fn,delay){
var volid=true;
return function(){
if(volid==false){
return false; //函数不生效
}
volid=false;
setTimeout(()=>{
fn();
volid=true;
},delay);
}
}
function throttle2(fn,delay){
var timer=null;
return function(){
if(!timer){ // 如果定时器存在,表示函数还在冷却
timer=setTimeout(()=>{
fn();
timer=null;
},delay);
}
}
}
// 时间戳的方式实现节流函数
function throttle3(fn,delay){
var pre=Date.now();
return function(){
var now=Date.now();
if(now-pre>=delay){
fn()
pre=Date.now();
}
}
}
节流函数可用于实现实时搜索功能(面试题)
input.onchange 内容改变,且输入框失去焦点时触发
input.oninput 输入内容时触发
var dd=document.getElementById("input1");
dd.oninput=throttle(search,1000);
function search(){
alert(dd.value)
$.ajax({
url:'',
type:'get',
data:dd.value,
success:function(){},
error:function(){}
})
}
19.函数柯里化
将一个接收多个参数的函数,变成接受一个参数(原参数列表的第一个参数),并返回一个接受剩余参数的函数,且可以结算结果的技术
特点:函数变成单一参数,单一返回,不关注其它冗余参数,便于讨论与优化
应用:参数复用,提前判断,延迟加载
function sum(a,b,c,d){
return a+b+c+d;
}
function currying(fn,...args){
return function(...rest){
var allargs=args.slice(0);
allargs.push(...rest);
if(allargs.length<fn.length){
// return carrying.call(this,fn,...allargs);
return currying(fn,...allargs);
}else{
// return fn.apply(this,allargs); this指向window
return fn(...allargs);
}
}
}
var carryingSum=currying(sum);
console.log(carryingSum(1)(2,3,4))
20. 普通函数与箭头函数区别
- 箭头函数是普通函数的简写形式。箭头函数都是匿名函数。let a=()=>{};
- 箭头函数不能用作构造器
- 普通函数的this总是指向调用它的对象,当用作构造器时,指向当前对象实例;箭头函数this总是捕获它所在的上下文this,且任何方法都不能改变他的指向(call apply)
- 箭头函数没有arguments对象,可以用...获取参数列表
- 箭头函数没有原型,没有super
21. JS 性能优化的方法
- 避免多次修改DOM或CSS,尽量集中处理,减少重绘或重排的次数
- 使用抖动与节流,避免事件处理函数频繁触发
- 事件委托
- 闭包中的对象进行手动清除(不会被垃圾回收机制回收)
- 尽可能得合并脚本,页面中的script标签越少,加载也就越快,响应也就越迅速
- <script> defer和async属性