JavaScript新特性
准备工作
安装node和nodemon
12.16.3版本的node已经逐渐支持es2015以上的新特性
nodemon的作用是监听文件代码的变动,自动重启
下载方式 cnpm i -g nodemon
let,const和var的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sChS9ND2-1597674511391)(D73EA6E006584F0785348448336FD73F)]
let 与块级作用域
作用域:顾名思义,只代码中的某一个成员起作用的范围.
在ES6以前只有两种作用域,分别是全局作用域和函数作用域,在ES2015里又新增了一个块级作用域.“块"指的是代码中用一对花括号”{}"所包裹起来的范围
<!--例如if语句和循环语句-->
if(true){
console.log('这里是块级作用域')
}
for(var i = 0 ; i < 10 ; i++){
console.log(i,'这里是块级作用域')
}
在以前“块”是没有单独的定义的,这就导致我们在块中定义的成员外部也可以访问到
<!--例如-->
if(true){
var a = 123;
}
console.log(a); //==>打印出123
这一点对于复杂代码是非常不利的,也是不安全的
let就是拥有块级作用域的声明变量,通过let声明的变量只能在所声明的代码块中被访问到
<!--例如-->
if(true){
let a = 123;
}
console.log(a); //==>打印出 a is not defind
<!--这说明块级作用域内的声明,在外部是无法使用的-->
块级作用域的这种特性非常适用于for循环当中的计数器,传统循环如果出现循环嵌套的情况就必须为计数器设置不同的名称,否则就会出现问题
for(var i = 0 ; i < 3 ; i++){
for(var i = 0 ; i < 3 ; i++){
console.log(i)
}
console.log('内层结束i=' + i)//当i=3时,不符合外部循环条件,循环停止
}
<!--输出结果0 1 2 内层结束i=3-->
<!-- A,B两个for循环的嵌套,他们声明的变量都是i,双层循环嵌套实际上应该3*3打印9次,但上面就打印了三次,原因是两次循环声明变量用的是var,所以他们的变量i不是不是块级成员,而是全局成员,那内层声明的i就会覆盖掉外层声明的i,那么内层拿到的是3,当i=3时,不符合外部循环条件i<3,循环停止 -->
for(let i = 0 ; i < 3 ; i++){
for(let i = 0 ; i < 3 ; i++){
console.log(i)
}
console.log('内存循环输出i=' + i)
}
<!--输出结果: 0 1 2 内存循环输出i=0 0 1 2 内存循环输出i=1 0 1 2 内存循环输出i=2-->
let就不会有这问题,let所声明的变量只会在当前循环的代码块中生效,i只是当前块级作用域的局部变量,不会影响到外层的变量
除此之外还有一个典型的应用场景—就是我们循环的去注册事件时,在事件的处理函数中访问循环的计数器,这种情况下就会出现一些问题
var elements = [{},{},{}];//每一个空对象都代表一个元素
for(var i = 0 ; i < elements.length ; i++){
elements[i].onclick=function(){
console.log(i)
}
}
elements[0].onclick();//3
elements[1].onclick();//3
elements[2].onclick();//3
<!--这里打印的i实际上是全局作用域的i,当指向函数的时候实际上循环已经执行过后i已经累加到了3,所以不管你打印那个i最后的结果都是3-->
<!--实际上这也是一个闭包的应用场景,我们通过使用闭包也可以解决这类问题-->
var elements = [{},{},{}];//每一个空对象都代表一个元素
for(var i = 0 ; i < elements.length ; i++){
elements[i].onclick = (function(i){
return function(){
console.log(i)
}
})(i)
}
elements[0].onclick();//0
elements[1].onclick();//1
elements[2].onclick();//2
<!--闭包通过借助函数作用域来拜托全局作用域的影响-->
在块级作用域就不用这么麻烦了,我们把声明计数器的var改成let,使我们的i只能在块级作用域内被访问,这样问题自然就解决了,其实这个内部也是一种闭包的机制,在onclick执行的时候循环已经结束了,这时i已经销毁了,只有执行闭包机制我们才能获得这个i
for循环的特别之处–for循环内部有两层作用域
for(let i = 0 ; i < 3 ; i++){
let i = 'foo';
}
<!--输出结果:foo foo foo-->
let i = 0 ;//外部循环的局部变量
for(i = 0 ; i < 3 ; i++){
let i = 'foo';//块级作用域内部的局部变量
}
let 和 var 的区别
let 不会出现声明变量提升的情况
var 会出现声明变量提升的情况
console.log(a);//值为underfined,这里的现象叫变量声明的提升
var a = 1;
------------
console.log(a);//报错:在初始化之前不能访问'a'
let a = 1;
为啥不对var升级?
直接升级var会导致很多原有的工作出现问题,所以出现了let
const 恒量/常量
const可以声明恒量和常量,在let基础上多了一个只读特性,变量一旦声明之后就不能够被修改。
<!--1.const在声明后不能再次被赋值-->
const a = 1;
a = 2;
// TypeError: Assignment to constant variable.类型错误:赋值给常量变量。
<!--2.const声明时必须设置初始值,声明和赋值不能像var一样,放到两个语句当中-->
const a ;
a = 1;
// SyntaxError: Missing initializer in const declaration.在const声明中缺少初始化器
const声明的成员不允许被修改,只是说不允许声明过后重新指向一个新的内存地址,并不是说不允许修改恒量中的属性成员
const obj = {};
obj.name = 'hh';
<!--这种情况并没有修改obj所指向的内存地址,只是修改了这块内存空间当中的数据,所以说是被允许的-->
<!--那么相反如果我们把obj指向一个新的空对象此时就会报错,因为这种赋值会改变obj的内存指向-->
obj = {};
// TypeError: Assignment to constant variable.类型错误:赋值给常量变量。
++最佳实践:不用var,主用const,配合let++
因为var有很多陋习,如先使用变量再去声明变量,主要使用const能让我们更明确代码中声明的变量会不会被修改
数组的解构
// 数组的结构
const arr = [100,200,300];
// 以前获取数组中指定的元素需要通过索引获取对应的值.如下:
const a = arr[0];
const b = arr[1];
const c = arr[2];
console.log(a,b,c);//100 200 300
// 现在通过解构的方式去快速提取数组中指定的成员
const arr = [100,200,300];
const [a,b,c] = arr;//以前设置变量名的地方放一个[],然后在[]里面是从数组中提取出来的数据所存放的变量a,b,c,那内部按照变量名出现的位置分配数组中所对应的位置的数组
console.log(a,b,c);//100 200 300
// 如果想获取第三个成员
const arr = [100,200,300];
const [,,c] = arr;//就把前两个变量去掉,保留逗号,确保解构的格式与数组是一致的
console.log(c);//300
const arr = [100,200,300];
const [a,...d] = arr;//三个点表示提取从当前位置往后的所有成员
console.log(d);//[ 200, 300 ]
// 需要注意的是,这种三个点的用法只能在解构的最后一个位置使用
// 解构位置的成员少于数组长度,就会从前到后的顺序去提取,后面的多出来的成员就不会被提取
const arr = [100,200,300];
const [a] = arr;
console.log(a);//100
//相反呢,解构位置的成员大于数组的长度,多余的成员会返回undefined
const arr = [100,200,300];
const [a,b,c,d] = arr;
console.log(a,b,c,d);//100 200 300 undefined
//给解构成员设置默认值,在解构位置变量后面跟上=并附上默认值,那么当我们没有提取到数组当中对应的成员,那么这里就会得到默认值
const arr = [100,,300];
const [a,b='bbb',c,d='ddd'] = arr;
console.log(a,b,c,d);//100 bbb 300 ddd
// 实例:截取字符串中的某一个值
// 以前的做法
const str = '/a/b/c';
const arr = str.split('/');
const a = arr[1];
console.log(a);//a
// 现在的做法
const [,rabt] = arr;
console.log(rabt);//a
对象的解构
对象的解构是需要根据属性名去匹配提取的为不是位置
// 解构对象
const obj = {name:'hcb',age:'25'};
const { name , age } = obj;//在花括号里同样是提取出来的数据所存放的变量名,不过变量名有一个很重要的作用,那就是去匹配所对应的成员,从而去提取指定成员的值
console.log(name,age);//hcb 25
const obj = {name:'hcb',age:'25'};
const { age } = obj;//age就是提取了obj对象age属性的值
console.log(age);//25
解构对象的其他特点基本上是和解构数组完全一致的,例如没有匹配到的成员返回underfined,可以设置默认值等。
解构中的特殊情况:解构中的变量名是用来匹配被解构对象中的属性名的,当当前的作用域当中有同名的变量名就会产生冲突
const obj = {name:'hcb',age:25};
const age = 27;
const { age } = obj;//因为obj里的age属性必须通过age去提取,所以这里必须使用age去提取,这样的话这一冲突就不可避免
console.log(age);
// SyntaxError: Identifier 'age' has already been declared
// SyntaxError:标识符'age'已经被声明
//使用重命名的方式解决变量名冲突的问题
const obj = {name:'hcb',age:25};
const age = 27;
const { age:objAge } = obj;
console.log(objAge);//25
//在重命名后需要使用默认值
const obj = {name:'hcb'};
const age = 27;
const { age:objAge = 26} = obj;
console.log(objAge);//26
//解构对象的更多用法
const { log } = console;
log(1);//
log('abb');//abb
//这样一来简化了代码的编写,整体的体积也会减小很多
模板字符串字面量
模板字符串需要用反引号``去标识
//如果在字符串当中需要使用反引号可以使用\转义
const str = `this is a \`apple\``;
console.log(str);//this is a apple
新特性
传统的字符串并不支持换行,模板字符串支持多行字符串,对于输出HTML字符串非常方便
const str = `<div>
<p>this is a apple</p>
</div>
`;
console.log(str)
<div>
<p>this is a apple</p>
</div>
模板字符串还支持通过插值表达式 的方式在字符串中嵌入所对应的数值
const thing = 'apple';
const str = `this is a ${thing}`;
console.log(str);//this is a apple
这里的${}是标准的JavaScript,这里不仅可以嵌入变量,还可以嵌入任何标准的js语句,这个表达式返回的结果最终会输出在字符串当中插值表达式的位置
const str = `这是${1 + 1 }个苹果`;
console.log(str);这是2个苹果
模板字符串标签函数–模板字符串的高级用法
在定义模板字符串之前添加一个标签,这个标签实际上就是一个特殊的函数,添加这标签就是调用这个函数
const str = console.log`abc`;//[ 'abc' ]
//为啥用console.log会打印一个数组呢?
//尝试定义一个标签函数
先定义一个标签和变量,然后定义一个使用标签函数的模板字符串,使用这个函数之前要先定义这个函数
const name = 'hcb';
const gender = true;
function myTagFunc(strings){//函数接受数组参数
console.log(strings);//[ '', ' is a ', '' ]
//打印出来发现,这个数组的内容就是模板字符串内容分割过后的结果
//因为这个模板字符串中可能会有表达式,这里的数组就是按照表达式分割过后
//那些静态的内容,所以是一个数组
}
const result = myTagFunc`${name} is a ${gender}`;
//除了数组以外,这个函数还可以接收到所有模板字符串当中表达式的返回值,如name和gender
const name = 'hcb';
const gender = true;
// function myTagFunc(strings,name,gender){//函数接受数组参数
// console.log(strings,name,gender);//[ '', ' is a ', '' ] 'hcb' true
// }
//等同于
function myTagFunc(strings,...values){//函数接受数组参数
console.log(strings,...values);//[ '', ' is a ', '' ] 'hcb' true
}
const result = myTagFunc`${name} is a ${gender}`;
//那么这个 函数的返回值就是带标签的模板字符串所对应的返回值
const name = 'hcb';
const gender = true;
function myTagFunc(strings){//函数接受数组参数
const sex = gender?'boy':'girl';
return name + strings[1] + sex;
}
const result = myTagFunc`${name} is a ${gender}`;
console.log(result);//hcb is a boy
标签函数的作用实际上就是我们对模板字符串的加工.我们可以利用标签函数这一特性去实现模板的多语言化,比如翻译成中文或英文,或者是检查我们的模板字符串当中是否存在一些不安全的字符的需求,甚至可以使用这种特性来去实现小型的模板引擎
字符串的扩展方法
常用方法 返回值为true或false
- includes(‘某某’) 判断字符串当中是否包含某某
- startsWith(‘某某’) 判断字符串是不是某某开头
- endsWith(‘某某’) 判断字符串是不是某某结尾
以上这些方法可以更方便的判断字符串中是否包含指定的内容,相比于indexOf和正则表达式查找会便捷很多!
注意:以上方法严格区分大小写!
const message = `Where are you going?`;
console.log(
message.includes('you'),
message.startsWith('Where'),
message.startsWith('where'),
message.endsWith('?')
);//true true false true
函数的扩展
参数默认值
es2015当中为函数的形态列表扩展了一些特别有用新的语法
- 参数的默认值
//函数参数的默认值--传统写法
function foo(enable){
// 以前想要为函数的参数去定义默认值,我们需要在函数体中通过逻辑代码来实现,例如foo函数的参数enable,我们需要是enable的默认值是true,那么我们需要通过逻辑判断来决定是否使用默认值。
// enable = enable || true;//短路运算法
// 在这里你会发现人们经常会犯错的地方,因为很多人喜欢用短路运算的方式设置默认值,在这个函数的情况下是不能通过短路运算的方式设置默认值,因为这样会导致如果我们传入false的时候也会 使用默认值,那这事很明显的错误,正确做法是判断enable是否等于undefined,然后使用默认值
enable = enable === undefined ? true : enable;
console.log('foo invoked - enable')
console.log(enable)
}
foo(false);
//es2015函数的参数默认值
//函数参数的默认值--es2015写法
function foo(enable = true){//这里传递的默认值会在我们没有传递实参或undefined的时候使用
console.log('foo invoked - enable')
console.log(enable)
}
foo(false);
需要注意的是,当有多个参数的时候,带有默认值的形参一定要出现在参数列表的最后,因为我们的参数是按照次序传递的,如果带有默认值的参数不放在最后的话,那我们的默认值将无法工作
//多个默认值的错误写法
function foo(enable = true,name){
console.log('foo invoked - enable')
console.log(enable,name)
}
foo('hcb');
//多个参数的正确写法,带有默认值的参数放在最后
function foo(name,enable = true){
console.log('foo invoked - enable')
console.log(name,enable)
}
foo('hcb');
剩余参数 …
在ECMAscript中很多方法都可以传递任意个数的参数如console.log()方法,
对于未知的参数,以前使用arguments对象去接收,arguments实际上是一个伪数组,在es2015新增了…操作符,这种操作符有两个作用
- rest作用:剩余操作符
//对于未知参数使用arguments对象接收
function foo(){
console.log(arguments)
}
// arguments对象实际上是一个伪数组;
foo(1,2,3,4);//{ '0': 1, '1': 2, '2': 3, '3': 4 }
//...操作符,rest作用
//...操作符接收当前位置开始往后多有的实参,只能用一次,只能出现在形参的最后一位
function foo(str,...args){
console.log(args)
}
// ...操作符意数组的形式返回函数传递的参数
foo('数数',1,2,3,4);//数数 [ 1, 2, 3, 4 ]
展开数组 …
- …操作符除了收齐剩余数据的用法还有种spread(展开)用法,展开操作符的用途很多先来了解一下与函数相关的数组参数展开
const arr = ['foo','bar','baz'];
//想把数组中的每个成员按照次序传递给console.log方法
console.log(
arr[0],
arr[1],
arr[2]
);//foo bar baz
//如果数组中的元素个数是不固定的,那一个个传的方式就行不通了
//以前使用函数方法的apply方法去调用函数,因为这种方法可以以数组的形式接收我们的实参列表
console.log.apply(console,arr);//foo bar baz
//apply的第一个参数时this指向,因为log方法是console调用的,这里指向的是console对象的本身,第二个参数是我们需要传递的实参列表这样一个数组
// es2015方法通过...的方法展开数组,大大简化了操作
console.log(...arr);//foo bar baz
箭头函数
es2015简化了函数表达式的定义方式
//传统的定义方式
function inc(number){
return number + 1
}
const num = inc(1);
console.log(num)
//es2015定义函数的方式
const inc = n =>n+1;//这里使用的是编程中的连体字符Fira Code,简写,默认返回箭头后面的值
console.log(inc(100));//101
const inc2 = (n,m) => n + m;//多个参数
const inc3 = (n,m) =>{//多行代码需要自己手动写return
if(n-m>0){
console.log(`${n}>${m}`)
}else{
console.log(`${n}<=${m}`)
}
return n+m;
}
//使用箭头函数会让我们的代码更简短更易读
const arr = [ 1,2,3,4,5,6 ];
const filterArr = arr.filter(v => v%2);//过滤取余
console.log(filterArr);//[ 1, 3, 5 ]
箭头函数与this指向
相比于普通的函数,箭头函数还有一个重要的变化就是不会改变this的指向
const person = {
name:'tom',
sayHi:function(){
// 传统写法,this指向调用这个方法的对象
console.log(`Hi,my name is ${this.name}`);//Hi,my name is tom
},
// es2015箭头函数当中没有this机制,不会改变this的指向,也就是说在我们箭头函数的外面this是什么,那么在里面拿到的就是什么
sayHello:()=>{
console.log(`Hello,my name is ${this.name}`);
// 上面代码中,person.sayHello()方法是一个箭头函数,这是错误的。调用person.sayHello()时,如果是普通函数,该方法内部的this指向person;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致sayHello箭头函数定义时的作用域就是全局作用域。
},
sayHiAsync:function(){
const _this = this;
setTimeout(function(){//这个函数最终会被放在全局对象里面调用,所以this拿不到当前作用域里面的对象,拿到的是全局对象
//因为是调用setTimeout函数的对象是window对象,所以name是undefined
// 以前解决这种问题的方法就是定义一个_this来保存当前作用域的this
console.log(`Hi,my name is ${_this.name}`);//Hi,my name is undefined
},500)
//es2015的做法是使用箭头函数不改变this的指向
setTimeout(()=>{
console.log(this.name);//tom
},1000)
}
};
person.sayHi();
person.sayHello();
person.sayHiAsync();
对象字面量的增强
const bar = 456
const obj = {
foo:123,
// bar:bar,//传统写法
bar,//es6写法,同上等价
// method:function(){
// console.log('method',this.foo)
// },//传统写法
method(){
console.log('method',this.foo)
},//es6写法,同上等价,和普通的function函数的this的指向一样,内部的this指向当前对象
//另外对象最大的变化时可以使用表达式的返回值作为对象的属性名
//传统直接添加动态属性名会报错
// Math.random():789//SyntaxError: Unexpected token .
// es2015后可以直接[]添加动态属性名,这种属性名叫做计算属性名
[Math.random()]:789
}
// es5通过索引的方式添加属性名
// obj[Math.random()] = 789;
console.log(obj);//{ foo: 123,bar: 456,method: [Function: method],'0.988395699398273': 789 }
obj.method();//method 123
对象的扩展方法
Object.assign()方法:将多个源对象的属性复制到目标对象当中,如果对象中有相同的属性,源对象中的属性就会覆盖掉目标对象中的属性
const source1 = {
a:123,
b:456
}
const source2 = {
b:147,
d:555
}
const target = {
a:456,
c:789
}
const result = Object.assign(target,source1,source2);//相同属性后面的对象会覆盖前面的对象
console.log(target);//{ a: 123, c: 789, b: 147, d: 555 }
console.log(target === result);//true
复制对象的特性可以帮助我们在修改当前对象数据的时候不会修改对象内存地址的数据
function func(obj){
// obj.name = 'hcb';//在函数中修改对象的属性值,那外界的对象也会发生变化,因为他们都指向同一个内存地址,也就是同一个数据
const funcObj = Object.assign({},obj);//复制对象后就是一个全新的对象,修改的数据不会修改源对象的内存地址的数据
funcObj.name = 'hcb';
console.log(funcObj)
}
const obj = {name:'ddc'};
func(obj);//{ name: 'hcb' }
console.log(obj);//{ name: 'ddc' }
// 浅拷贝
const obj = {a:{a:'ccc'},b:123};
const obj2 = Object.assign({},obj);
obj2.a.a = 'ddd';
console.log(obj);//{ a: { a: 'ddd' }, b: 123 }
//深拷贝
const nObj = {a:'hcb',b:24};
const nObj2 = Object.assign({},nObj);
nObj2.a = 'dg';
console.log(nObj);//{ a: 'hcb', b: 24 }
Object.is方法:用来判断两个值是否相等
//传统判断数同值相等的方法
console.log(0 == false);//true,两等运算符会比较时自动转换数据类型
console.log(0 === false);//false,//三等会严格比较值和数据类型
console.log(-0 === +0);//true,严格等于不能区分-0和+0
console.log(NaN === NaN);//false,以前认为NaN是任意类型的值,所以严格等于的情况下是false
// es2015判断同值的方法
console.log(Object.is(-0,+0));//false,能判断-0和+0的不等
console.log(Object.is(NaN,NaN));//true,//现在认为NaN是一个特殊的值,所以相等
Proxy(破乳阿 科c) 代理对象
如果我们想监视某个对象的读写,可以使用Object.defineProperty()方法来为我们的对象添加属性,这样就可以捕获到对象属性的读写过程,这种方法使用非常广泛,在vue3.0之前的版本就是使用Object.defineProperty()的方法实现的数据响应,从而完成双向数据绑定
在es2015又专门设置了一个Proxy类型,专门为对象设置访问代理器的,Proxy可以轻松监视到对象的读写过程,相比于Object.defineProperty(),Proxy更加强大,更加方便
下面看如何使用Proxy:
const person = {
name:'hcb',
age:24
}
// 创建Proxy代理对象,第一个参数:代理的目标对象,第二个参数:代理的处理对象
const ProxyPerson = new Proxy(person,{
get(target,property){//监听代理对象调用属性名,第一个参数:代理的目标对象,第二个参数:外部访问时访问的属性名
console.log(target,property);
//正确的做法是先去判断代理对象是否存在这样一个属性,如果存在就返回值,如果不存在就返回undefined
return property in target ? target[property] : undefined
},
set(target,property,value){//三个参数:代理目标对象,写入的属性名称,写入的属性值
console.log(target,property,value);
// 先进行校验
if(property === 'age'){
if(!Number.isInteger(value)){//当属性名是age时,判断一下值是否是int型,不是就报错
throw new TypeError(`${value} is not an int`)
}
}
//为代理目标设置属性名和属性值
target[property] = value;
}
})
// 通过代理对象为person写入一个真的属性
ProxyPerson.gender = true;
// ProxyPerson.age = '444';//set函数就会报错
ProxyPerson.age = 444;
console.log(ProxyPerson.name);//hcb
console.log(ProxyPerson.cc);//undefined
Proxy vs Object.defineProperty()
- Proxy更加强大一些,defineProperty只能监视属性的读写,而Proxy能够监视到更多对象操作,如deleteProperty或对对象方法的调用等等
//delete对象操作监视
const person = {
name:'hcb',
age:27
}
const personProxy = new Proxy(person,{
deleteProperty(target,property){
// 代理对象删除属性的方法,参数:1.需要进行删除操作的目标对象;2.要删除的属性
delete target[property]
}
})
delete personProxy.age;
console.log(person);{ name: 'hcb' }
除了delete操作还有很多对象操作,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9FUVvQvL-1597674511392)(77944BA56DB0477A89613FC308C73C68)]
- Proxy更好的支持数组对象的监视,defineProperty常见数组用法就是重写数组的操作方式指通过自定义的方式去覆盖掉数组的原型对象的push等方法,以此来劫持对应这个方法调用的过程
//如何用Proxy对象对数组进行监视
const list = [];
const listProxy = new Proxy(list,{
set(target,property,value){//三个参数:目标对象,数组下标,设置的值
console.log('set',property,value);
target[property] = value;
return true;//表示设置成功
}
})
listProxy.push(10);//set 0 10.表示Proxy可以根据push操作推算出来当前添加的值所处的下标,并对整个目标数组进行调整设置
// listProxy.splice(0,0,9)
/**
* set 1 10
* set 0 9
* set length 2
* */
listProxy.splice(1,0,9)
/**
* set 1 9
* set length 2
* */
console.log(list);//[ 10, 9 ]
- Proxy以非侵入的方式监管了对象的读写
也就是对一个已经定义的对象,Proxy不需要对象本身进行任何的操作就可以监视到对象的读写
const person = {
name:'hcb',
age:24
}
const personProxy = new Proxy(person,{
get(target,property){
return target[property]
},
set(target,property,value){
target[property] = value;
}
})
personProxy.name = 'tom';
console.log(personProxy.name)
而Object.definePropert需要用特定的方式单独去定义那些需要被监视的属性,对于一个已经存在的属性,需要做很多额外的操作
// Object.defineProperty以入侵的方式监管对象的读写
const person = {}
Object.defineProperty(person,'name',{
get(){
console.log('name 被访问')
return person._name
},
set(value){
console.log('name 被设置')
person._name = value;
}
})
Object.defineProperty(person,'age',{
get(){
console.log('age 被访问')
return person._age
},
set(value){
console.log('age 被设置')
person._age = value;
}
})
person.name = 'hcb';
console.log(person,person.name);//{ _name: 'hcb' } hcb
Reflect (瑞弗莱 克t) 统一的对象操作API
Reflect是es2015提供的一个全新的静态对象,是一个静态类,它不能通过 new Reflect() 的方法去构建一个实例对象,只能够调用静态类中的一些静态方法,和Math对象差不多.
Reflect内部封装了一系列对对象的底层操作,共13个,这13个方法的方法名与Proxy对象的Hander对象中的方法名是一致的,Reflect的成员方法其实就是对Proxy的处理对象的方法的默认实现,如get和set方法在不配置时,默认调用Reflect的get和set方法
const person = {
name:'hcb',
age:24
}
const personProxy = new Proxy(person,{})
// 相当于
const personProxy2 = new Proxy(person,{
get(target,property){
return Reflect.get(target,property)
},
set(target,property,value){
Reflect.set(target,property,value)
}
})
personProxy.name = 'tom';
console.log(personProxy.name,personProxy2.name)
Reflect的价值在于它统一提供了一套用于操作对象的API,体验更为合理
//按照传统方式-方法不统一
const obj = {
name:'hcb',
age:24,
sex:'男'
}
console.log('name' in obj);//判断对象是否有这个属性名---操作符方式
console.log(delete obj['age']);//删除对象的这属性名--对象中的方法
console.log(Object.keys(obj));//获取当前对象的所有属性名
//es2015做法
const person = {
name:'bg',
age:27,
sex:'男'
}
console.log(Reflect.has(person,'name'));//判断当前对象是否有这个属性
console.log(Reflect.deleteProperty(person,'age'));//删除当前对象的属性
console.log(Reflect.ownKeys(person));//获取当前对象的所有属性名
Promise
Promise也是es2015提供的一个内置对象,提供了一种全新的异步解决方案,通过链式调用的方式解决了传统异步编程中回调函数嵌套过深的问题
class 类
在此之前ECMAscript都是通过定义函数以及函数的原型对象实现的类型
function Person(name){
this.name = name;//通过this去访问当前的实例对象
}
// 在实例之间共享一些成员使用prototype
Person.prototype.getName = function(){
console.log(`Hi,my name is ${this.name}`)
}
const p = new Person('hcb');
p.getName();
自从es2015开始,我们就可以使用class关键词声明一个类型,这种独立定义类型的语法相比较之前函数的语法更容易理解,结构也更清晰一点
// 1.定义Person类型
class Person {
//2.在构造函数添加一些额外的逻辑,使用constructor构造函数方法
constructor(name){
this.name = name;//使用this访问当前的实例对象
}
//3.定义类的方法
say(){
console.log(`Hi,my name is ${this.name}`);
}
}
const p = new Person('hcb');
p.say();
静态方法
在我们类型当中的方法一般分类实例方法和静态方法,实例方法需要通过这个类型构造的实例对象去调用,而静态方法则直接通过类型本身去调用
在es2015中新增添加静态成员的static关键词
// 1.定义Person类型
class Person {
//2.在构造函数添加一些额外的逻辑,使用constructor构造函数方法
constructor(name){
this.name = name;//使用this访问当前的实例对象
}
//3.定义类的方法
say(){
console.log(`Hi,my name is ${this.name}`);
}
//定义静态方法
static create(name){
return new Person(name)
}
}
const p = Person.create('hcb');
p.say();
类的继承 extends
// 1.定义Person类型
class Person {
//2.在构造函数添加一些额外的逻辑,使用constructor构造函数方法
constructor(name){
this.name = name;//使用this访问当前的实例对象
}
//3.定义类的方法
say(){
console.log(`Hi,my name is ${this.name}`);
}
//定义静态方法--创建Person类型的实例
static create(name){
return new Person(name)
//需要注意的是,因为静态方法是挂载在类型上面的,所以静态方法内部的this不会指向实例对象,而是当前的类型
}
}
// 继承Person类
class PersonInfo extends Person {
constructor(name,age){
super(name);//继承父类的构造函数中的逻辑属性
this.age = age;
}
say(){
super.say();//继承父类的方法
console.log(`My age is ${this.age}`);
}
}
const info = new PersonInfo('tom',18);
info.say();
Set数据结构
Set可以理解为一种集合,与传统的数组非常类似,不过Set的值是不能重复的,每个值在同一个Set当中都是唯一的,set是一个类型
const s = new Set();
s.add(1).add(2).add(3).add(4).add(2);//add方法添加值 由于Set.add()方法可以返回集合本身,可以链式调用,如果添加重复的值,那么重复的值会被忽略掉
console.log(s);//{ 1, 2, 3, 4 }
s.forEach((i)=>console.log(i));//遍历set集合
for (let i of s) {//遍历set集合
console.log(i)
}
console.log(s.size);//Set的长度,相当于length
console.log(s.has(2));//判断set集合是否包含这个值,返回值是true或false
console.log(s.delete(4));//删除set集合中指定的值,删除成功返回true
s.clear();//清除set集合当中的全部内容
console.log(s);
set常见的应用方式是为数组进行去重
const arr = [1,2,3,4,5,1,5];
const result = new Set(arr);//数组去重
console.log(result);//Set { 1, 2, 3, 4, 5 }
const resultToArr = Array.from(result);//Set集合转数组
console.log(resultToArr);//[ 1, 2, 3, 4, 5 ]
Map数据结构
Map的数据结构和对象有所类似,本质上都是键值对集合,但是对象的键只能是字符串类型,而Map的键可以是任意类型
const obj = {};
obj[true] = 'value';
obj[132] = 'value';
obj[{a:1}] = 'value';
console.log(Object.keys(obj));//[ '132', 'true', '[object Object]' ],所有键都被转换成了字符串
Map是严格意义上的键值对集合,用来去映射两个任意类型数据之间的对应关系
const m = new Map();//创建Map实例
const hcb = {name:'hcb'};
m.set(hcb,18);
m.set(123,456)
console.log(m);//Map { { name: 'hcb' } => 18 }
console.log(m.get(hcb));//获取指定键的值
console.log(m.has(hcb));//判断某个键是否存在
console.log(m.delete(hcb));//删除Map中指定的键值
m.clear();//清空所有键值
console.log(m)
// 遍历Map用forEach方法
m.forEach((value,key)=>{
console.log(value,key)
})
Map与对象最大的区别就是可以用任意类型作为键,而对象只能使用字符串作为建
Symbol
Symbol(符号)表示一个独一无二的值,主要用于解决对象属性值命名相同引起冲突的问题,Symbol目前最主要的作用就是为对象添加一个独一无二的属性标识符(属性名)
//Symbol是数据类型,独一无二,永远不会重复
const sym = Symbol();
console.log(typeof sym);//数据类型 symbol
console.log(Symbol() === Symbol());//false,Symbol永远是唯一的
//考虑到开发时的调试,Symbol允许我们添加一个字符串作为标识文本,这样对于多次使用Symbol的时候就可以在控制台区分出哪个是对应的Symbol
console.log(Symbol('abc'));//Symbol(abc)
console.log(Symbol('def'));//Symbol(def)
console.log(Symbol('ghi'));//Symbol(ghi)
es2015以前解决对象属性名冲突问题的方式是约定
// share.js
const cache = {}
// a.js
cache['a_foo'] = Math.random()
//b.js
cache['b_foo'] = 123
console.log(cache);//{ a_foo: 0.029474284031667963, b_foo: 123 }
es2015以后通过symbol解决对象属性名冲突的问题
// share.js
const cache = {}
// a.js
cache[Symbol('foo')] = Math.random()
//b.js
cache[Symbol('foo')] = 123
console.log(cache);//{ [Symbol(foo)]: 0.4502646589114949, [Symbol(foo)]: 123 }
es2015开始,对象可以使用Symbol作为属性名
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj);//{ [Symbol()]: '123', [Symbol()]: '456' }因为Symbol都是独一无二的,不用担心冲突的问题
// 使用计算属性名的方式,直接在对象字面量中使用Symbol作为属性名
const obj2 = {
[Symbol()]:'123'
}
console.log(obj2);//{ [Symbol()]: '123' }
借助这种特性去模拟实现对象的私有成员,
// 借助这种特性去模拟实现对象的私有成员,
// 所谓私有成员就是指程序内部可以访问,但外部无法直接访问的成员
const name = Symbol();
const person = {
[name]:'hcb',
sayName(){
console.log(this[name]);
return this[name];
}
}
const myName = person.sayName();//hcb
console.log(myName);//hcb
Symbol 补充
- Symbol的唯一性
//不管Symbol传入的值是否相同,Symbol都是不相等的
console.log(
Symbol('foo') === Symbol('foo')
);//false
如果想在全局复用一个相同的Symbol值,可以使用全局变量的方式实现,或者使用Symbol类型提供的静态方法实现
//Symbol.for()方法可以接收一个字符串作为参数,相同的字符串就一定会返回相同的Symbol类型的值
// 这个方法内部维护了一个全局注册表,为我们的字符串和Symbol值提供了一个一一对应的关系
const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1,s2,s1 === s2);//Symbol(foo) Symbol(foo) true
// 需要注意的是,在这个方法之中维护的是字符串和Symbol直接的对应关系,也就是说如果我们传入的不是字符串,那这个方法会把它转化成字符串,如下
console.log(
Symbol.for(true) === Symbol.for('true')
);//true
- Symbol的属性
// 而且在Symbol当中提供了很多内置的Symbol常量属性,用来去作为内部方法的标识,这些标识符可以让自定义对象去实现js的内置接口
console.log(Symbol.iterator);//Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for...of 循环使用。
console.log(Symbol.hasInstance)//Symbol.hasInstance用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。
const obj = {}
// console.log(obj.toString());//[object Object]==>Object叫做对象的自定义标签
// 用Symbol去修改标签
const obj2 = {
[Symbol.toStringTag]:'XObject'
};
// console.log(obj2.toString());//[object XObject]
// 通过Symbol值作为属性名的一些特性
const obj3 = {
[Symbol()]:'symbol value',
foo:'normal value'
}
// 1.在for...in中是无法拿到的,
for(let key in obj3){
console.log(key)
}
// 2.通过Object.keys()方法也获取不到Symbol值属性名
console.log(Object.keys(obj3));//[ 'foo' ]
// 3.通过JSON.stringify序列化对象为的字符串的情况下,symbol值的属性也会被忽略掉
console.log(JSON.stringify(obj3));//{"foo":"normal value"}
// 总之以上特性使得Symbol值特别适合作为对象的私有属性
// 当然想要获取到Symbol值属性名也是有方法的,Object.getOwnPropertySymbols可以获取到Symbol值属性名,但是获取不到普通属性名
console.log(Object.getOwnPropertySymbols(obj3));//[ Symbol() ]
for…of循环
ECMAscript中遍历数据的多种方法:
- for循环:比较适合遍历普通的数组
- for…in循环:遍历键值对
- 函数式的遍历方法 比如一些对象的遍历方法如forEach
以上这些遍历方法都有一定的局限性,所以es2015借鉴多种语言引入了一种全新的方式叫做 for…of语言,作为遍历所有数据结构的统一方式,只要明白for…of的工作原理就可以遍历任意一种数据结构
for…of循环的基本用法
const arr = [100,200,300,400];
for(const item of arr){
console.log(item);
}
//这种方法取代了数组的forEach方法
arr.forEach(item=>{
console.log(item)
})
//想比forEach方法,for...of循环可以使用break中止循环
for(const item of arr){
if(item > 200){
break;
}
console.log(item)
}
arr.forEach()//不能中止遍历跳出循环
以前为了中止遍历必须使用数组实例的some和every方法
some方法返回return true中止遍历
const isYes = arr.some(item=>{
if(item >100){
console.log(item)
return true;
}else{
console.log(item)
}
})
console.log(isYes)
// every返回 return false中止遍历
const isNoc = arr.every(item =>{
if(item <300){
console.log(item)
return true;
}else {
return false;
}
})
console.log(isNoc)
// 遍历Set
const s = new Set(['foo','bar']);
for(const item of s){
console.log(item)
}
// 遍历Map
const m = new Map();
m.set('foo',123);
m.set('bar',456);
for(const item of m){
console.log(item)
}
for(const [key,value] of m){
console.log(key,value);
}
// 遍历对象
const obj = {foo:123,bar:456};
for(const item of obj){//报错obj is not iterable,对象不能迭代
console.log(item)
}
for…of说是可以遍历所有的数据类型,那为啥连最基本的对象都不能遍历呢?
可迭代接口
上篇说到 for…of循环是es2015最新推出的一种语法,是一种所有遍历所有数据结构的统一方式,但是呢,我们尝试发现它只能遍历一些数组类的数据结构,如果我们尝试遍历普通对象就会报出一种错误,那这 究竟是我说错了还是说有什么原因呢?
其实是这样的,ECMAscript用于表示有结构的数据类型越来越多,从最早的数组和对象到现在的Set和Map,而且我们开发者还可以组合数据类型去定义一些符合业务需求的数据结构,那为了提供一种统一的遍历方式,es2015就提供了Iterable接口,意思是可迭代的,可以把它理解成一种规则标准
例如在ECMAscript当中,任意一种类型都有toString方法,这就是因为它们都实现了统一的规格标准,而在编程语言当中更专业的说法就是他们都实现了统一的接口
那可迭代接口就是可以被for…of统一遍历访问的规则标准,换句话说,只要这个数据结构实现了可迭代接口,它就可以被for…of遍历,那也就是说我们之前尝试了for…of的数据类型,它都已经实现了可迭代(Iterable)接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3JFUlai-1597674511393)(82FA34AF15D7499584453419CD29EAF4)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JRjK7imo-1597674511394)(D0A44CFD655C44DA850BA30BA4B92195)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vl2VxzAW-1597674511395)(E681E0ED596F48FDAAF24926D4183C69)]
第一,以上三个数据类型可以被for…of遍历是以为他们的__proto__里都有Symbol(Symbol.iterator)这个方法;第二,这个方法的名字叫做iterator.那根据这两个理由可以确定,Iterable接口就是我们对象当中必须要挂在一个iterator的方法
Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for…of 循环使用。
iterator的方法是干什么的呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ElvtHt7T-1597674511396)(5BB7AEB68BFA4058896FD3F187CDB87C)]
我们得到一个结论,所有被 for…of遍历的数据类型都要实现 Iterable接口,它的内部必须挂载一个iterator方法,这方法返回一个带有next方法的对象,我们不断调用next()方法就可以实现对内部数据的遍历,下面我们演示一下
const s = new Set(['foo','bar','hcb']);
const iterator = s[Symbol.iterator]();//调用set对象的迭代方法,获取迭代器对象
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
/**
* 返回结果:
* { value: 'foo', done: false }
* { value: 'bar', done: false }
* { value: 'hcb', done: false }
* { value: undefined, done: true }
* { value: undefined, done: true }
**/
//其实这就是for...of循环的工作原理
实现可迭代接口
/**
* 实现逻辑:三层对象,最外层对象实现了可迭代接口(Iterable),
* 这个接口约定了内部必须有一个返回迭代器的iterator方法,
* iterator方法返回的对象实现了迭代器接口(iterator),
* 这个接口约定了内部必须有一个用于迭代的next方法,
* 在next方法中返回的对象实现的是迭代结果接口(IterationResult),
* 这个接口约定的是我们必须有一个value属性,表示当前迭代的数据,值可以是任意 类型,
* 除此之外还必须有一个done属性,值是布尔值 ,用于表示迭代是否结束
* */
// const obj = {
// //使计算属性名的方式定义到对象当中
// [Symbol.iterator]:function(){
// //在iterator方法的内部返回一个实现迭代器接口的对象,也就是说在这个对象中要提供一个next方法,实现向后迭代的逻辑
// return {
// next:function(){
// //在next方法内部返回一个迭代结果对象,这个对象有两个成员分别是value和done
// return {
// value:'hcb',
// done:true
// }
// }
// }
// }
// };
const obj = {
store:['foo','bar','hcb'],
//使计算属性名的方式定义到对象当中
[Symbol.iterator]:function(){
//在iterator方法的内部返回一个实现迭代器接口的对象,也就是说在这个对象中要提供一个next方法,实现向后迭代的逻辑
// 因为要迭代一个数组,需要一个下标
let index = 0;
// 因为next的this并不是obj对象我们 需要定义 一个 变量接收当前this
const self = this;
return {
next:function(){
//在next方法内部返回一个迭代结果对象,这个对象有两个成员分别是value和done
const result = {
value:self.store[index],
done:index >= self.store.length
}
index++;
return result;
}
}
}
};
for(const item of obj){
console.log('循环体',item)
}//循环体 foo 循环体 bar 循环体 hcb
实现迭代器到底有什么用?实现迭代器的目的是什么?
迭代器模式
//迭代器模式
//场景:你我协同开发一个任务清单
//我的代码==============================
const todos = {
life:['吃饭','睡觉','打豆豆'],
learn:['语文','数学','外语'],
work:['喝茶'],
// 创建一个统一的遍历接口,调用者就不用关系数据的内部结构是什么样的,更不用担心我的数据结构内部改变后所产生的影响
each:function(callback){
const arr = [].concat(this.life,this.learn,this.work)
for(const item of arr){
callback(item)
}
},
//创建一个迭代器接口也是一样的,对外提供一个统一的接口
[Symbol.iterator]:function(){
let index = 0;
const arr = [...this.life,...this.learn,...this.work];
return {
next:function(){
return {
value:arr[index],
done:index++ >= arr.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)
// }
//以上我每增加一个一个属性,你都有增加一个for...of循环,很麻烦
// todos.each((item)=>console.log(item))
// for...of迭代对象
for(const item of todos){
console.log(item)
}
迭代器的意义是:迭代器模式的核心就是对外提供统一遍历的接口,让外部不用再关心内部的数据结构是怎么样的.上面案例中的each方法只适用于当前的数据结构,es2015的迭代器是语言层面实现的迭代器模式,它可以适用于任何数据结构,只需要用代码去实现iterator方法,实现迭代逻辑就可以了
生成器 (Generator)
引入这个新的函数的目的在于避免异步编程中回调函数嵌套过深的问题,从而去提供更好的异步编程解决方案,下面我们了解一下生成器的语法以及应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQwfVSEn-1597674511396)(2B188C0975FD4A529B5E819DE8592BD9)]
这说明生成器也实现了迭代器接口协议,一般生成器函数都会搭配yield关键词,yield关键词和return都可以返给函数本身值,但是yield不能中止函数体的运行
// 定义生成器函数就是在普通的function后面跟一个*号
function * foo(){
console.log(1111)
yield 100
console.log(2222)
yield 200
console.log(3333)
yield 300
}
const generator = foo();//生成器函数会帮我们返回一个生成器对象,调用这个生成器对象的next方法才会让这个函数的函数体开始执行,在执行过程中一旦遇到yield关键词,函数就会暂停下来,而且yield后面的值将会作为函数的结果返回,如果继续调用next方法,函数就会从暂停的位置继续执行,周而复始一直到这个函数完全结束,next方法返回的done的值也会变成true
// 普通函数会先打印hcb再打印generator,但是生成器函数只打印出一个Generator对象
console.log(generator);
console.log(generator.next());//打印一次出现一次说明函数yield后面的语句还没有执行,因为如果执行力它就会把后面全部打印出来
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
/**
* Object [Generator] {}
* 1111
* { value: 100, done: false }
* 2222
* { value: 200, done: false }
* 3333
* { value: 300, done: false }
* { value: undefined, done: true }
* */
* */
生成器函数的最大特点就是惰性函数,抽一下,动一下
生成器应用
//案例:发号器
function * createIdMaker(){
let id = 1;
while (true){//不用担心会出现不断循环的问题
yield id++;//yield每执行一次都会暂停函数体
}
}
const idMaker = createIdMaker();
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
console.log(idMaker.next())
/Generator 应用
//案例1------------------------------------------------------
const todos = {
store:['foo','bar','hcb'],
[Symbol.iterator]:function(){
let index = 0;
const store = this.store;
return {
next:function(){
const result = {
value:store[index],
done:index++ >= store.length
}
return result;
}
}
}
}
for(const item of todos){
console.log(item)
}
// ----------------------------
// 使用Generator替换next函数
const todos = {
store:['foo','bar','hcb'],
[Symbol.iterator]:function * (){
const store = this.store;
for(const item of store){
yield item
}
}
}
for(const item of todos){
console.log('todos',item)
}
// 案例2------------------------------------------------------------
const libarys = {
life:['吃饭','睡觉','打豆豆'],
learn:['上课','读书','学新技术'],
work:['业务','开会'],
[Symbol.iterator]:function *(){
const arr = [...this.life,...this.learn,...this.work];
for(const item of arr){
yield item
}
}
}
for(const item of libarys){
console.log('libary',item)
}
Generator最重要的还是解决异步编程中回调函数嵌套过深的问题
ES Modules(语言层面的模块化规范)
在模块化开发会详细介绍
ECMAscript2016 概述
- Array新增includes方法,可以查找指定的值包括NaN,如果查找到返回true否则返回false,比indexOf方法好用
- 新增指数运算符:这种运算符对于数学密集型应用是一个很好的补充
// 传统指数运算符
console.log(Math.pow(2,10))//相当于2的10次幂
// es2016指数运算符
console.log(2 ** 10)
ECMAscript2017 概述
//ECMAscript2017
const obj = {
foo:'value1',
bar:'value2'
}
//Object.values --------------------------------------------
// 返回对象的所有属性值
console.log(Object.values(obj));//[ 'value1', 'value2' ]
//Object.entries (恩吹斯)--------------------------------------------
// 以数组的形式返回对象的键值对,可以配合for...of遍历
console.log(Object.entries(obj));//[ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]
for(const [key,value] of Object.entries(obj)){
console.log(key,value)
}
/**
* foo value1
*bar value2
* */
// Map函数需要的 就是[ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]这种格式的数组,所以可以借助Object.entries将对象转换成Map类型的对象
console.log(new Map(Object.entries(obj)));//Map { 'foo' => 'value1', 'bar' => 'value2' }
//Object.getOwnPropertyDescriptors --------------------------
// 获取对象当中属性的完整描述信息,主要配合es2015的get,set使用
const p1 = {
a:123,
b:456,
get sum(){
return this.a + this.b
}
}
// console.log('sum',p1.sum);//sum 579
// const p2 = Object.assign({},p1);
// p2.a = 4
// console.log('sum',p2.sum);//sum 579
// 为什么p2的sum的值没有改变的呢?这是因为object.assign在复制对象时把sum当成普通的属性去对待了,所以才会出现这种情况,
// 我们可以使用Object.getOwnPropertyDescriptors属性获取对象的信息
const descriptors = Object.getOwnPropertyDescriptors(p1);
console.log('descriptors',descriptors)
// Objct.defineProperties将对象信息定义到一个新的对象当中
const p2 = Object.defineProperties({},descriptors)
p2.a = 789;
console.log(p2.sum)
// String.prototype.padStart / String.prototype.padEnd --------------------
// ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
// 'x'.padStart(5, 'ab') // 'ababx'
// 'x'.padStart(4, 'ab') // 'abax'
// 'x'.padEnd(5, 'ab') // 'xabab'
// 'x'.padEnd(4, 'ab') // 'xaba'
// 上面代码中,padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
// 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
const books = {
a:5,
b:16,
c:118
}
for(const [name,count] of Object.entries(books)){
console.log(`${name.padEnd(16,'-')}|${count.toString().padStart(3,'0')}`)
}
// 这两个属性的效果就是用给定的字符串去填充目标字符串的开始或结束的位置,知道我们的计算达到指定长度为止
// 函数参数中添加尾逗号
function foo(
name,
age,
){
}
// Async / Await解决异步函数嵌套过深的问题,本质上是使用promise的语法糖