概述
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可以簡化通路器屬性的定義。
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"; //異常
注意:上面聲明的對象的私有成員__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);
當然使用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
從輸出結果可以看出使用使用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已經被聲明
輸出的錯誤提示辨別符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) {
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覆寫
結果
因為對象中不允許存在重複的屬性,是以後添加的屬性将覆寫先添加的屬性。調用函數時也可以将數組展開作為參數值:
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
七、箭頭函數
箭頭函數(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,在浏覽器端this始終指向window對象。
math.add2.call(math);
math.add2.apply(this);
math.add2.bind(this)();
運作結果如圖4-4所示。
圖4-4 call、apply與bind應用于箭頭函數的運作狀态
(3)、arguments,caller、callee在箭頭函數中不存在。
(4)、prototype屬性在箭頭函數中不存在。
(5)、箭頭函數不能作為構造器,因為沒有this,不能初始化新建立的對象;沒有prototype限制了繼承。
(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可以起到類似枚舉的作用,可以消除魔術字元串。
十、模闆字元串
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、了解示例代碼,為"每一行"示例代碼寫上注釋。
提示:如果書中出現錯誤請及時告訴任課老師或學習委員,也可直接回報到:[email protected],還有可能成為幸運讀者。
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];
十二、源代碼
https://gitee.com/zhangguo5/JS_ES6Demos.git