1、ECMAScript 与JavaScript
- ECMAScript 也是一门脚本语言,就是JavaScript的语言本身,通常被看作JavaScript的标准化规范,实际上JavaScript是ECMAScript的扩展语言。
- 因为ECMAScript 中只提供了最基本的语法,而JavaScript实现了ECMAScript 这种语言的标准,并且在此基础之上,做了些扩展。
- 总之,在浏览器中的JavaScript = ECMAScript + web APIS提供的(BOM+DOM);在node环境中,JavaScript = ECMAScript + Node APIS(fs,net…)。
文章图片
文章图片
- 从2015年开始ES保持每年一个版本的迭代,并且从2015年开始,ECMAScript 决定不再按照版本号命名,而是使用发行年份命名。由于这样的决定是在ES2015诞生的过程当中产生的,所以很多人也习惯把ES2015称之为ES6。
文章图片
- 解决原有语法上的一些问题或者不足
- 对原有语法进行增强,使之变得更为便捷、易用
- 全新的对象、全新的方法、全新的功能
- 全新的数据类型和数据结构
- 作用域:某个成员能够起作用的范围。
- 在ECMAScript 2015之前,ES只有两种作用域(全局作用域、函数作用域)。
- 在ECMAScript 2015中,新增了块级作用域。块:代码中用一对花括号{}包裹起来的范围。例如:
//块级作用域--if语句
if (true) {
console.log('hello, baby');
}//块级作用域--for循环
for (let i = 0;
i < 8;
i++) {
console.log(i);
}
- 以前块是没有独立的作用域的,导致我们在块中定义的成员,外部也可以访问到,这点对于复杂代码是非常不利的,也是非常不安全的。
if (true) {
var bar = 'hello'
}
console.log(bar);
//hello
- 有了块级作用域,使用let定义变量,就解决了上述问题。
if (true) {
let bar = 'hello'
}
console.log(bar);
//ReferenceError: bar is not defined
- let这个关键字的特性非常适合我们声明for循环中的计数器。
//for循环内部有两层作用域
for (let i = 0;
i < 3;
i++) {
for (let i = 0;
i < 3;
i++) {
console.log(i);
}
console.log('内存结束 i = ' + i);
}
// 内存结束 i = 0
// 0
// 1
// 2
// 内存结束 i = 1
// 0
// 1
// 2
// 内存结束 i = 2
- let不存在变量声明的提升。必须先声明再使用。
console.log(foo);
//ReferenceError: Cannot access 'foo' before initialization
let foo = 'aa'
const
- ES2015中新增了一个const关键字,用来声明一个只读的恒量/常量。
- 它的特点就是在let的基础上多了【只读】特性,只读就是变量一旦声明过后就不允许再被修改。
const name = 'aaa'
name = 'bbb'//TypeError: Assignment to constant variable.
- 既然const是恒量,那么const在声明的同时,就必须要去设置一个初始值。而且const不能像var一样,声明和赋值不能放在两个语句中。
- const所声明的成员不能被修改,只是说不允许在声明过后,重新去指向一个新的内存地址,并不是说不允许修改恒量中的属性成员。
const obj = {}
obj.name = 'eeee'
console.log(obj);
//{ name: 'eeee' }obj = {}//TypeError: Assignment to constant variable.
最佳实践:不用var,主用const,配合let 。
数组的解构
通过数组解构的方式,可以从数组当中去快速提取数组中的成员。(与元素的顺序有关,需通过元素的位置去匹配)
//数组解构
const arr = [2, 4, 5]//传统做法
const foo = arr[1]
console.log(foo);
//4//数组解构方式
const [, baz] = arr
console.log(baz);
//4//剩余参数
const [f, ...rest] = arr
console.log(f);
//2
console.log(rest);
//[ 4, 5 ]//超过
const [one, two, three, more] = arr
console.log(more);
//undefined//默认值
const [a, b, c, d = 'ok'] = arr
console.log(d);
//ok//适用场景-如拆分字符串
const path = '/foo/bar/baz'
// const tmp = path.split('/')
// const dir = tmp[1]const [, dir] = path.split('/')
console.log(dir);
//foo
对象的解构
通过对象解构的方式,可以从对象当中去快速提取对象中的属性。(与属性的顺序无关,只需通过属性名去匹配)
//对象解构
const obj = { age: 18, class: '科技一班' }
// const obj = { age: 18 }
const { age } = obj
console.log(age);
//18//属性重命名
const { age: myAge } = obj
console.log(myAge);
//18//默认值
// const { class: myClass = '太空二班' } = obj
// console.log(myClass);
//太空二班//适用场景-简化console.log方法
const { log } = console
log('foo')
log('aaa')
log('bbb')
// foo
// aaa
// bbb
模板字符串字面量
模板字符串需要使用`来表示。模板字符串的新特性:
- 支持换行,可以直接在模板字符串中敲入换行符
//支持换行
const str = `hello, 2020,
we are here`
console.log(str);
// hello, 2020,
// we are here
- 支持插值表达式方式去字符串中插入JS表达式
//支持插入JS表达式
const name = 'jack'
const msg = `hey, ${name}, welcome to ${new Date().getFullYear()}`
console.log(msg);
//hey, jack, welcome to 2020
- 带标签的模板字符串:作用是对模板字符串进行加工
//带标签的模板字符串
const str = console.log`hello`//[ 'hello' ]const name = 'rock'
const gender = truefunction myTagFunc(strings, name, gender) {
// console.log(strings, name, gender);
//[ 'hey,', ' is a ', '' ] rock male
// return 123
const sex = gender ? 'man' : 'woman'
return strings[0] + name + strings[1] + sex + strings[2]
}const res = myTagFunc`hey,${name} is a ${gender}`
// console.log(res);
//123
console.log(res);
//hey,rock is a man
字符串的扩展方法
- includes()
- startsWith()
- endsWith()
//字符串的扩展方法
const msg = 'Error: a is not defined.'console.log(msg.startsWith('Error'));
//true
console.log(msg.endsWith('ok'));
//false
console.log(msg.includes('a'));
//true
函数参数的默认值
//函数参数的默认值
function foo(bar, enable = true) {
console.log(bar, enable);
}
foo(3) //3 true
foo(3, false) //3 false
剩余参数
ES2015新增了个…操作符,有两个作用:
- rest作用,即剩余操作符
//以前的做法
function foo() {
console.log(arguments);
}
foo(1, 2, 3)//[Arguments] { '0': 1, '1': 2, '2': 3 },伪数组//剩余参数做法
function bar(first, ...args) {
console.log(first, args);
}
bar(3, 4, 5)//3 [ 4, 5 ]
- spread作用,即展开操作符
//展开数组参数
const arr = ['foo', 'bar', 'baz']console.log(
arr[0],
arr[1],
arr[2]
);
//foo bar baz//以前做法
console.log.apply(console, arr);
//foo bar baz
//ES6
console.log(...arr);
//foo bar baz
箭头函数
//以前定义函数
function func(num) {
return num + 2
}
//箭头函数
const func1 = n => n + 2console.log(func(3));
//5
console.log(func1(3));
//5//使用箭头函数大大简化了回调函数的编写
const arr = [3, 2, 4, 6, 7]
const even = arr.filter(function (item) {
return item % 2
})console.log(even);
//[ 3, 7 ]const even1 = arr.filter(item => item % 2)
console.log(even1);
//[ 3, 7 ]
箭头函数会让我们的代码变的更简短而且更易读,并且,箭头函数不会改变this的指向。
const person = {
name: 'Rock',
sayHi1: function () {
console.log(`Hi, this is ${this.name}`);
},
sayHi2: () => {
console.log(`Hi, this is ${this.name}`);
},
sayHiAsync1: function () {
setTimeout(function () {
console.log(this.name);
}, 1000)
},
sayHiAsync2: function () {
setTimeout(() => {
console.log(this.name);
}, 1000)
}
}
person.sayHi1()//Hi, this is Rock
person.sayHi2()//Hi, this is undefined
person.sayHiAsync1()//undefined
person.sayHiAsync2()//Rock
对象字面量增强
ECMAScript 2015升级了对象字面量的语法。
//对象字面量
const bar = 123
const obj = {
foo: 34,
bar,
func: function () {
console.log(this.foo);
},
func2() {
console.log(this);
},
[bar]: 888
}//可以使用表达式返回值作为对象的属性名
obj[Math.random()] = 123console.log(obj);
//{ '123':888, foo: 34, bar: 123, func: [Function: func], func2: [Function: func2], '0.5719176856362458': 123}
obj.func2()//{ '123':888, foo: 34, bar: 123, func: [Function: func], func2: [Function: func2], '0.5719176856362458': 123}
对象扩展方法
- Object.assign():将多个源对象中的属性复制到一个目标对象中。
const source = { a: 2, b: 3 }const source1 = { c: 222, d: 21 }const target = { a: 888, c: 999 }const res = Object.assign(target, source, source1) console.log(res); //{ a: 2, c: 222, b: 3, d: 21 } console.log(res === target); //true//可以用来复制对象 function func(obj) { obj.name = 'func obj' console.log(obj); }function func2(obj) { const funcObj = Object.assign({}, obj) funcObj.name = 'func obj' console.log(funcObj); }const obj = { name: 'global obj' } // func(obj)//{ name: 'func obj' } // console.log(obj); //{ name: 'func obj' }func2(obj)//{ name: 'func obj' } console.log(obj); //{ name: 'global obj' }
- Object.is():用来去判断两个值是否相等。
console.log( 0 == false,//true 0 === false, //false +0 === -0,//true NaN === NaN,//false Object.is(+0, -0),//false Object.is(NaN, NaN)//true );
- 想要监视一个对象中属性的读写,可以使用Object.defineProperty来为对象添加属性。
- 在ECMAScript 2015中设计了Prixy,专门用来为对象设置访问代理器。可看作门卫,使用它,可以轻松监视到对象的读写过程。
const person = { name: 'Rock', age: 5 }const personProxy = new Proxy(person, { get(target, prop) { return prop in target ? target[prop] : 'default' }, set(target, prop, value) { if (prop === 'age') { //校验 if (!Number.isInteger(value)) { throw new TypeError(`${value} is not int`) } } target[prop] = value } })console.log(personProxy.name); //Rock console.log(personProxy.xxx); //default// personProxy.age = 'ss' //TypeError: ss is not int personProxy.age = 20 console.log(personProxy); //{ name: 'Rock', age: 20 }
- Proxy的优势:
- Proxy更为强大:defineProperty只能监视属性的读写,Proxy能够监视到更多对象操作。
//delete操作 const personProxy2 = new Proxy(person, { defineProperty(target, prop) { delete target[prop] } })delete personProxy.age console.log(person); //{ name: 'Rock' }
Proxy的其他对象操作 :
文章图片
- Proxy更好的支持数组对象的监视
//数组操作 const list = [] const listProxy = new Proxy(list, { set(target, prop, value) { console.log('set', prop, value); target[prop] = value return true } }) listProxy.push(100) // set 0 100 // set length 1
- Proxy是以非侵入的方式监管了对象的读写。
- Proxy更为强大:defineProperty只能监视属性的读写,Proxy能够监视到更多对象操作。
- 统一的对象操作API。
- Reflect属于一个静态类,不能通过new Reflect()方式去构造一个实例对象,只能去调用静态类中的方法,如Reflect.get()。
- Reflect内部封装了一系列对对象的底层操作,提供了13个静态方法。
- Reflect成员方法就是Proxy处理对象的默认实现。
const obj = { a: 123, b: '346' }const proxy = new Proxy(obj, { get(target, prop) { return Reflect.get(target, prop) } })console.log(proxy.a); //123
- 统一提供了一套用于操作对象的API
const obj2 = { name: 'Rock', age: 20 } //传统做法 console.log('name' in obj2); //true console.log(delete obj2['age']); //true console.log(Object.keys(obj2)); //[ 'name' ]//Reflect方式 console.log(Reflect.has(obj2, 'name')); //true console.log(Reflect.deleteProperty(obj2, 'age')); //true console.log(Reflect.ownKeys(obj2)); //[ 'name' ]
Reflect的对象操作:
文章图片
提供了一种更优的异步编程解决方案,通过链式编程方式解决了传统异步编程中回调函数嵌套过深的问题。
class类
在此之前,ECMAScript中都是通过定义函数以及函数的原型对象来去实现的类。
//传统实现类--通过函数及函数的原型对象
function Person(name) {
this.name = name
}
Person.prototype.say = function () {
console.log(`hi, my name is ${this.name}`);
}const person = new Person('Jack')
person.say()//hi, my name is Jack//ES6实现
class Person1 {
constructor(name) {
this.name = name
}
say() {
console.log(`hi, my name is ${this.name}`);
}
}
const per = new Person1('Rock')
per.say()//hi, my name is Rock
静态方法和实例方法
实例方法:需要通过这个类构造的实例对象去调用。
静态方法:直接通过这个类本身去调用。
ES2015中新增添加静态成员与静态方法的static关键字。
//静态方法和实例方法
class Person {
constructor(name) {
this.name = name
}
//实例方法
say() {
console.log(`hi, my name is ${this.name}`);
}
//静态方法
static create(name) {
return new Person(name)
}
}
const tom = Person.create('Tom')
tom.say()//hi, my name is Tom
类的继承
继承是面向对象中一个非常重要的特性。通过继承能够抽象出来相似类型之间重复的地方。
ES2015之前,大多数情况都是使用原型实现继承,但在Es2015中新增关键字extends关键字实现继承。
class Person {
constructor(name) {
this.name = name
}
//实例方法
say() {
console.log(`hi, my name is ${this.name}.`);
}
}class Student extends Person {
constructor(name, age) {
super(name)
this.age = age
}
hello() {
super.say()
console.log(`my age is ${this.age}.`);
}
}const student = new Student('Jerry', 6)
student.hello()
// hi, my name is Jerry.
// my age is 6.
Set
可以理解为集合,与传统的数组比较类似,但是Set中的元素不允许重复。每一个值在Set当中都是唯一的。
//Set数据结构
const set = new Set()
set.add(1).add(2).add(3).add(4)
console.log(set);
//Set(4) { 1, 2, 3, 4 }//遍历
set.forEach(i => console.log(i))
// 1
// 2
// 3
// 4//for...of遍历
for (let item of set) {
console.log(item);
}
// 1
// 2
// 3
// 4//获取set的长度
console.log(set.size);
//4//判断是否包含某个元素
console.log(set.has(19));
//false//删除元素
console.log(set.delete(3));
//true//清空元素
set.clear()
console.log(set);
//Set(0) {}
常见应用场景:为数组去重
//数组去重
const arr = [1, 2, 3, 2, 4, 1, 2, 5]
const result = new Set(arr)
console.log(result);
//Set(5) { 1, 2, 3, 4, 5 }//set转换为数组
const resArr1 = Array.from(new Set(arr))
const resArr2 = [...new Set(arr)]
console.log(resArr1, resArr2);
//[ 1, 2, 3, 4, 5 ] [ 1, 2, 3, 4, 5 ]
Map
与ECMAScript中对象非常类似,都是键值对集合,ECMAScript中键只能为字符串类型,不方便存放复杂数据结构。
//ES6以前
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'
console.log(Object.keys(obj));
//[ '123', 'true', '[object Object]' ]
console.log(obj['[object Object]']);
//value
Map用来去映射两个任意类型数据之间的映射关系。与对象最大的区别就是,它可以用任意类型的数据作为键,而对象只能用字符串作为键。
//ES6
const map = new Map()
const jack = { name: 'jack' }
map.set(jack, 90)
map.set('age', 19)
console.log(map);
//Map(1) { { name: 'jack' } => 90, 'age' => 19 }
console.log(map.get(jack));
//90console.log(map.has(jack));
//true
// console.log(map.delete('age'));
//true
// map.clear()
// console.log(map);
//Map(0) {}//遍历
map.forEach((value, key) => {
console.log(value, key);
})
// 90 { name: 'jack' }
// 19 age
Symbol
一种全新的原始数据类型
在ES2015之前,对象的属性名都是字符串,而字符串是有可能重复的,容易引起冲突。
ES2015为了解决这个问题,提供了一种全新的数据类型,叫Symbol(符号)。作用是表示一个独一无二的值。
最主要的作用就是为对象添加独一无二的属性名。
//ES2015以前
//store.js
const cache = {}
//a.js
cache['a_foo'] = Math.random()
//b.js
cache['b_foo'] = '123'console.log(cache);
//{ a_foo: 0.8887530628553206, b_foo: '123' }//ES2015
const s = Symbol()
console.log(s);
//Symbol()
console.log(typeof s);
//symbol
console.log(Symbol() === Symbol());
//false
截止到ES2019,一共定义了7种原始数据类型,未来还会新增BigInt原始数据类型,用于去存放更长的数字。目前处于stage-4阶段,预计下个版本被标准化,标准化过后就是8种数据类型了。
每次通过Symbol去创建的值一定是唯一的,如果想全局复用一个相同的Symbol值,可以使用全局变量方式或使用Symbol提供的静态方法:
console.log(Symbol('foo'));
//Symbol(foo)
console.log(Symbol('bar'));
//Symbol(bar)
console.log(Symbol('car'));
//Symbol(car)const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj);
//{ [Symbol()]: '123', [Symbol()]: '456' }const obj1 = {
[Symbol()]: 888
}
console.log(obj1);
//{ [Symbol()]: 888 }//a.js
const name = Symbol()
const person = {
[name]: 'abc',
say() {
console.log(this[name]);
}
}
//b.js
person.say()//abcconst obj2 = {
[Symbol()]: 'symbol value',
foo: 'normal value'
}
//不能遍历普通对象
// for (let i of obj2) {
//console.log(i);
//TypeError: obj is not iterable
// }
console.log(Object.keys(obj2));
//[ 'foo' ]
console.log(JSON.stringify(obj2));
//{"foo":"normal value"}console.log(Object.getOwnPropertySymbols(obj2));
// {"foo":"normal value"}
// [ Symbol() ]
提供了一些内置的Symbol常量:
- Symbol.iterator
- Symbol.hasInstance
在ECMAScript中遍历数据的方法:
- for:遍历普通的数组
- for…in:遍历键值对
- 一些对象的遍历方法(如 forEach等)
这些遍历方式都有一定的局限性,E2015引入了全新的for…of循环。这种遍历方式以后会作为遍历所有数据结构的统一方式。
const obj = {
[Symbol()]: 'symbol value',
foo: 'normal value'
}
//不能遍历普通对象
// for (let i of obj) {
//console.log(i);
//TypeError: obj is not iterable
// }const arr = [23, 45, 67, 87]// for (const item of arr) {
//console.log(item);
// }
// 23
// 45
// 67
// 87//forEach不能跳出循环
// arr.forEach(item => console.log(item))//for...of能跳出循环
for (const item of arr) {
console.log(item);
if (item > 50) {
break
}
}
// 23
// 45
// 67const s = new Set(['aaa', 'bbb'])
for (let item of s) {
console.log(item);
}
// aaa
// bbbconst m = new Map()
m.set('foo', 123)
m.set('bar', 345)
for (let [key, value] of m) {
console.log(key, value);
}
// foo 123
// bar 345
可迭代接口
ES中能够表示有结构的数据类型越来越多,为了给各种各样的数据结构提供统一的遍历方式,ES2015提供了Iterable接口,即可迭代的。实现Iterable接口就是for…of的前提。
//迭代器(iterator)
const set = new Set(['aa', 'bb', 'cc'])
const iterator = set[Symbol.iterator]()console.log(iterator.next());
//{ value: 'aa', done: false }
console.log(iterator.next());
//{ value: 'bb', done: false }
console.log(iterator.next());
//{ value: 'cc', done: false }
console.log(iterator.next());
//{ value: undefined, done: true }
只要对象实现了Iterator,那对象也可以使用for…of方法。
//实现可迭代接口
const obj = {
//iterable
[Symbol.iterator]: function () {
//iterator
return {
next: function () {
//IterationResult
return {
value: 'aaa',
done: true
}
}
}
}
}const obj2 = {
store: ['foo', 'bar', 'baz'],[Symbol.iterator]: function () {
let index = 0
const self = thisreturn {
next: function () {
const result = {
value: self.store[index],
done: index >= self.store.length
}
index++
return result
}
}
}
}for (const item of obj2) {
console.log('循环体', item)
}
// 循环体 foo
// 循环体 bar
// 循环体 baz
for…of可以取代forEach,而且for…of内可以使用break。
迭代器模式
迭代器的核心就是对外提供统一遍历接口,让外部不用再去关心内部这个数据结构是啥样的。
// 场景:你我协同开发一个任务清单应用
//实现代码
const todos = {
life: ['阅读', '运动', '音乐'],
learn: ['JS', '算法', 'Vue'],
work: ['编码'],
// 提供统一遍历访问接口
each: function (callback) {
const all = [].concat(this.life, this.learn, this.work)
for (const item of all) {
callback(item)
}
},
// 提供迭代器(ES2015 统一遍历访问接口)
[Symbol.iterator]: function () {
const all = [...this.life, ...this.learn, ...this.work]
let index = 0
return {
next: function () {
return {
value: all[index],
done: index++ >= all.length
}
}
}
}
}// for (const item of todos.life) {
//console.log(item)
// }
// for (const item of todos.learn) {
//console.log(item)
// }
// for (const item of todos.work) {
//console.log(item)
// }todos.each(function (item) {
console.log(item)
})console.log('-------------------------------')for (const item of todos) {
console.log(item)
}
// 阅读
// 运动
// 音乐
// JS
// 算法
// Vue
// 编码
生成器(Generator)
- 避免异步编程中回调嵌套过深,提供更好的异步编程解决方案。
- 生成器对象其实也实现了Iterator接口协议。
function* foo() { console.log('qqq')//qqq return 100 }const result = foo() console.log(result.next())//{ value: 100, done: true }
- 生成器一般与yield关键字配合使用。
function* foo() { console.log('1111') yield 100 console.log('2222') yield 200 console.log('3333') yield 300 }const generator = foo()console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停 console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停 console.log(generator.next()) // 。。。 console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined // 1111 // { value: 100, done: false } // 2222 // { value: 200, done: false } // 3333 // { value: 300, done: false } // { value: undefined, done: true }
- 生成器函数会自动帮我们返回一个生成器对象,调用该对象的next方法才会让这个生成器函数的函数体开始执行,遇到yield函数执行会暂停下来。yield后的值会作为next()的返回值。
// Generator 应用 // 案例1:发号器 function* createIdMaker() { let id = 1 while (true) { yield id++ } }const idMaker = createIdMaker()console.log(idMaker.next().value) console.log(idMaker.next().value) console.log(idMaker.next().value) console.log(idMaker.next().value) // 1 // 2 // 3 // 4// 案例2:使用 Generator 函数实现 iterator 方法 const todos = { life: ['阅读', '运动', '音乐'], learn: ['JS', '算法', 'Vue'], work: ['编码'], [Symbol.iterator]: function* () { const all = [...this.life, ...this.learn, ...this.work] for (const item of all) { yield item } } }for (const item of todos) { console.log(item) } // 阅读 // 运动 // 音乐 // JS // 算法 // Vue // 编码
4、ECMAScript 2016概述 发布于2016/6,是个小版本,仅实现了两个特性:
- includes()
const arr = [2,3,'aa',4] console.log(arr.indexOf(2)); //0 console.log(arr.includes(1)); //-1 console.log(arr.includes(2)); //true
- 指数运算符
console.log(Math.pow(2,10)); //1024 console.log(Math.pow(2,10)); //1024
- Object对象的三个方法
Object.values()、Object.entries()、Object.getOwnPropertyDescriptors()(获取对象中完整信息的描述) - 字符串填充方法
String.prototype.padStart()、String.prototype.padEnd() - 允许函数参数中添加尾逗号
- Async/Await
- ECMAScript 2015 Language Specification
- Flow
- Flow-type
推荐阅读
- ES6|ES6新特性常用总结
- 前端|ES6语法新特性
- JavaScript|Jquery对象、Jquery选择器、Jquery Dom操作及事件
- 前端|ES6新特性总结
- JavaScript|JavaScript — 对象创建模式与继承模式
- JavaScript|JavaScript — 线程机制与事件机制
- javascript|ES6新特性(上)——let及const、解构赋值、模板字符串、Symbol
- JavaScript|ES6新特性总结(2)解构赋值、模板字符串、Symbol
- #|ES6基础学习——第一天(let 声明、const 声明、解构赋值、模板字符串(反引号)、简化对象写法、箭头函数、参数默认值、rest 参数、spread 扩展运算符)