現在就可以使用的5個 ES6 特性
這篇文章介紹了5個 ES6 特性,使你的 JavaScript 代碼變的更好。不用說,我們大多數前端開發工程師非常關注 JavaScript 的性能和特性,這就是為什么 ES6 對于我們來說是如此令人興奮。
ES6的變化是巨大的,是令人興奮的,也有令人困惑的地方。在本文中,我將介紹5個 ES6 的新特性,您可以立即使用它們來增強你的 JavaScript代碼,以及哪些特性不建議使用。
Ecma是什么?
JavaScript 多年來已經有了很多版本,但總體而言,進化很緩慢,直到最近。 我覺得在過去的兩年里,發生了比過去二十年更多的變化。 ES6 是 EcmaScript 6 的簡稱。它也稱為 EcmaScript 2015 或 ES 2015。這些都是“新的Javascript”的不同名稱。
我不完全確定為什么最近 JS 改變如此之大,但似乎是因為主要的 JavaScript 引擎廠商終于有興趣推動該語言的發展了。
此外,像 TypeScript 這樣的 轉移器 的出現,使得在將其用于瀏覽器之前,可以使用新的語言特性。這兩個組合將大大推動 JavaScript 的發展。
JS很重要,因為它是 web 的構造者,并越來越多的服務器端使用開始 Node ,越來越多的人使用 Cordova,React Native 和 Electron 來開發手機和桌面應用程序。
簡而言之:JS無處不在。
所以重要的是我們來推動它。不演化的語言開始走向死亡。改善語言意味著我們可以改進我們的代碼。我們的應用程序可以更少地出現錯誤。其中一些特性能使代碼更快地運行。所以讓我們從變量開始,來一起來看看 ES6 的新特性。
變量的問題
在 JavaScript 中,當您要創建一個變量時,可以使用 var 關鍵字。 var 非常強大,但它有一些問題。首先,變量總是不斷變化的。沒有辦法在 JavaScript 中定義一個真正的常量。您可以通過創建具有不可變屬性的對象來近似偽造常量。您可以覆蓋該屬性的setter,使其不能由外部代碼設置,但它需要很多額外的代碼,需要一個完整的對象來代替一個簡單的變量。
var VARS = {
foo
set = function() { }
get = function() { return 42 }
}
VARS.foo = 42; // now I have a constant
常規Javascript變量還有一個作用域問題。 看看這段代碼。
function badCode() {
if(foo) {
var x = 5;
}
if(bar) {
console.log(x);
}
}
在函數體內通過 var 聲明的變量,在函數體內全局可用, var 聲明的變量沒有塊級作用域。如果你認為上面的 console.log 語句不能正常執行,因為在 bar 定義在不同的塊中。那么我告訴你,在類似于 java 或 C# 等語言中,你是對的,但是在 JavaScript 中, var 聲明的變量,在函數體內全局可用,而不是在語句塊中可用。
當我們添加變量提升時,它會變得更糟。 考慮這個代碼:
function badCode() {
console.log("foo is",foo);
var foo = 'bar';
}
我在 foo 定義之前使用它。 這段代碼意味著什么? JavaScript 引擎會將變量聲明提升到函數塊的頂部。 但變量提升很容易引發一些難以診斷的細微錯誤。
看看這個 for 循環:
for(var i=0; i<5; i++) {
console.log(i);
}
console.log(i);
該變量 i 僅在循環體內使用,但我仍然可以在循環體外部引用它。 這可能會引發大量的 bug 。
好消息是我們不用再必須使用 var 定義變量了,我們可以使用 const 和 let 。
介紹 const 和 let
新的關鍵字 const 完全符合這個名字的意思。它可以定義一個真正的常量。如果您嘗試重新設置常量值,您將收到運行時錯誤。更好的是,Lint語法檢查工具可以在編譯時檢測出這種錯誤,所以您可以在開發時較早的發現這種錯誤。
另一種新的變量定義方式是使用 let 關鍵字。 let 就像 var ,但是它是塊級作用域而不是函數作用域。下面代碼 console.log(x) 會報錯:
function goodCode() {
if(foo) {
let x = 5;
}
if(bar) {
console.log(x); //error
}
}
讓我們回顧一下 for 循環的例子:
function goodCode() {
for(let i=0; i<5; i++) {
console.log(i);
}
console.log(i); //error
}
現在,我的變量只限于for循環體內使用。沒有辦法可以不經意地使用。另外, let 定義的變量不能提升,所以所有這些不可思議的移動變量都消失了。
新的關鍵字 const 和 let 可以完全替代 var 。 使用現代瀏覽器和最新版本的Node,沒有任何理由使用 var 了,甩掉歷史包袱。(愚人碼頭注:個人認為還是要看使用環境。)
超級字符串
ES6引入了一種新類型的字符串,稱為模板字面量。我更喜歡稱之為超級字符串。你使用一個超級字符串就像一個常規的字符串,但是你不光可以使用單引號或雙引號,你還可以使用反引號 ` 。
var q = 'foo';
var qq = "foo";
var bq = `foo`;
var qq = "Sally sells \"sea shells\"";
var bq = `Sally sells "sea shells"`;
到現在為止還挺好,但沒有什么非同尋常的地方。不過它確實有一個直接的優勢。如果您需要在字符串中使用雙引號或單引號,則不必再轉義(愚人碼頭注:原來單引號雙引號配對不是也可以嗎?不知道作者為什么這樣寫。嘎嘎)。然而,超級字符串還有其他一些技巧。
多行字符串串聯
終于我們可以有真正的多行字符串了。如果你需要引用幾行的字符串?你不必再使用加號的方式鏈接字符串。直接放入換行符,它就能正常工作。
var q = 'foo';
var qq = "foo";
var bq = `foo`;
var qq = "Sally sells \"sea shells\"";
var bq = `Sally sells "sea shells"`;
表達式換碼
另一個新特性是表達式換碼(愚人碼頭注:前端說的比較多的是替換符)。在超級字符串中,您可以在 ${} 的括號內放入任何有效的 JavaScript 表達式。這比雙引號更加干凈,最新的IDE一般都會語法高亮顯示這些表達式。
var name = "Alice";
var greeting = `Good morning ${name}`;
var amount = 5;
var price = 3;
var sales = `That costs ${amount*price} dollars`;
將表達式換碼與多行字符串支持相結合,為我們提供了很棒的HTML模板。
var template = `
<div>
<h3>Good Morning ${name}</h3>
<p>
That item will cost you
<b>${amount*price}</b>
dollars.
</p>
</div>`
箭頭函數
上面說的是字符串和變量,現在讓我們來看看函數。如果您以前聽過ES6,很多可能都是關于箭頭函數的。這是更緊湊的常規函數語法。他們也有一個非常重要的區別: this 變量指向的是不同的東西。
假設你想循環一個數組使其元素的值乘2,并且生成一個新數組。你可以用下面的 for 循環來做,但是這樣會產生額外的變量,并且可以很容易寫錯。
var output = [];
for(var i=0; i<;input.length; i++) {
output[i] = input[i] * 2;
}
JavaScript數組有一個新方法 map ,它在每個元素上調用一個函數來生成一個新的元素,然后將其放入新數組中。代碼如下:
var output = input.map(function(x) {
return x * 2;
});
這看起來更好,更簡潔。 x * 2 部分是惟一實際工作的部分。 其余的是語法開銷。如果使用箭頭功能,我們可以這樣做:
var output = input.map((x)=>x*2);
哇!這個更加簡潔了。讓我解釋一下上面的代碼。箭頭函數可以重寫函數,而不需要實際的 function 關鍵字。而是在包含函數參數的括號后面跟 => 符號。
//常規函數
function (x) {
return x * 2;
}
//箭頭函數樣式
(x) => {
return x * 2;
}
箭頭函數讓我們寫同樣的代碼更加簡潔。但我們還可以繼續簡化。刪除空格,相同的更簡短。
(x) => { return x * 2; }
我們還可以使它更簡短。如果箭頭函數只包含一個表達式,我們可以刪除 return ,大括號和分號,寫成一個單行表達式,它會自動返回它的值。這樣就簡化到極致了。
(x) => x * 2
var output = input.map((x)=>x*2);
箭頭函數可以使您的代碼非常緊湊和強大。 但是還有一個非常重要的事情是:它修復了 this 。
this 魔咒
在 JavaScript 中,最不可思議是變量 this 總是引用函數被調用的那個對象。所以像下面的代碼肯能不會是你認為的那樣執行。
function App() {
this.clicked = false;
var button = document.getElementById('button');
button.addEventListener('click', function(evt) {
this.clicked = true; //不會安裝你想的那樣執行
});
}
當您使用其他對象時, this 上下文可能與預期的不同。當你將一個函數傳遞到其他地方被調用時,它可能會使用不同的 this 調用該函數。如果您將一個事件處理程序添加到 button , this 將指向 button 。有時這就是你想要的,但在上面的代碼中不是。我們希望 this 指向 App 對象,而不是 button 。
this 是JavaScript長期存在的問題。很常見的做法是,開發人員創造了一種 self 模式,使用臨時變量 self 保存對 this 的正確引用。看起來很惡心,但它能正常工作。
function App() {
this.clicked = false;
var button = document.getElementById('button');
var self = this; //特別注意這一行
button.addEventListener('click', function(evt) {
self.clicked = true;
});
}
另一種解決問題的方法是 bind 綁定這個函數。 bind 方法強制 this 執行一個特定的對象,不管函數后面如何被調用。
function App() {
this.clicked = false;
var button = document.getElementById('button');
var callback = (function(evt) {
this.clicked = true
}).bind(this);//特別注意這一行
button.addEventListener('click',callback);
}
再次執行, this 能正常指向 App 對象,但它不是很好。我們需要寫額外的代碼,并且 bind 會帶來額外的開銷。箭頭函數為我們提供了更好的方法。
function App() {
this.clicked = false;
var button = document.getElementById('button');
button.addEventListener('click',()=>{
this.clicked = true;
});
}
箭頭函數自動捕獲該函數定義時作用域中的 this 變量,而不是來自函數被調用的作用域中。這意味著您可以將函數傳遞給其他地方,使用時要知道 this 正確的指向。在上面的代碼中,沒有任何的 hack ,就能按照我們所期望的那樣執行。
簡而言之,箭頭函數真的很棒。我可以盡可能地使用它。它們使您的代碼更短,更容易閱讀,而且 this 也變得很明智。
Promises
箭頭函數的另一個很強大的特性是它們能與 Promises 配合的很好。Promise 是 JavaScript 中一種新的對象,旨在幫助需要很長時間執行的事情。JavaScript 沒有線程,所以如果你想做一些可能需要很長時間的事情,那么你必須使用回調。
例如,在Node中,您可能需要加載文件,解析它,做一個數據庫請求,然后寫一個新的文件。這些都必須按順序完成,但它們都是異步的,所以你必須嵌套你的回調。這就產生了被JS開發人員稱之為“金字塔”式的代碼風格。你需要大量的嵌套代碼。
fs.readFile("file.txt", function(err, file) {
db.fetchNames(function(err, names) {
processFile(file, names, function(err, outfile) {
fs.writeFile('file.txt',outfile, function(err, status) {
console.log("we are done writing the file");
})
});
})
});
這段代碼很丑,很難理解,而且有很多討厭的隱藏 bug。 Promises 可以幫助我們擊敗 “金字塔” 式的代碼風格。
JavaScript Promise 表示一個值當前可能不可用,但是將來有值的對象。使用 then 函數可以添加回調,當最終值 ready 時,調用這個回調。
var prom = makeSomePromise();
//value not ready yet
prom.then((value)=>{
//do something with the value
})
Promises then 回調很像傳統的回調,但 Promises 增加了一個額外的轉變 :他們可以鏈式調用。讓我們重溫一下上面的代碼。每個函數必須按順序調用,每個函數都取決于前一個函數的結果。使用Promises ,我們可以這樣做。
fs.readFile("file.txt")
.then((file) => {
return db.fetchNames().then((names)=>{
return processFile(file,names)
})
})
.then((outfile)=>{
return fs.writeFile('file.txt',outfile);
})
.then(()=>{
console.log("we are done writing the file");
});
請注意看,箭頭函數如何使這個代碼變得漂亮干凈。每個 then 回調返回一個值。這個值傳遞給下一個函數,所以我們所有的函數都可以很容易的鏈式調用。
現在你會注意到, processFile 命令需要前面兩個值的結果,但是 Promises 只傳遞一個值。我們也不用關心 readFile 和 fetchNames 執行的順序。我們只想知道何時完成。Promises 可以做到這一點。
Promise.all()
假設要從文件名數組中加載每個文件,并在完成時通知。我們可以用 Promise.all() 來做到這一點。 all 都是 Promise 中的一個實用方法,它獲取一系列 Promises ,并返回了一個新的 Promises ,當所有的子 Promises 完成時,它將得到 resolves 狀態。以下是我們如何使用 Promise.all 加載所有文件的示例。(假設 readFile 是一個讀取文件返回 Promises 的函數)。
var proms = filenames.map((name)=> readFile(name));
Promise.all(proms).then((files)=>{
console.log(`we have ${files.length} files`);
});
現在我們可以重寫我們原來的Node示例:
Promise.all([
fs.readFile("file.txt"),
db.fetchNames()
])
.then((vals) => processFile(vals[0],vals[1]))
.then((outfile)=> fs.writeFile('file.txt',outfile))
.then(()=> console.log("we are done writing the file"));
我將讀取的文件和數據庫調用合并到使用 Promise.all 的單個 Promise 中。它將返回的值是一個數組,包含兩個子 Promise 的結果,所以我可以把它們放到 processFile 中。請注意,我使用了縮寫箭頭語法使代碼更小更干凈。
處理失敗
現在考慮如果這些 Promises 中有一個失敗會發生什么?為了處理第一個失敗,我們可以將其放入 try / catch 語句塊中,但下一個 then 仍然會被調用。如果第一個失敗,我們希望一切都停止。Promises有另外一個技巧:捕獲回調。
在下面的新版本的代碼中,如果有任何失敗,它將立即跳過其余的 Promises ,跳轉到結尾的 catch 回調中。 catch 回調中,我們還可以添加更多的子句。
Promise.all([
fs.readFile("file.txt"),
db.fetchNames()
])
.then((vals) => processFile(vals[0],vals[1]))
.then((outfile)=> fs.writeFile('file.txt',outfile))
.then(()=> console.log("we are done writing the file"))
.catch((e) => {
console.log("some error happened");
});
編寫自定義Promises
當然 Promises 只有在我們調用的API 實際使用 Promises 的情況下才能工作。我們可以期待許多庫開始轉換為 Promises,當然我們也可以編寫自己的 Promises 。我們使用 Promise 構造函數來實現。
function makePromise(foo,bar) {
return new Promise((resolve, reject) => {
try {
//do long stuff
resolve(value);
} catch {
reject(error);
}
});
}
它需要兩個值: resolve 和 reject 。這些都是回調函數。回調里面你可以做任何需要花很長時間執行的事情,即使是它涉及多個回調。當完全完成后,調用具有最終值的 resolve() 。然后,這將發送到任何你使用 Promises 的第一個 then 子句。
如果發生錯誤,并且您想 reject 該值,而不是拋出錯誤,請使用 reject() ,傳遞所需的任何替代值。
這是一個現實中的例子。 我一直使用 AJAX 調用,但它們可能非常丑陋,像這樣。
var url = "http://api.silly.io/api/list/e3da7b3b-976d-4de1-a743-e0124ce973b8?format=json";
var xml = new XMLHttpRequest();
xml.addEventListener('load', function() {
var result = JSON.parse(this.responseText);
console.log(result);
});
xml.addEventListener('error',function(error) {
console.log("something bad happened");
});
xml.open("GET",url);
xml.send();
讓我們把這個代碼包裝成一個承諾 Promise。
function doGet(url) {
return new Promise((resolve,rej)=>{
var xml = new XMLHttpRequest();
xml.addEventListener('load', function() {
var result = JSON.parse(this.responseText);
resolve(result);
});
xml.addEventListener('error',function(error) {
reject(error);
});
xml.open("GET",url);
xml.send();
});
}
基本上是相同的代碼,但我可以像這樣調用它。
var url = "someapi.com/dostuff?foo=bar";
doGet(url).then((obj)=>{
console.log(obj);
});
哇,這樣更干凈。實際上,我們不需要自己編寫 AJAX Promise 包裝器,因為有一個新的Web標準 fetch 已經為我做了這些事情。但是現在所有瀏覽器都還沒支持 fetch ,所以我們可以使用我們自己編寫的 Promise ,直到瀏覽器支持 fetch 的時候。
第一次包裝 Promise 可能有點困難,但一旦你開始使用它們,我想你會很喜歡他們。它們可以非常容易的將多個函數集成到一個具有邏輯意義的單個工作流中,并正確地捕獲所有錯誤。
Arrays
最后我想向大家展示一些新的 Array 功能。大多數功能對于ES6 來說不能算是新的,其實有些是相當老了。然而,它們終于得到了各方面的支持,并與 箭頭函數和 Promise 結合的很好。所以我認為他們是新功能。
假設你想對數組的每個元素做一些事情。可以使用 forEach 或 map 函數代替 for 循環。
var values = [1,2,3,4,5,6,7,8,9,10];
values.forEach((x)=>console.log(x));
var doubled = values.map((x) => x*2);
forEach 函數在數組中的每個元素上運行回調。 map 函數也在每個元素上運行,但它將每個回調的結果存儲到一個新的數組中。
如果要從數組中過濾出某些值,請使用 filter 函數。
//查找匹配 filter 所有的值
var evens = values.filter((x)=>x%2==0);
Array還具有基于某些標準在數組中查找單個值的功能。
//查找第一個匹配的值
var six = values.find((x) => x >= 6);
//找到第一個匹配的索引
var index = values.findIndex((x) => x >= 6);
//如果至少有一項匹配,則為true
var any = values.some((x) => x > 10);
最后,可以使用 reduce 函數將數組減少到單個值。 reduce 非常強大,可以用來做很多事情,比如一個數組求和或 flatten 嵌套數組(愚人碼頭注:降低數組嵌套,例如將 [1,[2,3],4] 轉換為 [1,2,3,4] ,這就叫 flatten)。
//將數組減少到單個值
var sum = values.reduce((a,b) => a+b, 0);
//flatten 嵌套數組
var list1 = [[0, 1], [2, 3], [4, 5]];
var list2 = [0, [1, [2, [3, [4, [5]]]]]];
const flatten = arr => arr.reduce(
(acc, val) => acc.concat(
Array.isArray(val) ? flatten(val) : val
),
[]
);
flatten(list1); // returns [0, 1, 2, 3, 4, 5]
flatten(list2); // returns [0, 1, 2, 3, 4, 5]
循環對象中的屬性,您可以使用 Object.keys 獲取一個包含所有屬性名稱數組,然后用 forEach 循環它
var obj = {
first:'Alice',
middle:'N',
last:'Wonderland',
age:8,
height:45,
}
Object.keys(obj).forEach((key)=>{
console.log("key = ", key);
console.log("value = ", obj[key]);
});
要避免的事情
我已經介紹了 ES6 今天就可以使用的五個功能,但ES6中還有許多其他功能,這個階段你應該避免?或者是因為它們沒有提供有價值的東西,或者還沒有得到很好的支持。這些包括:
- Destructuring
- Modules
- Default Parameters
- Spread Operator
- Symbols
- Generators and Iterators
- Weak Maps and Weak Sets
- Proxies
解構允許你通過名稱從對象中引值。它在幾種情況下有用,但是我發現最好的用法是從模塊中提取函數。不幸的是模塊仍然是一團糟而且不要在任何地方使用,所以現在應該避免使用他們。
除了解構,建議你不要使用默認參數,和擴展操作符( ... )。我發現這些比他們使用價值更麻煩,至少現在是。
Symbols,Generators(生成器),Iterators(迭代器),Weak Maps (弱映射)和 Weak Sets(弱集合),Proxies(代理)是非常有用的,但是它們在任何地方都不支持,所以我建議你等一會兒再能使用它們。
還有一個新的類語法。它仍然使用JavaScript的原型繼承,但它使得定義一個類的語法更加清晰和一致。然而,如果沒有新的模塊支持,它也沒有價值,所以我建議等一會兒。
class Foo extends Bar {
constructor(stuff) {
super(stuff);
this.first = "first name"
}
doStuff() {
}
doMoreStuff() {
}
}
我可以用它嗎?
大多數桌面和移動端的瀏覽器都支持我向你展示的所有內容。但是,根據您的用戶情況,您可能需要支持舊的瀏覽器/舊版移動操作系統。每當你想知道他們的支持情況,去 caniuse.com 。它會告訴你每個瀏覽器的什么版本支持什么。
如果您需要支持IE 10 以下的瀏覽器,那么可以使用像 TypScript 或 Babel 這樣的轉換器。
所以這些ES6的五個很棒的功能,你可以立即開始使用。
示例代碼
我們創建了一個使用 箭頭函數 和 Promises 的 示例代碼的目錄 ,還有許多實用的Web服務進行通信,如實時語言翻譯,地理編碼和chatbot apis等等。
來自:http://www.css88.com/archives/7291