只要学不死,就往死里学。
概述
ECMAScript是一种可以在宿主环境中执行计算并能操作可计算对象的基于对象的程序设计语言。
ECMAScript是一种语言设计标准,虽然通常也被称为JavaScript,但是严格意义上,ECMAScript并不等于JavaScript,前者是后者的设计标准,后者是前者的具体实现和扩展(在浏览器环境中,JavaScript不仅仅实现了ECMAScript,同时还基于浏览器实现了针对DOM和BOM的操作)。
特性
ECMAScript有很多版本,其中ECMAScript2015最为突出。首先,其改变了传统的以版本命名的方式,改为以年份命名。其次,其明确了标准发布时间,由以前的时间不固定改为一年一个版本。最后,其增加了诸多特性,使js更像现代化编程语言。
ES6可以特指ECMAScript2015, 也可以泛指ECMAScript2015及以后的所有版本。
ECMAScript2015新增的特性可以总结为四大类:
- 解决原有语言的不足(let,const,块级作用域等)。
- 增强语法(解构,展开,默认参数等)。
- 全新的对象,方法,功能(Promise等)。
- 全新的数据类型和数据结构(Symbol等)。
块级作用域
在ECMAScript2015以前,js中存在两种作用域:
- 全局作用域。
- 函数作用域。
ECMAScript2015新增了块级作用域,代码只在相应的代码块中起作用。
// before
if (true) {
var num = 1
}
console.log(num) // 输出1, 可以在块之外访问变量
// after
if (true) {
let num = 1
}
console.log(num) // 抱错,不存在num, num只在{}构成的代码块中起作用
let,const
let,const和var一样,都是声明变量的方式,只是let,const会给变量添加作用范围,规避了var带来的诸多问题。
let
- let只能先声明,后使用。
// before
console.log(num) // 输出1, 因为浏览器编译js代码的时候,会将变量提升,因此可以在变量声明前访问变量
var num = 1
// after
console.log(num)
let num = 1 // 抱错,因为let变量只能先声明后使用。
- let声明的变量只能在块级作用域内起作用。
// before
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i) // 输出三次 3, setTimeout在执行时,操作的都是全局作用域中的i变量。
}, 100)
}
// after
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i) // 输出0, 1, 2, 由于每次setTimeout在执行的时候,都使用的是其作用域内的i,不会相互影响
}, 100)
}
const
const的用法和let相同,只不过,const声明的变量是只读的,不能修改。
这里的只读,指的是不能修改变量指向的内存地址,但是可以修改变量的属性。
const obj = {
name: 'zhangsan'
}
obj.name = 'lisi' // 可以修改
obj = {
name: 'lisi'
} // 抱错,修改了变量的内存指向地址,指向了新的地址。
解构
解构允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。
// before, 用下面的方式获取数组中的每一项,并赋值给对应的变量
let arr = [1, 2, 3]
let a = arr[0]
let b = arr[0]
let c = arr[0]
// after
let [a, b, c] = arr // 解构会将数组中的每一项自动赋值到对应位置的变量
let [, , c] = arr // 如果只想获取第三个,可以传入两个,
let [a, b] = arr // 如果只想获取前两个,可以省略后面。
// 对象也可以解构
let obj = {
name: 'zhangsan',
age: 18,
gender: true
}
let { name, age, gender } = obj // 由于对象属性没有顺序,因此需要变量和属性名相对应的方式解构。
let { address = '北京' } = obj // 可以为不存在的属性默认值
let { name: otherName = 'lisi'} = obj // 可以为某个属性对应的变量重命名
模版字符串
js原有"", ''两种方式表示字符串,ECMAScript2015新增了``表示字符串。
// 模版字符串支持多行字符串,方便包含换行符的字符串声明
let div = `<div>
this is div
</div>`
let name = 'zhangsan'
let intro = `my name is ${name}` // 可以嵌入变量等任何包含返回值的js合法语句,将被替换成返回值。
除了上述基础应用外,还包含一种特殊的带标签的模版字符串。
let obj = {
name: 'zhangsan',
age: 18,
gender: 0
}
// 定义标签方法
// strs表示用${}分割后的字符串数组,后续的参数代表${}对应的计算值
function changeGender(strs, name, gender) {
console.log(strs, name, gender) // [ '我叫', ',我是', '' ] zhangsan 0
let g = gender === 0 ? '男人' : '女人'
return strs[0] + name + strs[1] + g
}
// 使用
console.log(changeGender`我叫${obj.name},我是${obj.gender}`)
字符串扩展
为字符串添加了常见的includes,startsWith,endsWith方法。
const str = 'my name is zhangsan'
console.log(str.includes('name')) // 判断字符串中是否包含name
console.log(str.startsWith('my')) // 判断字符串是否以my开头
console.log(str.endsWith('zhangsan')) // 判断字符串是否以zhangsan结尾
对象字面量增强
加强对象字面量的声明方式,简化声明代码。
let name = 'zhangsan'
let person = {
// 将同名的变量添加到对象上
name,
// 简化对象上方法属性定义
getName() {
return this.name
},
// []表示计算属性,计算得到的值作为属性的属性名
[Math.random()]: 18
}
Object.assign
可以将多个对象的属性赋值到目标对象上,有则替换,没有则添加。
let obj = {
name: 'zhangsan',
age: 18
}
let obj2 = {
address: '北京'
}
let person = {
name: ''
}
console.log(Object.assign(person, obj, obj2))
// 复制一个全新的对象
let copied = Object.assign(person)
Object.is
Object.is可以用于判断值是否相等。
console.log(0 == false) // true, == 会先将值做转换,然后比较
console.log(0 === false) // false, ===会执行严格的判断
// === 无法正确识别的情况
console.log(+0 === -0) // true,
console.log(NaN === NaN) // false , === 认为每个NaN都是独立的一个值
// Object.is可以正确判断上述的两种情况
console.log(Object.is(+0, -0)) // false,
console.log(Object.is(NaN, NaN)) // true
参数默认值
在函数声明的时候,可以用更简单的方式给参数添加默认值。
// before
function intro(name, age) {
name = name || 'default'
console.log(`my name is ${name}`)
}
// after, 默认值参数必须放在非默认值参数的后面
function intro(age, name = 'default') {
console.log(`my name is ${name}`)
}
剩余参数
对于不定个数参数函数,可以用剩余参数将某个位置以后的参数放入到一个数组中。
// before
function add() {
// arguments获取所有变量, arguments是一个伪数组
return Array.from(arguments).reduce(function (pre, cur) {
return pre + cur
}, 0)
}
console.log(add(1, 2, 3))
// after
function intro(name, ...args) {
console.log(name, args) // zhangsan [ 18, '北京' ]
}
intro('zhangsan', 18, '北京')
参数展开
和默认参数相反,参数展开可以在调用函数的时候将数组中的每一项依次赋值给函数中相应位置的参数。
function intro(name, age, address) {
console.log(name, age, address)
}
const arr = ['zhangsan', 18, '北京']
// before
// 1. 利用获取每个位置的值实现
intro(arr[0], arr[1], arr[2])
// 2. 利用apply方法实现
intro.apply(intro, arr)
// after
intro(...arr)
箭头函数
箭头函数可以简化函数的声明,尤其是在回调函数的声明上。
const arr = ['zhangsan', 18, '北京']
// before
arr.forEach(function (item) {
console.log(item)
})
// after
arr.forEach(item => console.log(item))
箭头函数与普通的function函数的this指向不同,function的this指向调用者的上下文,是在调用时指定,而箭头函数的this是在声明时指定,指向父级的上下文。
const obj = {
name: 'lisi',
getName() {
console.log(this.name)
}
}
var name = 'zhangsan'
let getName = obj.getName
getName() // zhangsan, window调用,this指向window
obj.getName() // lisi, obj调用,this指向obj
//----------------------------------------------
const obj2 = {
name: 'lisi',
getNameFn() {
return () => {
console.log(this.name)
}
}
}
obj2.getNameFn()() // lisi, 箭头函数的this指向父级上下文,即getNameFn的上下文,由于getNameFn由obj2调用,因此this指向obj2
Promise
可以利用Promise写出更优雅的异步代码,规避回调地狱。
new Promise(resolve => {
setTimeout(() => {
resolve(1)
}, 100)
}).then(value => {
setTimeout(() => {
resolve(value + 1)
}, 100)
}).then(value => {
setTimeout(() => {
resolve(value + 1)
}, 100)
})
Proxy
通过Proxy代理可以实现对对象编辑获取等操作的拦截,从而在操作之前实现某种操作(例如Vue3.0就是利用Proxy实现数据双向绑定)。其和Object.defineProperty作用类似,但是相比Object.defineProperty,其语法更为简洁,而且作用范围更广(如Object.defineProperty没法监控数组项的增加和删除,Proxy可以)。
对象字面量代理
let obj = {
name: 'zhangsan',
age: 18
}
let objProxy = new Proxy(obj, {
get(target, property) {
console.log(`获取${property}值`)
return target[property] ? target[property] : 'default'
},
set(target, property, value) {
if (property === 'age') {
value = value > 25 ? 25 : value
}
target[property] = value
}
})
console.log(objProxy.address) // default
objProxy.age = 18
console.log(objProxy.age) // 18
objProxy.age = 30
console.log(objProxy.age) // 25
Proxy对象实例化时,第二个参数可以传入更多handler,如下图:
数组代理
let arr = [1, 2, 3]
let arrProxy = new Proxy(arr, {
set(target, property, value) {
value = value > 10 ? 10 : value
return target[property] = value
}
})
arrProxy.push(11)
console.log(arr) //[ 1, 2, 3, 10 ], 拦截成功,和push相似的shift,unshift,pop均可触发
Reflect
Reflect是ES2015新增的静态工具类,包含一系列针对对象的操作API,目的是提供统一的对象操作方式,结束目前混杂的对象操作。
let obj = {
name: 'zhangsan',
age: 18
}
// before
// get
console.log(obj.name)
console.log(obj['name'])
// set
obj['address'] = '北京'
// delete
delete obj.address
// after
// get
console.log(Reflect.get(obj, 'name'))
// set
Reflect.set(obj, 'address', '北京')
// delete
Reflect.deleteProperty(obj, 'address')
提供统一的操作api不仅代码美观,而且更容易让新手上路。
Reflect提供的方法和Proxy的代理方法是一一对应的,如果Proxy中没有传入相应的代理方法,那么Proxy内部默认使用Reflect对应方法实现。
class
在ES2015之前,js可以使用function和原型链实现类的声明。
function Person(name) {
// 实例属性
this.name = name
}
// 实例方法
Person.prototype.intro = function () {
console.log(`my name is ${this.name}`)
}
// 静态方法
Person.create = function (name) {
return new Person(name)
}
// 使用
let zhangsan = Person.create('zhangsan')
zhangsan.intro()
ES2015中添加了class关键字,可以用class关键字快速声明类。
class Person {
// 静态属性
static tag = 'Person'
constructor(name) {
// 实例属性
this.name = name
}
// 实例方法
intro() {
console.log(`my name is ${this.name}`)
}
// 静态方法, 利用static关键字
static create(name) {
return new Person(name)
}
}
// 使用
let zhangsan = Person.create('zhangsan')
zhangsan.intro()
ES2015在提供快速声明类的class关键字之外,还提供了extends关键字实现类的继承
class Student extends Person {
constructor(name, number) {
// 调用父类的构造方法
super(name)
// 声明自己的实例属性
this.number = number
}
say() {
// 调用父类实例方法
super.intro()
console.log(`我的学号:${this.number}`)
}
}
// 使用
let zhangsan = new Student('zhangsan', '10001')
zhangsan.say()
具体关于class的知识点还有很多,不再赘述。
Set
Set是ES2015新增的数据结构,用来表示集合的概念,特点是Set内部的值是不重复的,常常利用这个特点为数组去重。
Set基本使用如下:
// 声明集合,可以传入默认值,不传则是空集合
let s = new Set([1])
// 新增
s.add(2)
// 获取集合长度
console.log(s.size)
// 遍历
s.forEach(item => {
console.log(item)
})
// 删除
s.delete(2)
// 清空集合
s.clear()
如何去重?
// 简单数据去重
let arr = [1, 2, 1, 'one', 'two', 'one']
console.log(Array.from(new Set(arr)))
// 对象数组去重
let objArr = [
{
name: 'zhangsan',
age: 17
},
{
name: 'lisi',
age: 16
},
{
name: 'zhangsan',
age: 17
}
]
function unique(arr) {
// Set中判断对象是否重复是判断对象所指内存地址是否相同,所以不能直接将对象数组放入Set中
// 将对象数组转为字符串数组,方便对比。
let arrStrs = arr.map(item => JSON.stringify(item))
let uniqueStrs = Array.from(new Set(arrStrs))
return uniqueStrs.map(item => JSON.parse(item))
}
console.log(unique(objArr))
Map
Map是ES2015新增的数据结构,用来表示键值对的集合,可以弥补对象字面量的不足。对象字面量只能使用字符串作为键,即使使用计算属性传入非字符串作为键值,对象内部也会将其转为字符串,而Map没有这个限制,其键可以是任何数据。
let obj = new Map()
let person = {
name: 'zhangsan'
}
// 插入值
obj.set(person, '北京')
// 判断值是否存在
console.log(obj.has(person))
obj.forEach((item, key) => {
console.log(item, key)
})
// 删除
obj.delete(person)
// 清空
obj.clear()
// 获取大小
console.log(obj.size)
Symbol
Symbol是ES2015引入的新的原始数据类型(和string, number等并列),用来表示独一无二的值(只要调用Symbol(),那么生成的值就不同)。
- 独一无二
console.log(Symbol() === Symbol())
// Symbol可以接受一个参数作为标记值,这个值只是用于表示Symbol生成的变量的含义(描述自身),方便调试。
// 即使传入相同的参数,那么调用多次返回的值也不相同
console.log(Symbol('bar') === Symbol('bar'))
- 可用于声明私有属性
let obj = {
name: 'zhangsan',
[Symbol()]: '打篮球'
}
console.log(obj[Symbol()]) // undefined , 使用者无法获取obj里面的Symbol键值
console.log(Object.keys(obj)) // [ 'name' ] object的key方法也获取不到
console.log(Object.getOwnPropertySymbols(obj)) // 只有使用getOwnPropertySymbols方法能够获取到对象上定义的所有Symbol类型的键
- 静态方法
Symbol.for: 方法会根据给定的键 key
,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
const s = Symbol('bar')
console.log(Symbol.for('bar') === s) // false Symbol()创建的值不会保存在注册表中
console.log(Symbol.for('foo') === Symbol.for('foo')) // true
Symbol.iterator: 可以用作对象的键的值,由于其永远是不重复的,所以不担心被覆盖。在后面的实现Iterable迭代器接口会用到。
for...of
ES2015新增的数据遍历方式,可以用for...of遍历任何数据(只要其实现了Iterable接口)。与forEach相比,其可以在内部添加break关键字随时停止遍历。
const arr = [1, 'one', false, NaN, undefined]
// 遍历数组
for (let item of arr) {
console.log(item)
// 终止遍历
if (!item) {
break
}
}
// 遍历Set
const set = new Set(arr)
for (let item of set) {
console.log(item)
}
// 遍历Map
let map = new Map()
map.set({ name: 'zhangsan' }, '北京')
map.set('age', 18)
for (let [key, value] of map) {
console.log(key, value)
}
Iterable
上节中提到,如果想要for...of遍历某种数据,那么该数据必须实现Iterable接口。
Iterable接口要求实现一个方法,该方法返回一个迭代器对象(iterator), 迭代器对象包含next方法,next方法返回一个包含value,done两个键值的对象,value保存下一次遍历时的数据,done用于表示迭代器是否完成。
const todos = {
life: ['吃饭', '睡觉'],
course: ['语文', '数学', '英语'],
// 实现Iterable接口
[Symbol.iterator]: function () {
let allTodos = [...this.life, ...this.course]
let index = 0
return {
next: function () {
return {
value: allTodos[index],
done: index++ >= allTodos.length
}
}
}
}
}
for(let item of todos) {
console.log(item)
}
上述代码体现了编程模式中常用的迭代器模式。
迭代器模式指的是提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。
也就是说在todos对象中用了life和course两个字段存储需要做的事情,而外面在使用该对象的时候,不用关心todos对象内部如何存储,只需要通过for...of遍历获取所有数据,这样就降低了数据定义和数据使用的耦合度。当todos新增一个新的字段存储需要做的事情时,只需要修改todos,而不需要需改使用者。
Generator
Generator生成器是ES2015新增的一种异步编程解决方案,用于解决异步编程中回调函数嵌套的问题,其通常使用*和yeild两个关键字。
Generator生成器生成的方法是惰性执行的,只有调用者在调用next方法后其才会执行,遇到yield关键字又停止。
- 生成id
function* createIdMaker() {
let id = 0
while (true) {
yield id++
}
}
const idMaker = createIdMaker()
console.log(idMaker.next().value) // 0
console.log(idMaker.next().value) // 1
console.log(idMaker.next().value) // 2
- Itrerator方法
const todos = {
life: ['吃饭', '睡觉'],
course: ['语文', '数学', '英语'],
// 实现Iterable接口
[Symbol.iterator]: function* () {
const all =[...this.life, ...this.course]
for(let item of all) {
yield item
}
}
}
for (let item of todos) {
console.log(item)
}
- 异步编程
详情见上一篇异步编程
Modules
Modules是ES2015提供的标准化模块系统,模块化为你提供了一种更好的方式来组织变量和函数。你可以把相关的变量和函数放在一起组成一个模块。todo
等js模块化部分学完之后再补充
ES2016
ES2016相对于ES2015是一个小的版本,只提供了如下的小特性。
数组includes方法
提供includes方法方便查找数组中是否存在某一项。
const arr = [1, 'one', false, NaN, undefined]
// before
console.log(arr.indexOf(1) > -1) // true
console.log(arr.indexOf(NaN) > -1) // false 对于NaN的查找出错
// after
console.log(arr.includes(NaN))// true,可以正常查找
指数运算符
方便在大量的数学运算中使用。
// before
console.log(Math.pow(2, 10))
// after
console.log(2 ** 10)
ES2017
ES2017和ES2016一样,也是小版本。
Object.values
和Object.keys相对应,获取对象的所有值。
const obj = {
name: 'zhangsan',
age: 18
}
Object.values(obj).forEach(item => {
console.log(item)
});
Object.entries
获取键值对的数组,相当于将Object.keys和Object.values组合。
const obj = {
name: 'zhangsan',
age: 18
}
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value)
});
getOwnPropertyDescriptors
用于获取对象的属性描述信息,可以用于补充解决Object.assign的问题。
const obj = {
firstName: 'zhang',
lastName: 'san',
get fullName() {
return this.firstName + ' ' + this.lastName
}
}
let copied = Object.assign({}, obj)
copied.firstName = 'li'
console.log(copied.fullName) // zhangsan
// Object.assign 在拷贝计算属性时,将计算属性的值拷贝过来,导致拷贝后的对象中计算属性有问题
使用getOwnPropertyDescriptors就可以避免这种拷贝问题。
const obj = {
firstName: 'zhang',
lastName: 'san',
get fullName() {
return this.firstName + ' ' + this.lastName
}
}
let copied = {}
Object.defineProperties(copied, Object.getOwnPropertyDescriptors(obj))
copied.firstName = 'li'
console.log(copied.fullName)
padStart/padEnd
用于在字符串的前面或者后面填充一定数量的某种字符,可以使字符串显示的更加美观。
const obj = {
number: '1',
age: '18'
}
Object.entries(obj).forEach(([key, value]) => {
console.log(`${key.padEnd(10, '-')}|${value.padStart(3, '0')}`)
})
//number----|001
//age-------|018
允许函数参数中添加尾逗号
允许像数组和对象那样,在声明和调用函数时在末尾加上逗号,只是为了方便部分人的书写习惯,没有实在意义。
const arr = [1, 2, 3,] // 在数组末尾可以加入逗号,不影响数组声明
// 声明函数时可以在参数的末尾加入逗号
function add(a, b,) {
return a + b
}
// 调用函数时可以在参数的末尾加入逗号
console.log(add(1, 2,))
Async/Await
新增的异步编程解决方案,同样是用于解决回调函数嵌套的问题。详情见上一篇异步编程。
ES2018
在对象上应用展开和剩余
和数组的展开和剩余相似,ES2018允许在对象上使用展开和剩余。
const obj = { one: 1, two: 2, three: 3, four: 4, five: 5 }
const { one, four, ...rest } = obj
// one => 1, four => 4
// rest => { two: 2, three: 3, five: 5}
const obj2 = { foo: 'bar', ...rest }
// obj2 => { foo: 'bar', two: 2, three: 3, five: 5}
// 展开时,同名属性会覆盖
const obj3 = { foo: 'bar', two: 200, ...rest }
// obj3 => { foo: 'bar', two: 2, three: 3, five: 5}
const obj4 = { foo: 'bar', ...rest, two: 200 }
// obj4 => { foo: 'bar', two: 200, three: 3, five: 5}
正则:环视
// 环视
const intro = '张三是张三,张三丰是张三丰,张三不是张三丰,张三丰也不是张三'
// 向后否定 正向肯定 只有在张三后面不是丰的时候,才会用李四替代张三
const res1 = intro.replace(/张三(?!丰)/g, '李四')
// 向后肯定 正向肯定 只有在张三后面跟着丰的时候,才会用李四替代张三
const res2 = intro.replace(/张三(?=丰)/g, '李四')
// 向前肯定 反向肯定 只有在00前面是A的时候,才会用88替代00
const res3 = 'A00 B00'.replace(/(?<=A)00/g, '88')
// 向前否定 反向肯定 只有在00前面不是A的时候,才会用88替代00
const res4 = 'A00 B00'.replace(/(?<!A)00/g, '88')
正则:组名称
为正则组添加别名,方便查找正则组
const date = '2020-05-20'
const reg = /(?<year>\d{4})-(?<mouth>\d{2})-(?<day>\d{2})/
const res = reg.exec(date)
console.log(res)
// 可以在groups对象下,通过别名year获取值
console.log(res.groups.year) // 2020
Promise finally
添加finally方法,不论Promise是resolve还是reject,finally都会被执行。
new Promise((resolve, reject) => {
setTimeout(() => {
const now = Date.now()
now * 2 ? resolve(now) : reject(now)
}, 1000)
})
.then(now => {
console.log('resolved', now)
})
.catch(now => {
console.log('rejected', now)
})
.finally(now => {
console.log('finally', now)
})
ES2019
数组稳定排序
const arr = [
{ id: 1, value: 'A' },
{ id: 1, value: 'B' },
{ id: 1, value: 'C' },
{ id: 1, value: 'D' },
{ id: 1, value: 'E' },
{ id: 1, value: 'F' },
{ id: 1, value: 'G' },
{ id: 1, value: 'H' },
{ id: 1, value: 'I' },
{ id: 1, value: 'J' },
{ id: 4, value: 'K' },
{ id: 1, value: 'L' },
{ id: 1, value: 'B' },
{ id: 1, value: 'C' },
{ id: 1, value: 'D' },
{ id: 1, value: 'E' },
{ id: 1, value: 'F' },
{ id: 1, value: 'G' },
{ id: 1, value: 'H' },
{ id: 1, value: 'I' },
{ id: 1, value: 'J' },
{ id: 4, value: 'K' },
{ id: 1, value: 'L' },
]
// 旧版本的 ES 排序过后的结果可能不固定
console.log(arr.sort(function (a, b) {
return a.id - b.id
}))
try...catch可省略参数
简化try...catch语法
try {
throw new Error()
}
// catch后面可以省略参数e
catch{ }
ES2020
空值合并运算符
function foo(data) {
// ??表示当data为undefined或者null的时候取100
let result = data ?? 100
// 简化下面的写法
let result1 = data === null || data === undefined ? 100 : data
console.log(result)
}
可选链运算符
const list = [
{
title: 'foo',
author: {
name: 'zce',
email: 'w@zce.me'
}
},
{
title: 'bar'
}
]
list.forEach(item => {
// 如果author属性不存在,那么 item.author?.name 相当于item.author
console.log(item.author?.name)
})
// 还可以用下面类似的写法
// obj?.prop 获取对象属性值
// obj?.[expr] 获取对象属性值
// arr?.[index] 获取数组指定下标值
// func?.(args) 方法调用
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。