很多时候我们都会把ES6这个大兄弟加入我们的技术栈中。但是ES6那么多那么多特性,我们需要全部都掌握吗?秉着二八原则,掌握好常用的,有用的, 这个可以让我们快速起飞。接下来我们就聊聊ES6那些新特性吧
1.变量的声明 let,const我们都知道在ES6之前,var关键字声明变量。无论声明在何处,都会被视为在函数的最顶部(不再函数中的我们视为在全局作用域的最顶部)这就是函数变量的提升。例如:
function a(){
console.log(test);
var test = "helloword"
console.log(test);
}
a();
其实以上的代码实际上是:
function a(){
var test //变量的提升
console.log(test)//输出undefined因为test还没被赋值
test = "helloword"
console.log(test)//输出helloword
}
接下来ES6中的主角登场:
我们通常用的是let 和const 来声明变量,let表示变量,const表示常量。let 和const都是块级作用域。怎么理解块级作用域呢?
·在一个函数内部 ?
·在一个代码块内部 ?
说白了其实就是在 <strong>{}大括号</strong> 的代码块即为let 和const的作用域
接下来看下面的一段代码
function f(){
if(true){
let i = 4;
}
console.log(i); //在此处的i是访问不到的
}
let的作用域是在当前的代码块,而且不会被提升到当前函数的最顶部。
在来看下const
const也是ES6中新增的是用来声明常量的,用const声明的变量有两大主要特性:
1.const声明的是常量赋值后就不能被修改了。2.const也能形成块级作用域。看一下一段代码:
const a = 3;
a = 1;//这样写就出错了const是常量是不能被修改的
<em style = "color='yellow'">我们再来看一道比较经典的面试题</em>
var arr = []
for (var i = 0; i < 10; i++)
{
arr.push(function()
{
console.log(i)
})
}
arr.forEach(function(arr) {
arr()
})
这道题的最后结果输出来的是10个10。
但是我们的初衷是想输出来的是0到9.
没有ES6之前的时候可以永闭包解决
代码如下:var arr = [] for (var i = 0; i < 10; i++) { arr.push((function() //在此处用立即执行函数形成个闭包 { console.log(i) }))(i) } arr.forEach(function(arr) { arr()xue })
<p > 在有了let之后只需要把for循环中定义的 var i=0 ; 改成 let i = 0;就和使用闭包 达到相同的效果,ES6简洁的解决方案是不是更让你心动!</p>
2.新增的箭头函数以及函数默认参数
<hr>
1.函数的默认参数
在es5中我们是如何定义函数的默认参数的呢
```javascript
funtion action(n){
n = n || 99;
//当传入n时,n就是为传入的值
//当没有传入参数的时候,n就默认为99
return n;
}
但是仔细观察发现是有漏洞的当我们传入的参数n = 0 的时候 默认的就是false ,此时的n = 200;与我们实际想要的效果明显就是不一样的。
在es6中就为参数提供了默认值。再定义函数的时候便初始化了这个参数,以便在参数没有传递进去的时候使用。
funtion action(n =99){
console.log(n)
}
action()//200
action(300)//300
2.箭头函数
ES6中很有意思的一部分就是函数的快捷写法,也就是箭头函数。
箭头函数有最直观的3个特点:
· 不需要function 关键字来创建函数
·省略return关键字
· 继承当前上下文的this关键字
example
[0,9,8,7].map(x=>x +1)
//等同于
[0,9,8,7].map((function(x){
return x+1
}).bind(this))
说个箭头函数的写法注意点
当你的函数只有一个参数的时候,是可以省略掉括号的。当你的函数返回只有一个表达式的时候可以省略掉{};例如:
var people name =>'I am' + name
//原来写法是{return 'I am' + name},只有一个表达式{}return 都可以省略了
3.变量的解构赋值
<hr>
(1)数组的解构赋值
基本用法
在ES6 中允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构以前,为变量赋值,只能直接指定值。
在没有Es6之前给变量赋值经常是指定赋值
var x = 33;
var y = 44;
var z = 55
在ES6中允许使用以下方式赋值
var [x,y,z] = [33,44,55]
上面的一段代码表示,可以从数组中提取值,然后按照对应的位置依次进行变量赋值,在本质上也就是模式匹配只要是等号两边的模式一样,等号左边的变量就会被赋值对应的值。
但是如果解构不成功,变量的值就是undefined,如下:
var [a] = [];//a就是undefined;
var [a ,b] =[1]//同样a的值是1,b 的值就是undefiend;
以上的两个例子都是属于不完全解构,但可以成功。
<strong> 如果等号右边的不是数组,严格的来说就是不可以遍历的结构,解构赋值的过程中都会报错。</strong>
//以下都是会报错的Example
var [a] = 1;
let [a] = false;
let [a] = NaN;
let [a] = undefined;
let [a] = null;
let [a] = {};
上面的赋值会报错的主要原因是,前五个表达式在转化为对象后不具备(Iterator)接口,(最后一个表达式)要么本身就不具备(Iterator)接口。
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。在这里就不做详细说明了。
(2) 对象的解构赋值
解构赋值不仅适用于数组,还可以适用于对象如下:
let {name,age} = {name:"aaa",age:12}
name//"aaa"
age//12;
对象与数组的解构赋值有个本质的不同,数组的元素是按照次序排列的,对象中的属性排列是没有次序的,要想赋上值就必须保证,属性名必须保持一致才能获取的到值。
let {age,name} = {name:"aaa",age:12}
name//"aaa"
age//12;
let{foo} ={name:"aaa",age:12}
foo//undefined
上面的这个例子表示等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。
<strong>(3)对于属性名字不一样的必须写成下面这种形式</strong>
let{foo:baz} = {foo:"hello",baz:444}
baz//"hello";
foo//is not defined
let obj{name:"jack",age:22}
let{name:a,age:g}=obj
a//"jack"
g//22
这实际上说明,对象的解构赋值是下面形式的简写。
let { name: a, age: g } = { name: "jack", age: 22 };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { name:a } = { name: "jack", age: 22 };
a//"jack"
name // error: name is not defined
上面代码中,name是匹配的模式,a才是变量。真正被赋值的是变量a,而不是模式name。
(4)字符串的解构赋值
字符串也是可以解构赋值的这是因为,字符串再被解构赋值的时候就形成了一个类似数组的对象。
let [a,b,c,d,e,f]="goudan"
a//g
b//o
...
f//n
类似数组的对象都有 一个length属性,也可以给length赋值
let {length:len}="goudan"
len//6
<br>
4.数组的扩展
<hr>
在Es6中新增的对数组操作的方法主要有以下几种
(1)扩展运算符
扩展运算符 (spread)是三个(···)。
1,该运算符在函数调用中用的比较多,
function add(a,b){
return a+b;
}
let num = [22,33];
add(...num)//55
add(...numbers)就是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
2,扩展运算符可以替代函数中的apply
由于扩展运算符可以展开数组,所以不需要apply方法,将数组转化为函数的参数了。
下面是扩展运算符取代apply的一个实际的例子,应用Math.max()方法,简化求出一个数组的最大元素的写法 。//ES5中 Math.max.apply(null,[3,2,5]) //在es6中的写法 Math.max(...[3,2,5]) 等同于Math.max(3,2,5);
由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。
3,扩展运算符也可以复制对象
作用是用于取出对象中所有可以遍历的属性,拷贝到当前对象中,也是浅拷贝
var obj = {name:"feng",color:["yellow","blue"]};
var obj1 ={...obj}
obj1.color.push("red")
console.log(obj)//{ name: 'feng', color: [ 'yellow', 'blue', 'red' ] }
//在obj1中color中新增加一个red发现obj中color中也多了一个red ... 也是浅拷贝。
(2)Array.from()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象,以及可以遍历的对象。其中包括ES6中新增的数据结构(Set 和 Map)
接下来列举一个类数组对象,Array.from()将它转化为真正的数组。
let arr = {
'0':"a",
'1':"b",
'2':"c",
'3':"d",
length:4
};
console.log(Array.isArray(Array.from(arr)))//true
我们在运用的时候,常见的类似数组的对象是DoM 操作返回的无序集合,以及在函数内部的arguments对象,Array.from(),以及扩展运算符(...)都可以把他们转化真正的数组。
/Dom获的集合
let lis =document.getElementsByTagName("li")
console.log(Array.isArray(Array.from(lis)))//true
//以及函数中的arguments对象
!function a(){
var arr = Array.from(arguments);
console.log(arr)//[1,2]
}(1,2)
上面两段代码中,通过Documents,以及函数中arguments方法返回的都是一个类似数组的对象,通过使用Array.from方法将其转化为真正的数组。
另外除了Array.from,我们上面讲的数组中的扩展运算符(···)也是可以将某些数据类型转化为数组。
//arguments对象
!function(){
const arr = [...arguments];
console.log(arr)
}(2,3)//[2,3]
//Dom返回的对象
console.log(Array.isArray([...document.getElementsByTagName("li")]))//true
扩展运算符所使用的是遍历器接口(Iterator),如果一个对象没有这个接口,就无法转化。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。
Array.from({length:4});
//[undefined,undefined,undefined,undefined]
这段代码中只有length属性,Array.from可以将其转换为4个Undefined,而通过扩展运算符就没有办法转换。
除此之外,Array.from还可以接受第二个参数,类似于map方法可以对数组中的每个元素进行处理然后将处理后的值放到数组中。
var arr = [2,5,6,7]
console.log(Array.from(arr,item=>item*item));//[4,25,36,49]
// 等同于一下map的写法
console.log(arr.map(item=>item*item));//[4,25,36,49]
<p style="text-indent:2em;">由此看来Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。
</p>
(3)Array.of()
Array.of主要是用于将一组值转化为数组
Array.of的出现主要是对Array创建数组的不足进行的补充,因为用Array创建数组,因为参数个数的不同,会造成差异。
如下:
Array()//[]
Array(3)//[,,,]
Array(1,2,3)//[1,2,3]
//当Array()中的参数只有一个的时候,
此时的参数就代表了数组的长度,只有
参数大于等于2的时候才会返回,参数组成的新数组。
//Array.of
console.log(Array.of())//[]
console.log(Array.of(3))//[3]
console.log(Array.of(3).length)//1
//而Array无论参数有几个最后都会返参数组成的数组。
Array.of功能的模拟实现如下
function arrayof{
return Array.slice.call(arguments);
}
(4)find(),以及findIndex()
find()方法与findIndex()最主要的区别:
find() 用于找到第一个符合条件的成员,并且它的参数是一个回调函数,当没有找到时就会返回undefined。
var arr = [1,-2,4]
console.log(arr.find(a =>(a<0)));//-2
console.log(arr.find(a =>(a>5)));//undefined
findIndex()用于找到第一个符合条件成员的位置,如果没有符合条件的成员就会返回-1;
var arr =[2,3,4]
console.log(
arr.findIndex(item=>{
return item>1;
}));//0
console.log(
arr.findIndex(item=>{
return item>6;
}));//-1
findIndex() 还有一个特殊之处就是,它可以借助Object.is发现数组中的NaN
console.log([NaN].findIndex(a =>Object.is(NaN,a)));//0
(5)fill()方法
fill()的主要用法是用特定的值,来填充一个数组;
所以用fill()初始化一个数组是非常方便的
console.log([1,2,3].fill(4))//[4,4,4]
console.log(Array(4).fill(7));//[7,7,7,7]
fill()是 可以接收三个参数的,其中第一个参数是来确定需要填充的值,后两个参数是填充的起始位置以及结束位置。
console.log([2,3,5,6,8].fill(9,1,3));//[ 2, 9, 9, 6, 8 ]
fill()有一个注意细节就是,如果填充的类型是一个对象,那么别赋值的是同一个内存地址,所以用fill()实现的是浅拷贝。
let obj = new Array(2).fill({age:12})
console.log(obj)
obj[0].age = 13
console.log(obj)
//[ { age: 12 }, { age: 12 } ]
//[ { age: 13 }, { age: 13 } ]
(6)includes()
includes()方法的主要作用是为了确定数组是否有特定的值,返回的结果是一个Boolean值
var arr = [3,4,7,5]
console.log(arr.includes(3),
arr.includes(8));//true false
在没有includes()方法之前我们使用indexOf方法来确定数组中有没有特定的值。但是indexOf有两个致命的缺点:
一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。
二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。includes()使用的是不同的判断方法.
console.log([NaN].indexOf(NaN))
//-1
console.log([NaN].includes(NaN))//true
5.ES6中对象的扩展
<hr>
(1)对象中属性的简写
如果属性名,和属性值是名字相同,且属性值是一个变量名则直接可以简写为只有属性名即可如下:
var name = "xiaoqiang";
var age = 20;
var obj = {
name:name,
age:age,
say:function(){
console.log(`I am ${this.name} ${this.age} years old`)
}
}
obj.say();//I am xiaoqiang 20 years old
由于其中的属性名和属性值一样就可以简写为如下,其中的函数也可以简写代码如下:
var name = "xiaoqiang";
var age = 20;
var obj = {
name,
age,
say(){ console.log(`I am ${this.name} ${this.age}years old`) }
}
obj.say();//I am xiaoqiang 20 years old
(2)新增的Object.assign()
Object.assign()需要注意的细节比较多如下:
1.Object.assign()的第一个参数是目标对象,assign 这个方法会把其他参数中的属性全部加到第一个参数身上。
let obj = {}
let obj2 = {
name:"xiaofei",
age:12
}
Object.assign(obj,obj2)
console.log(obj)//{ name: 'xiaofei', age: 12 }
2.第一个参数在属性复制过程中,可以会被修改,后面的会覆盖前面的属性
let obj = {
name:"goudan"
}
let obj2 = {
name:"xiaofei",
age:12
}
Object.assign(obj,obj2)
console.log(obj){ name: 'xiaofei', age: 12 }
3.assign这个方法的返回值就是第一个参数的引用,也就是返回了第一个参数。
console.log(Object.assign(obj,obj2));//{ name: 'xiaofei', age: 12 }
//assign()通过这个方法,得到的返回值就是第一参数引用
4.assign这个方法会把原型上面的发展也拷贝了。
let obj = { name:"goudan"}
let obj2 = { name:"xiaofei",age:12}
obj2.__proto__.sex = "man"
console.log(Object.assign(obj,obj2));//在obj的__proto__对象上面也会有sex属性
5.assign也不拷贝不可枚举的属性
Object.defineProperty(obj2,"age",{
writable:true,
configurable:true,
enumerable:false //在此处设置age的属性不可枚举
})
console.log(Object.assign(obj,obj2));//{ name: 'xiaofei' }
6.还有一点需要注意的是assign是浅拷贝
let obj = {};
let obj2 = { name:"xiaofei",frinends:['feng','hua','xue']}
console.log(Object.assign(obj,obj2));//{ name: 'xiaofei', frinends: [ 'feng', 'hua', 'xue' ] }
obj2.frinends.push('yue')//向obj2中friends新增一个元素,obj中也添加了一个元素。所以assign()是浅拷贝。
console.log(obj)//{ name: 'xiaofei', frinends: [ 'feng', 'hua', 'xue', 'yue' ] }
3.对象中属性的遍历
在ES6中共有5种方法实现对对象属性的遍历
这五种遍历方法都遵循同样的遍历次序规则:
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
(1) for...in
for...in 是用来遍历自身属性,和继承的可以枚举的属性。
var obj = {name:"feng",age:12}
obj.__proto__.sex="nan"
obj[Symbol()]="bol";
for(var item in obj){
console.log(item);//name,age,sex
}
(2).Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
console.log(Reflect.ownKeys(obj));//[ 'name', 'age', Symbol() ]
(3).Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
console.log(Object.keys(obj));//[ 'name', 'age' ]
(4).Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
console.log(Object.getOwnPropertyNames(obj));//[ 'name', 'age' ]
(5).Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
console.log(Object.getOwnPropertySymbols(obj));//[ Symbol() ]
6.ES6中set 和 map两种数据结构(集合)
<hr style="color:'#f2f2f2'">
1.set的基本用法
set和数据相似,也是一种 集合,主要的区别是,set里面的值是唯一的,没有重复的。set中可以放数组,不可以放对象,使用add向里面填充数据,可以使用delete删除其中的一个元素 ,创建set如下:
let coll = new Set([3,5,"feng","true"]);//放数组 console.log(coll) coll.add(22)//Set { 3, 5, 'feng', 'true' } console.log(coll)//Seet{ 3, 5, 'feng', 'true', 22 } coll.delete(3) console.log(coll)//Set { 5, 'feng', 'true' }
需要注意的是set不是数组,是一个相对象的数组,就是一个伪数组。Set中的数据可以用for of 以及 foeach来进行遍历。
coll.forEach(item =>console.log(item));//3 5 feng true for(let item of coll){console.log(item);}//3 5 feng true
接下来我们可以使用set来实现数组去重。set中的值都是唯一的,而数组中的值都是不唯一。
let coll = [1,1,12,3,3,true,true,NaN,NaN] let coll1 = [...(new Set(coll))] console.log(coll1)//[ 1, 12, 3, true, NaN ]
2,map的基本用法
它类似于对象,里面存放也是键值对,区别在于:对象中的键名只能是字符串,如果使用map,它里面的键可以是任意值。
Map的创建和用法如下
var m = new Map();
o = {p: "Hello World"};
m.set(o, "content");//使用set进行添加元素。
document.write(m.get(o))
// "content"
Map中的实例属性主要有
size:返回成员总数。
set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。
get(key):读取key对应的键值,如果找不到key,返回undefined。
has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
delete(key):删除某个键,返回true。如果删除失败,返回false。
clear():清除所有成员,没有返回值。
set():方法返回的是Map本身,因此可以采用链式写法。
<strong>主要看下Map的遍历方法</strong>
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回所有成员的遍历器。
let map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
document.write(key);
}
// "F"
// "T"
for (let value of map.values()) {
document.write(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
document.write(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
document.write(key, value);
}
// 等同于使用map.entries()
for (let [key, value] of map) {
document.write(key, value);
}
7.ES6中字符串的扩展
<hr>
1.includes(),startsWith(),endSWith()
在ES6之前,js中只有indexof方法,来确定一个字符串中是否包含在另一个字符串中。ES6中又提供了三种新的方法。
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let str = 'hello My word!'
console.log(str.startsWith("hello"));//true
console.log(str.endsWith('!'));//true
console.log(str.includes('My'));//true
2.repeat()
repeat(x)方法返回一个新的字符串,表示 将原字符串重复X次。
let str = "hello";
let str2 = str.repeat(3)
console.log(str2)//hellohellohello
3.字符串的遍历器接口
在ES6中为字符串添加了遍历器接口,使得字符串可以被for ...of 遍历。
let str = "helloboy";
for(let item of str){
console.log(item)// h e l l o b o y
}
4.padStart(),padEnd()
ES6引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
console.log('foo'.padStart(4,'a'));//afoo console.log('foo'.padEnd(4,'a'));//fooa
console.log('foo'.padStart(3,'a'));//foo
console.log('foo'.padEnd(3,'a'));//foo
上面代码中,padStart和padEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。
如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。
>padstart最常见的用途1.是用来补全在指定的位数。2.是用来提示字符串格式:
```javascript
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
8.ES6中class
<hr>
在没有Es6之前我们创建对象的主要方式有,1.使用字面量 2.构造器的方式 3.工厂模式 4.原型模式
ES6出现后我们就可以采用class的方法直接去创建对象了。接下来就来看下如何使用class去创建对象
1.class创建对象
采用class创建对象的格式如下
采用class创建对象需要注意的几个细节如下
1.class 是关键字,后面紧跟类名,类名首字母大写,采取的是大驼峰命名法则。类名之后是{}。
2.在{}中,不能直接写语句,只能写方法,方法不需要使用关键字
3.方法和方法之间没有逗号。不是键值对
用class创建一个类:
class Father{
constructor(name,age){
this.name =name;
this.age = age
}
message(){
console.log(`I am ${this.name} today ${this.age} years old`)
}
}
let father = new Father("father",23);
console.log(father.message());//I am father today 23 years old
2.static静态方法
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”;
class Father {
static classMethod() {
return 'hello';
}
}
Father.classMethod() // 'hello'
var father = new Father();
father.classMethod()
// TypeError: father.classMethod is not a function
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用Foo.classMethod(),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
<em>父类的静态方法,可以被子类继承。</em>
class Father {
static classMethod() {
return 'hello';
}
}
class Son extends Father {
}
Son.classMethod(); // 'hello'
上面代码中,父类Father有一个静态方法,子类Son可以调用这个方法。
3.class使用extends来实现继承。
extends继承的格式如下:
注意的细节有以下两点:
1.使用extends关键字来实现
2.在子类的构造器中,必须要显示调用父类的super方法。如果不使用
则this不可以用
继承实现的类如下:
class Son extends Father{
constructor(name,age,weight){
super(name,age);
this.weight =weight;
}
}
let son = new Son("son",12,123);
console.log(son.message());//I am son today 12 years old
<strong> 一旦继承父类之后,父类中的方法,子类可以直接拿来用。</srtong>
<hr style="border: 2px dashed #3333">
热门评论
慕课网markdown不给力啊