JS与ES6高级编程学习笔记(四)——ECMAScript6 新增语法
一、概述
JavaScript的实现标准是ECMAScript,简称"ES"。主流的浏览器都完整的支持ES 5.1与ES3标准。2015年6月17日,ECMA国际组织发布了ECMAScript的第六版,该版本正式名称为ECMAScript 2015,被称为ECMAScript 6或ES6(泛指ES6及以后的版本)。
ES6是对JavaScript的重大升级。JavaScript虽然应用广泛,但它中间有许多糟粕,在语法、代码组织、面向对象、安全、功能与性能方面的问题都随着时间的推移愈加明显,ES6很大程度上弥补了这些缺陷,增加了像块级作用域的变量声明、箭头函数、解构、Symble、Generator、模块、类等特性,强化了内置对象Array、Object、Math、Number、String的功能,增加了异步流控制与元编程。总之ES6越来越接近我们理想中认为的编程语,更加合理与高效。
二、对象字面量扩展
ES6中增加了一些新的特性允许使用更加简洁的方式定义对象字面量,如对象中属性的定义、方法的定义、使用表达式的作为属性名称、简洁的访问器属性定义及增加了super对象,这些特性极大的方便了对象的创建。
2.1、更简洁的属性定义
ES6允许直接在对象字面量中使用变量,省去键的声明,变量名默认作为键的名称,假若我们要声明如下对象:
var name="jack",age="19";
var user={"name":name,"age":age};
在ES6中可以将"name":name简化成name,更加简洁的属性定义如下:
var name="jack",age="19";
var user={name,age};
可见属性名就是变量名, 属性值就就是变量值。如果属性名与变量的名称不同则必须显式声明。
2.2、更简洁的方法定义
与属性定义一样,方法的定义也可以更加简洁,可以省去function与冒号,假若要定义如下对象:
var obj3={ //ES5 show:function(){ console.log("Hello Function!"); } }; obj3.show();
在ES6中可以简化成如下形式:
var obj4={ //ES6 show(){ console.log("Hello Function!"); } }; obj4.show();
ES6对象字面量中定义方法不仅更加简单,而且将获得更多的特性支持,比如后面将讲到的super。
2.3、属性名表达式
在ES6中对象字面量定义允许用表达式作为对象的属性名,即把表达式放在方括号内。假若要定义如下对象:
//ES5中的对象字面量中的属性名称与方法名称是不允许直接使用表达式的,可以使用[]单独定义 var obj1={ "prefix_name1":"tom", "prefix_login2":function(){}, //"prefix"+"_logout":function(){} //在ES5中不允许属性名为表达式 }; obj1["prefix"+"_attr3"]="foo"; console.log(obj1);
ES6之前对象字面量的属性名不允许是表达式,如果要使用表达式只能单独使用对象名加[]的形式操作,ES6中可以直接使用表达式作为属性名与方法名:
//ES6中的对象字面量可以直接使用表达式 var pf="prefix_",i=1; var obj2={ [pf+"name"+i++]:"tom", [pf+"login"+i++]:function(){}, [pf+"attr"+i++]:"foo" }; console.log(obj2);
属性名与方法名不仅可以是我们熟悉的表达式,还可以是后面我们将学习的Symble,也可以结合Generator。
2.4、访问器属性简洁定义
在上一章中我们定义访问器属性主要使用Object.defineProperty()静态函数完成,这样步骤比较麻烦,使用ES6可以简化访问器属性的定义。
"use strict" var user={ _age:0, get age(){ //读 return this._age; }, set age(value){ //写 //如果是数字且在0-130之间 if(!isNaN(value)&&value>=0&&value<=130){ this._age=value; }else{ throw "年龄必须是0-130之间的数字"; } } }; user.age=100; console.log(user.age); user.age="five"; //异常 console.log(user.age);
注意:上面声明的对象的私有成员__age仅仅只是一种编程规范,并没有真正对外部隐藏,依然可以直接访问,与__proto__属性类似。
2.5、直接指定原型
__proto__属性已在ECMAScript 6语言规范中标准化。__proto__用于直接对象的原型,一般__proto__指向该对象的构造函数的prototype对象,使用字面量创建对象只是一种语法糖,本质上还是使用Object,所以使用对象字面量创建的对象默认原型应该指向Object.prototype,现在可以直接在定义时就手动指定,间接实现继承的目的。
//ES5 //动物对象,作为dog的原型对象 var animal={name:"动物"}; var dog1={color:"黑色"}; //dog.__proto__=animal 设置dog对象的原型为animal对象 Object.setPrototypeOf(dog1,animal); console.log(dog1); //ES6 var dog2={color:"黑色",__proto__:animal}; //等价Object.setPrototypeOf(dog1,animal); console.log(dog2);
当然使用Object.setPrototypeOf静态函数也可以达到同样的目的,但使用__proto__更加便捷。
2.6、super
在简洁定义的方法中可以使用super访问到前对象的原型对象,类似Java中的super。
//ES5中调用原型中的方法 var animal={ eat:function(){ console.log('动物在吃东西'); } }; var cat1={} //猫 Object.setPrototypeOf(cat1,animal); //设置cat的__proto__为animal cat1.eat=function(){ //重写原型中eat方法 //调用原型中的eat方法 Object.getPrototypeOf(this).eat.call(this); //先获得当前对象的原型,再调用原型中的eat方法 console.log("猫在吃鱼"); } cat1.eat(); //ES6中调用原型中的方法使用super var cat2={ __proto__:animal, eat(){ super.eat(); //super指向原型对象 console.log("猫在吃鱼"); } }; cat2.eat();
在cat的eat方法中使用super引用到了当前对象的原型对象,Object.getPrototypeOf(this).
eat();也可以获得原型对象,但还是有区别的,使用super时当前上下文还是eat,使用getPrototypeOf后调用eat()方法的上下文只是animal,当然可以使用如下方式替换:
Object.getPrototypeOf(this).eat.apply(this);
看来本质还是语法糖,不过简化了过程;另外super只能在简洁定义的方法中使用。
三、声明块级作用域
在ES5中并没有块级作用域,对习惯了C系列编程语言的开发者来说非常容易犯错。因为JavaScript中有全局作用域与函数作用域,可以通过IIFE等方式模拟块级作用域,但这样使代码的可读性变差,ES6中使用let与const可以创建绑定到任意块的声明。
3.1、let
(1)、let关键字与var类似可以用于声明变量,不过let声明的变量只在当前块中有效。
//1let关键字与var类似可以用于声明变量,不过let声明的变量只在当前块中有效。 //ES5 var声明变量没有块级作用域 if(true){ var n1=100; } console.log(n1); //输出100 //ES6 let声明变量只在当前块中有效 if(true){ let n2=200; } console.log(n2);
从示例的输出结果可以看出m在整个全局作用域中都是可以访问的;而n则只在if这个块级作用域中有效,所以在外部访问时直接提示n未定义的错误消息。
(2)、使用let声明的变量不会被"提升"。
//2、使用let声明的变量不会被"提升"。 console.log(n3); //undefined var n3=300; console.log(n4); //异常,Cannot access 'n4' before initialization //在初始化之前不允许访问n4 let n4=400;
从输出结果可以看出使用使用var声明的变量输出了undefined,而使用let声明的变量n提示不能访问未初始化前的变量,是因为在ES6中,let绑定不受变量提升的约束,这意味着let声明不会被提升到当前执行上下文的顶部。这个变量处于从块开始到let初始化处理的"暂时性死区"(temporal dead zone,简称TDZ)之中。在代码块内,使用let命令声明变量之前,该变量都是不可用的。
(3)、循环中的let声明的变量每次都是一个新的变量,JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量时,就在上一轮循环的基础上进行计算。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li>Jack</li> <li>Rose</li> <li>Mark</li> </ul> <script> var users = document.getElementsByTagName("li"); for (var i = 0; i < users.length; i++) { users[i].addEventListener("click", function () { alert(this.innerText + "," + i); }, false); } console.log(i); </script> </body> </html>
运行结果如图4-1所示。
图4-1 点击Jack后运行状态
我们期待的是点击Jack时显示0,但没有,因为i是一个全局作用域变量,在for外依然可以访问,用闭包当然可以解决,但不容易理解,使用let后就可以达到预期了,运行结果如图4-2所示。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li>Jack</li> <li>Rose</li> <li>Mark</li> </ul> <script> var users = document.getElementsByTagName("li"); for (let i = 0; i < users.length; i++) { //修改为let users[i].addEventListener("click", function () { alert(this.innerText + "," + i); }, false); } console.log(i); </script> </body> </html>
图4-2 修改为let后点击Jack后运行状态
因为变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是0,另外在循环外访问i是不允许的。
(4)、使用let不允许重复声明。
//ES5 var m=100,m=200; console.log(m); //200 //ES6 let n=100; let n=200; //Identifier 'n' has already been declared,n已经被声明 console.log(n);
输出的错误提示标识符n已经被声明。
3.2、const
(1)、const用于声明一个拥有块级作用域的常量,声明后不允许修改,必须完成初始化。
const PI=3.1415926535898; //声明常量PI,这里必须初始化
PI=0.13; //错误:Assignment to constant variable. 不允许修改常量
(2)、const与let有许多特性是相同的,产生块级作用域,不会提升,存在暂时性死区,只能声明后使用,不允许重复定义。
(3)、const限制的只是变量指向的内存地址不允许修改,但地址所引用的对象是允许被修改的,因为简单类型变量直接就存放在内存地址中,而复杂类型(如对象和数组)则是间接引用的。
const LIST=[100,200];
LIST.push(300); //在数组中添加1个数
console.log(LIST); //结果:[100,200,300]
这样是可以正常运行的,但如果再次给LIST赋值就会报错了。
四、函数默认值
4.1、默认参数值
ES6之前定义函数是不能使用默认值的,只能通过一些技巧来弥补,逻辑运算符的非布尔类型运算这种方法使用最多:
function point(m, n) { m = m || 1; //如果m未提供参数,则参数的值为1 n = n || 1; //如果n未提供参数,则参数的值为1 console.log(m + "+" + n + "=" + (m + n)); } point(); //m=1,n=1,3 point(100); //m=100,n=1 point(200, 300); //m=200,n=300 point(undefined, 400); //m=1,n=400 point(500, undefined); //m=500,n=1 point(0, 0); //m=1,n=1 这个结果是错误的
从输出结果可以看出这种方法确实在多数情况是可行的,但调用point(0,0)时结果明显错了,因为我们期待的是m=0,n=0,但函数还是取了默认值,是因为0在逻辑运算时被视为假,所以返回了默认值。看来这样不仅麻烦而且有漏洞,在ES6中允许我们直接设置参数默认值:
function point(m=1, n=1) { console.log(m + "+" + n + "=" + (m + n)); } point(); //m=1,n=1,3 point(100); //m=100,n=1 point(200, 300); //m=200,n=300 point(undefined, 400); //m=1,n=400 point(500, undefined); //m=500,n=1 point(0, 0); //m=0,n=0 这个结果正确的
从输出结果可以看出不仅达到了设置默认值的目的,而且更加简单且没有错误。
4.2、默认值表达式
不仅可以为函数设置默认值,还可以为函数的参数设置表达式。
function point(m = 1+1, n =m+1) {
console.log("m=" + m + ",n=" + n);
}
point(); // m=2,n=3
函数默认值甚至可以是一个IIFE或一个函数。
function calc(m =(function (v) {
return ++v;
})(99)) {
console.log(m);
}
calc(); //100
calc(200); //200
m的默认值是一个立即执行函数表达式,调用时结果返回给了m作为默认值。如果是回调函数的默认值可以定义一个空函数也可以引用Function.prototype,这是一个空函数,这样可以免去创建对象的开销。如果函数的参数默认值是一个函数,在函数中抛出异常,这样就可以形成必填参数。
//1、函数默认值允许是表达式 function foo1(m = 1 + 1, n = m + 1) { console.log(m + n); } foo1(); //5 //2、函数默认值允许是函数 function foo2(add = function (m, n) { return m + n; }, a, b) { console.log(add(a, b)); } foo2(undefined, 2, 3); //5 foo2(function (x, y) { return x - y }, 2, 3); //-1 //3、IIFE,允许使用立即执行函数表达式 function foo3(m = 1, n = function (x) { return ++x; }(m)) { console.log(m+n); } foo3(); //3
运行结果:
五、spread与rest
5.1、spread展开
"…"是ES6中的一个新运算符,将"…"置于可iterable的对象之前时可以将对象展开,取出对象中的独立个体,可iterable的对象有:数组(Array)、字符串(String)、生成器(Generators)、集合(Collections)。
//1、数组的展开 var array1=[1,2,3]; var array2=[...array1,4,5,6]; //将array1展开 console.log(array2); //2、对象的展开 var car={"name":"汽车","color":"白色"}; var bus={speed:50,name:"公交车",...car}; //展开car,覆盖已的属性 console.log(bus); var suv={speed:130,...car,name:"SUV"}; //展开car,name属性被后定义的name覆盖 console.log(suv);
结果
因为对象中不允许存在重复的属性,所以后添加的属性将覆盖先添加的属性。调用函数时也可以将数组展开作为参数值:
function add(m,n,k) {
console.log(m+n+k);
}
add(...[100,200,300]); //输出:600
调用add方法时先将数组展开,把展开后的单个值依次赋值给函数的参数,如果数组中的个数多于参数时将优先数组下标更小的元素。
5.2、rest收集
rest与spread是相对的操作,rest可以收集剩余的参数,形成可变参数的函数,与java中的"…"类似。
function add(m,...argsArray) {
console.log(m,argsArray);
}
add(1); //m=1,argsArray=[]
add(1,2,3,4,5); //m=1,argsArray=[2,3,4,5]
在函数add中argsArray就是一个参数数组了,收集除第1个参数以外剩余的其它参数。虽然argments也可以收集参数,但rest与内置对象argments是有区别的:
(1)、rest只包含那些没有给出名称的参数,arguments包含全部参数;
(2)、arguments对象不是数组,rest是数组对象;
(3)、arguments是内置对象,默认就存在函数中,而rest必须使用"…"显示声明。
另外函数对象中的参数个数将不会计算rest参数:
console.log((function (a,...b) {}).length); //输出:1
六、解构
解构(Destructuring)是ES6中一种新的赋值方法,允许按照一定模式,从数组和对象中提取值,对变量进行赋值,使用解构将极大的方便从数组或对象中取值。
6.1、数组解构
数组解构可以方便的从数组中取值并赋值给变量,即等号左边的变量在等号右边的数组中的对应位置取得值,数组可以是字面量也可以是变量。
let [a,b,c]=[300,400,500];
console.log(a); //300
console.log(b); //400
console.log(c); //500
从输出结果可以看出解构是将数组中的对象逐个取出后分别赋值给了a,b,c这3个变量。未解构到值的变量为undefined。如果数组中的元素的个数多于要赋值的元素则优先下标更小的元素。
function getArray() {
return [100,200];
}
let [x,y,z]=getArray();
console.log(x,y,z); //输出:100 200 undefined
因为数组只有两个元素,z解构时未获到值,结果为undefined。当然解构时也可以跳过一些数组元素。
function getArray() {
return [100,200,300,400,500,600];
}
var [a,,b,,,c]=getArray();
console.log(a,b,c); //输出:100 300 600
数组中连续使用逗号起到跳过元素的作用。解构也可以与嵌套数组与rest一起使用。
let [x,y,z]=[100,[200,300],{a:400}];
console.log(x,y,z); //x=100,y=[200,300],z={a:400}
let [m,[n,k]]=[400,[500,600]];
console.log(m,n,k); //m=400,n=500,k=600
let [a,...b]=[700,800,900];
console.log(a,b); //a=700,b=[800,900]
从输出结果可以看出使用rest后变量b收集了数组中剩余的所有元素,b是一个数组。
6.2、对象解构
对象解构可以将对象中的值取出后按指定要求赋值给变量,非常方便从对象中提取数据。
//将对象中的name取出赋值给变量n
var {name:n,price:p}={name:"手机",price:1988,weight:180};
console.log(n,p); //输出:手机 1988
需要注意的是解构对象左边的表达式"name:n"的意思是取出对象中的属性name的值赋值给n,如果同名可以简化成属性名即可:
function getData() {
return {name:"手机",price:1988,weight:180};
}
var {name,price}=getData(); //等同于 var {name:name,price:price}=getData();
console.log(name,price); //手机 1988
解构对象时允许重复使用属性,允许使用表达式,注意如果不是赋值操作时需要使用括号,让其转换成一个表达式。
let obj={},phone={name:"手机",price:1988,weight:180};
({name:obj.x,price:obj["y"],name:obj.z}=phone);
console.log(obj); //{x: "手机", y: 1988, z: "手机"}
示例中可以看到name被引用了2次;在指定赋值对象的属性y时使用了字符串,这里可以是一个表达式,取值属性同样可以是一个表达式。解构可以配合rest一起使用。
let {name,...phone}={name:"手机",price:1988,weight:180};
console.log(name); //手机
console.log(phone); //{price: 1988, weight: 180}
示例中phone使用了rest特性,收集了除name之外的其它属性,创建了一个新对象,不过只允许使用次rest且必须放在末尾,另外解构赋值的拷贝是浅拷贝,解构赋值不会拷贝继承自原型对象的属性。
6.3、解构函数参数
只要是存在赋值的地方都可以解构,那么在调用函数时也可使用解构这一特性。可以解构数组作为函数的参数值。
function add([m,n]) {
console.log(m+"+"+n+"=",m+n);
}
add([100,200]); //100+200= 300
也可以解构对象作为函数的参数值。
function add({m,num:n}) {
console.log(m+"+"+n+"=",m+n);
}
add({m:300,num:400}); //300+400= 700
解构对象时可以结合函数默认值。
function add({m=100}={m:200},{n=300}) {
console.log(m+"+"+n+"=",m+n);
}
add({},{}); //100+300= 400
add({m:400},{}); //400+300= 700
add({},{n:500}); //100+500= 600
函数add接受两个对象的解构,第1个参数的默认值是解构赋值的,第2个参数n的默认值为300。
{m=100}的默认值是{m:200},m的默认值是100,n的默认值值是300
function add({m=100}={m:200},{n=300}){ console.log(m+"+"+n+"=",m+n); } add({},{}) //相当于let {m=100}={},{n=300}={} m=100,n=300 add({m:400},{n:500}) //相当于 let {m=100}={m:400},{n=300}={n:500},m=400,n=500 add({},{n:600}) //相当于let {m=100}={},{n=300}={n:600},m=100,n=600
七、箭头函数
箭头函数(Arrow Function)简化了函数的定义,它不仅仅是语法糖,更多是修正了JavaScript中this复杂多变的瑕疵。箭头函数与C#与Java中的Lambda表达式类似。
7.1、箭头函数的定义
箭头函数定义时可以省去function与return关键字,需要使用"=>"这个箭头样的符号,基本形式是:(参数列表)=>{函数体}。
function add(n){ //普通函数
return ++n;
}
var add=n=>++n; //箭头函数
从示例中可以看出箭头函数更加简洁,只有1个参数时可以省去圆括号,其余情况都要加圆括号;如果函数体中的表达式有2个或以上时需要使用大括号,且需要显式的使用return返回结果。
//fun是一个Function类型的参数
function handler(fun) {
console.log(fun(200,300));
}
handler(()=>600); //600,相当于function(){return 600;}
handler((m,n)=>m+n); //500,相当于function(m,n){return m+n;}
handler((m,n)=>{return m+n;}); //500
handler((m,n)=>{m+n;}); //undefined
最后一次调用handler时返回undefined是回为没有返回值。
7.2、箭头函数的特点
(1)、箭头函数this不再是动态的,指向其父作用域,定义时的作用域。this在函数声明的时候就做了绑定,箭头函数没有自己的this, 内部的this就是外层代码的this。
var n=200;
var math={
n:100,
add1() {
console.log(++this.n); //this是动态的
},
add2:()=>console.log(++this.n) //this指向window
};
math.add1(); //101
math.add2(); //201
因为add1是普通函数,this是调用时决定的,调用时this指向math,n为100;而箭头函数中的this是静态的,this指向window对象,n的值为200,这个特性在事情处理中需要特别注意。
<button id="btnA">普通函数</button>
<button id="btnB">箭头函数</button>
<script>
var math={
n:100,
add1() {
console.log(this); //this是动态的
},
add2:()=>console.log(this) //this指向window
};
document.getElementById("btnA").addEventListener("click",math.add1,false);
document.getElementById("btnB").addEventListener("click",math.add2,false);
</script>
运行结果如图4-3所示。
图4-3 箭头函数作为事件时的运行状态
从输出结果可以看出点击绑定箭头函数的按钮时输出的this为Window,而此时普通函数的this指向了当前按钮。
var obj = { a: 10, b: function () { console.log(this.a); //10 }, c: function () { return () => { console.log(this.a); //10 } } } obj.b(); obj.c()();
obj.b()调用的是一个普通函数指向调用对象obj,obj.c()()调用的是一个箭头函数,this指向obj.c()定义时的this,this依然是obj。
(2)、call、apply与bind也不能修改箭头函数的this。
math.add2.call(math);
math.add2.apply(this);
math.add2.bind(this)();
运行结果如图4-4所示。
图4-4 call、apply与bind应用于箭头函数的运行状态
//(2)、call、apply与bind也不能修改箭头函数的this。 var mymath={ n:100, add1(){ console.log(this.n); }, add2:()=>{ console.log(this.n); } }; var obj2={n:200}; mymath.add1.call(obj2); //200 mymath.add2.call(obj2); //?
(3)、arguments,caller、callee在箭头函数中不存在。
(4)、prototype属性在箭头函数中不存在。
(5)、箭头函数不能作为构造器,因为没有this,不能初始化新创建的对象;没有prototype限制了继承。
<script> let fun=()=>1; console.log(fun.prototype); var obj1=new fun(); </script>
(6)、不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
小结:箭头函数与普通函数无论在写法还是特性上都有区别,使用时容易受原有的经验影响而出错,对于简短的回调函数可以使用箭头函数,但不能滥用。
八、for...of VS 其它循环
8.1、for…of
-
在ES6中新增了一个循环控制语句for…of,它可以用于遍历可迭代(Iterator)的对象,原生具备Iterator接口的数据结构有:Array数组、Map、Set、String、TypedArray、arguments对象、NodeList对象。
语法格式:for (variable of iterable){ },iterable是一个可以迭代的对象,variable是每次迭代是的变量。
var users=["jack","rose","mark","lucy","tom"];
for(let user of users){
console.log(user); //jack rose mark lucy tom
}
使用for…of可遍历字符串。
var hello="Hello Iterator";
for(const c of hello){
console.log(c.toUpperCase()); //H E L L O I T E R A T O R
}
使用for…of可以遍历函数中内置对象arguments。
(function () { //IIFE
for(let a of arguments){
console.log(a); //true false 100 Hello {}
}
})(true,false,100,"Hello",{});
使用for…of可以遍历NodeList集合。
<ul>
<li>rose</li>
<li>mark</li>
<li>jack</li>
</ul>
<script>
var users=document.querySelectorAll("ul li");
for(let li of users){
li.innerHTML+=" ok";
}
</script>
运行结果如图4-4所示。
图4-4 for…of遍历NodeList页面状态
使用break、throw与return可以终止循环,continue可结束当次循环。
8.2、for…of与其它循环比较
-
ES6中新增加的for…of循环结构较其它循环结构是有一定的优势的,比较不同的循环的目的是更加合理的选择各种循环结构。
var users=["jack","rose","mark","lucy","tom"];
for(var i=0;i<users.length;i++){
console.log(users[i]); //jack rose mark lucy tom
}
for(let user in users){
console.log(user); //0 1 2 3 4
console.log(users[user]); //jack rose mark lucy tom
}
users.forEach((value,index)=>{
console.log(value); //0 1 2 3 4
console.log(index); //jack rose mark lucy tom
});
for(let user of users){
console.log(user); //jack rose mark lucy tom
}
从上面的输出结果我们可以得出如下结论:
(1)、for循环的缺点是需要跟踪计数器和退出条件。
(2)、for…in不需要踪计数器和退出条件,但只获得了下标。
(3)、forEach不能停止或退出(break,continue语句),它只是Array的一个实例方法,不适用其它对象。
(4)、for…of不能迭代对象。
小贴士:for…of不是万能的,合适的才是最好的,应该根据具体情况选择不同的循环结构。
九、Symbol
ES6中增加了一种新的数据类型symbol,主要目的是解决属性名冲突的问题,如果一个对象中已使用了某个属性名,再定义就会覆盖。Symbol可以实现唯一的属性名称,防止冲突。
9.1、创建symbol
-
symbol没有字面量形式,主要通过Symbol函数生成,语法:Symbole(description?: string | number): symbol,描述字符串可以是数字或字符。
let prop1=Symbol("name"); //创建一个symbol,name只是描述字符
let prop2=Symbol(12345); //12345是描述字符
console.log(typeof prop1); //输出:symbol
console.log(prop2 instanceof Symbol); //输出:false
var user={};
user[prop2]=18; //使用symbol作为属性名
console.log(user[prop2]); //输出:18
从示例的输出结果可以看出Symbol函数创建的特殊对象是symbol类型的,它并不是Symbol类型的对象,使用symbol作为属性名时不能像字符串一样直接写在对象字面量中,需要使用"[symbole]",描述字符串相同的symbol也是不一样的属性名。Symbole创建的对象总是唯一的。
const foo=Symbol("name");
const bar=Symbol("name");
var obj={
[foo]:100 //不能用"foo:"作为属性名
};
console.log(obj[foo]); //不能用"obj.foo"访问,输出:100
console.log(obj[bar]) //输出:undefined
console.log(foo===bar); //输出:false
使用bar作为键访问obj中的成员时返回undefined是因为bar与foo虽然有相同的描述述,但作为属性名是完全不一样,也就是说除非你能再次拿到foo这个symbol标签,否则你将无法再次修改对应的属性。
symbol可转换成字符串与Boolean类型,在ES2019中新增加了属性description可以获得描述信息, Symbol 值不能与其他类型的值进行运算。
var foo=Symbol("name");
console.log(foo.description); //输出:name,ES2019中可以获得描述字符
console.log(foo.toString()); //输出:Symbol(name),转换成String类型
console.log(!!foo); //输出:true,转换成boolean类型
foo+""; //错误:Cannot convert a Symbol value to a string
最后一句报错的原因是因为symbol标签与字符串进行加法运算,这是不允许的。
9.2、全局symbol
Symbol.for(key: string)方法可以根据key查找全局中是否存在symbol对象,存在就返回,不存就创建新的symbol对象并注册到全局。如果希望再次使用一个symbol标签可以使用该方法。
//注册一个全局的symbole标签
let name=Symbol.for("username");
let user={
[name]:"tom" //定义键为symbol的属性
};
let uname=Symbol.for("username"); //根据username在全局查找symbol标签
console.log(user[uname]); //输出:tom
console.log(name===uname); //输出:true
通过这种方法可以实现全局的symbol共享,使用for不管在那里登记的symbol都是全局的。使用Symbol.keyFor()可获得已登记Symbol的key。
function bar() {
return {[Symbol.for("s1")]: "rose"}; //注册全局symbol,返回对象
}
var obj=bar();
let s1 = Symbol.for("s1"); //根据key全局查找symbol
console.log(obj[s1]); //输出:rose,根据symbol访问对象中的属性
let key = Symbol.keyFor(s1); //根据symbol获得key
console.log(key); //输出:s1
只有使用Symbol.key登记的才是全局的symbol,才拥有key值,直接使用Symbol()生成的symbol没有key。
console.log(Symbol.keyFor(Symbol("s2"))); //输出:undefined
9.3、symbol应用
symbol是标签的意思,它的特点是每个标签都是唯一的,如果作为属性的键时可以保护属性不被覆盖。另外,在开发中我们经常要区分一些类别,或获得一个唯一的名称,而不关于他的语义时就可以使用标签了。
//定义等级类别
let TYPES={
HIGH: Symbol(), //高
MIDDLE: Symbol(), //中
LOW: Symbol() //低
};
console.log(TYPES.HIGH);
有人把在程序中反复出现用于区分类型的字符串称为"魔术字符串",使用symbol可以起到类似枚举的作用,可以消除魔术字符串。
9.4、Iterator接口
原生具备Iterator接口的数据结构有:Array数组、Map、Set、String、TypedArray、arguments对象、NodeList对象。
var arr = [1,2,3,4]; let iterator = arr[Symbol.iterator](); console.log(iterator.next()); //{ value: 1, done: false } console.log(iterator.next()); //{ value: 2, done: false } console.log(iterator.next()); //{ value: 3, done: false } console.log(iterator.next()); //{ value: 4, done: false } console.log(iterator.next()); //{ value: undefined, done: true }
实现Iterator接口
<script> var mall={ name:"天狗商城", products:["手机","电脑","图书"], [Symbol.iterator](){ let i=0; let that=this; return { next(){ if(i<that.products.length){ return {value:that.products[i++],done:false} }else{ return {value:undefined,done:true} } } } } }; for(let v of mall){ console.log(v); } </script>
运行结果:
十、模板字符串
10.1、基本用法
ES6中引入了模板字符串,以反引号( ` )作为界定符,也可以表示多行字符串,同时支持嵌入基本的字符串插入表达式(${变量或表达式}),可替传统的加号拼接方式。
let name="小明",height=188;
let introduce=`大家好,我是${name},我身高${height}cm!`;
console.log(introduce); //输出:大家好,我是小明,我身高188cm!
模板字符串支持多行模式。
let introduce=`
大家好,
我是${name},
我身高${height}cm!`;
console.log(introduce); //输出:大家好,我是小明,我身高188cm!
运行结果如图4-5所示。
图4-5模板字符串多行模式输出结果
使用模板字符串表示多行字符串,则所有的空格、缩进和换行都会被保留在输出中。
10.2、表达式
在模板字符串中使用${变量名}的形式可以获取变量中的值,大括号中也可以使用表达式,这样极大的增加了模板字符串的灵活性。
var m=100,n=200;
var str1=`${m}+${n}=${m+n}`;
console.log(str1); //输出:100+200=300
var obj={x:300,y:400};
var str2=`${obj.x/3}x${obj.y*2}=${(obj.x/3)*(obj.y*2)}`;
console.log(str2); //输出:100x800=80000
模板字符串之中还可以是函数的调用,IIFE等各种表达式,这样还可以实现模板字符串的嵌套。
//定义函数
let add = (x, y) => x + y;
console.log(`100+200=${add(100, 200)}`); //输出:100+200=300
console.log(`${(function (v) {return v;})("Hello IIFE!")}`); //Hello IIFE!
虽然可以这样做,一般情况下不建议这样,这样会降低代码的可读性。
十一、上机部分
11.1、上机任务一(30分钟内完成)
上机目的
1、掌握ES6中新语法的使用。
2、理解箭头函数中的this。
上机要求
1、使用ES6扩展的特性创建一个汽车对象,属性与方法定义如表4-1所示,其"汽车类型"属性是symbol类型的,为了消除魔术字符串,需要先定义一个类似枚举的对象,在新创建的对象引用;print方法用于向控制台输出汽车的所有属性,需要使用箭头函数。
序号 |
类别 |
中文名称 |
英文名称 |
类型 |
备注 |
1 |
属性 |
车牌 |
licenseNo |
Number |
普通属性 |
2 |
名称 |
name |
String |
简洁属性 |
|
3 |
价格 |
price |
Number |
访问器属性,限制价只能是0-999999之间的数字 |
|
4 |
类型 |
carType |
symbol |
普通属性(模拟枚举),SUV、SRV、CRV、MPV、RAV |
|
5 |
产地 |
madeIn_当前年 |
String |
属性名表达式,如:madeId_2033 |
|
6 |
方法 |
打印 |
|
箭头函数 |
显示所有属性 |
7 |
加价 |
increase |
简洁函数 |
根据加价百分比调整价格 |
表4-1 汽车对象的属性与方法
2、要求使用了let、简洁属性、简洁方法、symbol、箭头函数、属性名表达式、简洁访问器属性等ES6中的新特性,特别注意箭头函数中的this是指向父域的,是静态的,不是指向当前对象。
3、对创建的对象实现取值、修改、删除、迭代与方法调用操作。
推荐实现步骤
步骤1:先创建汽车类型对象,参考代码如下。
//汽车类型对象
const VehicleType={
SUV:Symbol("运动型多用途车"),
SRV:Symbol("小型休闲车"),
CRV:Symbol("城市休闲车"),
MPV:Symbol("多用途汽车"),
RAV:Symbol("休闲运动车")
};
步骤2:再根据要求使用对象字面量创建创建对象、访问对象。
步骤3:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
11.2、上机任务二(60分钟内完成)
上机目的
1、掌握ES6中新语法的使用。
2、巩固上课时所讲解的内容。
上机要求
1、调试本章的每一段示例代码,获得对应的预期结果。
2、理解示例代码,为"每一行"示例代码写上注释。
提示:如果书中出现错误请及时告诉任课老师或学习委员,也可直接反馈到:3109698525@qq.com,还有可能成为幸运读者。
11.3、上机任务三(90分钟内完成)
上机目的
1、掌握ES6中的语法新特性。
2、锻炼JavaScript的综合应用能力。
上机要求
1、完成一个"舒尔特方格"注意力测试与训练应用。它是全世界范围内最简单,最有效也是最科学的注意力训练方法。
提示:舒尔特方格 (Schulte Grid) 是在卡片上画25 个方格,格子内随机填写上数字1到25(当然也可以是字母)。训练时要求被测者按1到25的顺序依次指出其位置,用时间越短注意力水平越高
2、程序初始化时随机生成1-25个数据,数字一旦生成结束前不会再变化,如图4-6所示:
图4-6 页面初始化时的效果
3、点击开始按钮后提示下一个要点击的数字,显示当前的耗时时间,运行效果如图4-7所示。
图4-7 点击开始按钮时的运行状态
4、当按提示点击完正确的数字后显示下一个要点击的数字,运行时的状态如图4-8所示。
图4-8 点击正确的数字时的运行状态
5、当未按提示点击了错误的数字后提示应该点击的正确数字,运行时的状态如图4-9所示。
图4-9 点击错误的数字时的运行状态
6、当正确点击完所有的数字后提示用时,如图4-10所示。
图4-10 完成时的运行状态
舒尔特方格结果解读如表4-11所示。
7—12岁 |
26秒以内为优秀 42秒属于中等水平 50秒则较差 |
12—17岁 |
16秒以上为优良 26秒属于中等水平 36秒则较差 |
18岁以上 |
最快可到达8秒 20秒为中等 |
表4-11 舒尔特方格结果解读
7、尽可能多的使用ES6的新特性,注意封装代码,对外只暴露一个接口,分离脚本文件,考虑程序的可扩展性,可以设定生成的不是数字而是字母或其它字符、要求不是25个格子而是2N个等配置。
推荐实现步骤
步骤1:先理解"舒尔特方格"的定义与规则。
步骤2:再根据要求编写JavaScript脚本,优化脚本,替换非ES6脚本。
步骤3:反复测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
11.4、代码题
1、请使用ES6重构如下代码:
var body = request.body
var username = body.username
var password = body.password
提示:可以考虑使用对象解构。
2、想办法把上机阶段三的代码运行在不支持ES6的浏览器中。
11.5、扩展题
假定有如下数组:
var array1=[1,1,2,2,3,3];
请使用尽量少的代码去除数组中的重复元素,等到的数组如下:
array2=[1,2,3];
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?