現代JavaScript—ES6+中的Imports,Exports,Let,Const和Promise

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
原文出處://www.freecodecamp.org/news/learn-modern-javascript/

 

在過去幾年裡,JavaScript有很多的更新。如果你想提升寫程式碼的能力,這些更新將會對你有非常大的幫助。

對於程式設計師來說,了解這門語言的最新發展是非常重要的。它能使你跟上最新趨勢,提高程式碼品質,在工作中出類拔萃,從而進一步提升你的薪資待遇。

特別地,如果你想學習像React、 Angular或Vue這樣的框架,你必須掌握這些最新的特性。

最近,JavaScript增加了許多有用的功能,比如Nullish coalescing operator, optional chaining, Promises, async/await, ES6 destructuring,等等。

那麼現在,我們將探討每個JavaScript開發者都應該知道的概念。

 

JavaScript中的Letconst

 

在ES6之前,JavaScript使用var關鍵字來聲明變數,var只有全局作用域和函數作用域,所謂全局作用域就是在程式碼的任何位置都能訪問var聲明的變數,而函數作用域在變數聲明的當前函數內部訪問變數。此時是沒有塊級作用域的。

隨著let和const這兩個關鍵字的添加,JS增加了塊級作用域的概念。

如何在JavaScript中使用let

當我們在用let聲明變數時,用於聲明一次之後就不能再以相同的名稱重新聲明它。

// ES5 Code
var value = 10;
console.log(value); // 10

var value = "hello";
console.log(value); // hello

var value = 30;
console.log(value); // 30

如上所示,我們多次使用var關鍵字重新聲明了變數值。

在ES6之前,我們可以使用var重新聲明之前已經聲明過的變數,這就會導致了一個問題:如果我們在不知情的情況下,在其他地方重新聲明了該變數,很有可能會覆蓋原先變數的值,造成一些難以調試的問題。

所以,Let解決很好地解決此問題。當你使用let重新聲明變數值時,將會報錯。

// ES6 Code
let value = 10;
console.log(value); // 10

let value = "hello"; // Uncaught SyntaxError: Identifier 'value' has already been declared

但是,以下程式碼是合法的:
// ES6 Code
let value = 10;
console.log(value); // 10

value = "hello";
console.log(value); // hello

我們發現上面的程式碼看起來沒什麼問題,是因為我們重新給value變數賦了一個新值,但是並沒有重新聲明。

我們來看下下面的程式碼:

// ES5 Code
var isValid = true;
if(isValid) {
  var number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10

如上所示,在使用var聲明變數時,可以在if塊之外訪問該變數。

而使用let聲明的number變數只能在if塊內訪問,如果在if塊外訪問將會報錯。

我們來看下接下來的程式碼

// ES6 Code
let isValid = true;
if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // Uncaught ReferenceError: number is not defined

如上述程式碼所示,使用let分別在if塊內、if塊外聲明了number變數。在if塊外,number無法被訪問,因此會出現引用錯誤。

但是,如果變數number在if塊外已經聲明,將會出現下面的結果。

// ES6 Code
let isValid = true;
let number = 20;

if(isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}

console.log('outside:', number); // outside: 20

現在在單獨的範圍內有兩個number變數。在if塊外,number的值為20。

// ES5 Code
for(var i = 0; i < 10; i++){
 console.log(i);
}
console.log('outside:', i); // 10

當使用var關鍵字時,i在 for循環之外也可以訪問到。

 

// ES6 Code
for(let i = 0; i < 10; i++){
 console.log(i);
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined

而使用let關鍵字時,在for循環外部是不可訪問的。

因此,正如上述示例程式碼所示,let聲明的變數只能在塊內部可用,而在塊外部不可訪問。

我們可以使用一對大括弧創建一個塊,如下:

let i = 10;
{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // outside: 10

 

前面有提到,let在同一個塊中不能重新聲明變數,不過可以在另一個塊中重新聲明。如上程式碼所示,我們在塊內重新聲明了i,並賦值20,該變數僅可在該塊中使用。

在塊外,當我們列印變數時,我們得到的是10而不是之前分配的值,這是因為塊外,內部變變數i是不存在的。

如果在塊外未聲明變數,那麼將會報錯:

{
 let i = 20;
 console.log('inside:', i); // inside: 20
 i = 30;
 console.log('i again:', i); // i again: 30
}

console.log('outside:', i); // Uncaught ReferenceError: i is not defined

 

如何在JavaScript使用const

const關鍵字在塊級作用域中的工作方式與let關鍵字完全相同。因此,我們來看下他們的區別。

const聲明的變數為常量,其值是不能改變的。而let聲明的變數,可以為其賦一個新值,如下所示:

let number = 10;
number = 20;
console.log(number); // 20

但是以下情況,我們不能這樣使用const。

const number = 10;
number = 20; // Uncaught TypeError: Assignment to constant variable.

我們甚至不能使用const像下面一樣重新聲明。

const number = 20;
console.log(number); // 20

const number = 10; // Uncaught SyntaxError: Identifier 'number' has already been declared

現在,看下面的程式碼:

const arr = [1, 2, 3, 4];
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]

我們說過const聲明的常量,它的值永遠不會改變——但是我們改變了上面的常量數組並沒有報錯。這是為什麼呢?

注意:數組是引用類型,而不是JavaScript的基本類型

實際存儲在arr中的不是數組,而是數組存儲的記憶體位置的引用(地址)。執行arr.push(5),並沒有改變arr指向的引用,而是改變了存儲在那個引用上的值。

對象也是如此:

const obj = {
 name: 'David',
 age: 30
};

obj.age = 40;

console.log(obj); // { name: 'David', age: 40 }

 

這裡,我們也沒有改變obj指向的引用,而是改變了存儲在引用的值。

因此,上述的程式碼將會起作用,但下面的程式碼是無效的。

const obj = { name: 'David', age: 30 };
const obj1 = { name: 'Mike', age: 40 };
obj = obj1; // Uncaught TypeError: Assignment to constant variable.

這樣寫會拋出異常,因為我們試圖更改const變數指向的引用。

因此,在使用const時要記住一點:使用const聲明常量時,不能重新聲明,也不能重新賦值。如果聲明的常量是引用類型,我們可以更改存儲在引用的值。

同理,下面的程式碼也是無效的。

const arr = [1, 2, 3, 4];
arr = [10, 20, 30]; // Uncaught TypeError: Assignment to constant variable.

  

總結:

  • 關鍵字let和const在JavaScript中添加塊級作用域。
  • 當我們將一個變數聲明為let時,我們不能在同一作用域(函數或塊級作用域)中重新定義或重新聲明另一個具有相同名稱的let變數,但是我們可以重新賦值。
  • 當我們將一個變數聲明為const時,我們不能在同一作用域(函數或塊級作用域)中重新定義或重新聲明具有相同名稱的另一個const變數。但是,如果變數是引用類型(如數組或對象),我們可以更改存儲在該變數中的值。

好了,我們繼續下一個話題: promises。

 

JavaScript中的promises

 

對於很多新開發者來說,promises是JavaScript中較難理解的部分。ES6中原生提供了Promise對象,那麼Promise究竟是什麼呢?

Promise 對象代表了未來將要發生的事件,用來傳遞非同步操作的消息。

如何創造一個promise

使用promise構造函數創建一個promise,如下所示:

const promise = new Promise(function(resolve, reject) {
 
});

Promise的構造函數接收一個函數作為參數,並且在內部接收兩個參數:resolve,reject。 resolve和reject參數實際上是我們可以調用的函數,具體取決於非同步操作的結果。

Promise 有三種狀態:

  • pending: 初始狀態,不是成功或失敗狀態。
  • fulfilled:表示操作成功完成。
  • rejected: 表示操作失敗。

當我們創建Promise時,它處於等待的狀態。當我們調用resolve函數時,它將進入已完成狀態。如果調用reject,他將進入被拒絕狀態。

在下面的程式碼中,我們執行了一個非同步操作,也就是setTimeout,2秒後,調用resolve方法。

 

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});

我們需要使用以下方法註冊一個回調.then獲得1promise執行成功的結果,如下所示:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve(sum);
 }, 2000);
});

promise.then(function(result) {
 console.log(result); // 9
});

then接收一個參數,是函數,並且會拿到我們在promise中調用resolve時傳的的參數。

如果操作不成功,則調用reject函數:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
});

如果sum不是一個數字,那麼我們調用帶有錯誤資訊的reject函數,否則我們調用resolve函數。

執行上述程式碼,輸出如下:

 

 

 

調用reject函數會拋出一個錯誤,但是我們沒有添加用於捕獲錯誤的程式碼。

需要調用catch方法指定的回調函數來捕獲並處理這個錯誤。

promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

輸出如下:

 

 

 

所以建議大家在使用promise時加上catch方法,以此來避免程式因錯誤而停止運行。

鏈式操作

我們可以向單個promise添加多個then方法,如下所示:

promise.then(function(result) {
 console.log('first .then handler');
 return result;
}).then(function(result) {
 console.log('second .then handler');
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

當添加多個then方法時,前一個then方法的返回值將自動傳遞給下一個then方法。

 

 

如上圖所示,我們在第一個then方法中輸出字元串,並將接收的參數result(sum)返回給下一個result。

在下一個then方法中,輸出字元串,並輸出上一個then方法傳遞給它的result。

如何在JavaScript中延遲promise的執行

很多時候,我們不希望立即創建promise,而是希望在某個操作完成後再創建。

我們可以將promise封裝在一個函數中,然後從函數中返回promise,如下所示:

 

function createPromise() {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = 4 + 5;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}

這樣,我們就可以通過函數將參數傳遞給promise,達到動態的目的。

function createPromise(a, b) {
 return new Promise(function(resolve, reject) {
  setTimeout(function() {
   const sum = a + b;
   if(isNaN(sum)) {
     reject('Error while calculating sum.');
   } else {
    resolve(sum);
   }
  }, 2000);
 });
}

createPromise(1,8)
 .then(function(output) {
  console.log(output); // 9
});

// OR

createPromise(10,24)
 .then(function(output) {
  console.log(output); // 34
});

 

 

 

此外,我們只能向resolve或reject函數傳遞一個值。如果你想傳遞多個值到resolve函數,可以將它作為一個對象傳遞,如下:

const promise = new Promise(function(resolve, reject) {
 setTimeout(function() {
  const sum = 4 + 5;
  resolve({
   a: 4,
   b: 5,
   sum
  });
 }, 2000);
});

promise.then(function(result) {
 console.log(result);
}).catch(function(error) {
 console.log(error);
});

 

 

 

如何在JavaScript中使用箭頭函數

上述示例程式碼中,我們使用常規的ES5語法創建了promise。但是,通常使用箭頭函數代替ES5語法,如下:

const promise = new Promise((resolve, reject) => {
 setTimeout(() => {
  const sum = 4 + 5 + 'a';
  if(isNaN(sum)) {
    reject('Error while calculating sum.');
  } else {
    resolve(sum);
  }
 }, 2000);
});

promise.then((result) => {
 console.log(result);
});

你可以根據自己需要使用ES5或ES6語法。

 

ES6 Import 和Export 語法

在ES6之前,我們在一個HTML文件中可以使用多個script標籤來引用不同的JavaScript文件,如下所示:

<script type="text/javascript" src="home.js"></script>
<script type="text/javascript" src="profile.js"></script>
<script type="text/javascript" src="user.js"></script>

但是如果我們在不同的JavaScript文件中有一個同名的變數,將會出現命名衝突,你實際得到的可能並不是你期望的值。

ES6增加了模組的概念來解決這個問題。

在ES6中,我們編寫的每一個JavaScript文件都被稱為模組。我們在每個文件中聲明的變數和函數不能用於其他文件,除非我們將它們從該文件中導出並、在另一個文件中得到引用。

因此,在文件中定義的函數和變數是每個文件私有的,在導出它們之前,不能在文件外部訪問它們。

export有兩種類型:

  • 命名導出:在一個文件中可以有多個命名導出
  • 默認導出:單個文件中只能有一個默認導出

JavaScript中的命名導出

如下所示,將單個變數命名導出:

export const temp = "This is some dummy text";

如果想導出多個變數,可以使用大括弧指定要輸出的一組變數。

const temp1 = "This is some dummy text1";
const temp2 = "This is some dummy text2";
export { temp1, temp2 };
需要注意的是,導出語法不是對象語法。因此,在ES6中,不能使用鍵值對的形式導出。
   // This is invalid syntax of export in ES6
 export { key1: value1, key2: value2 }
import命令接受一對大括弧,裡面指定要從其他模組導入的變數名。
import { temp1, temp2 } from './filename';

注意,不需要在文件名中添加.js擴展名,因為默認情況下會考慮該拓展名。

 // import from functions.js file from current directory 
import { temp1, temp2 } from './functions';
          // import from functions.js file from parent of current directory
import { temp1 } from '../functions';

提示一點,導入的變數名必須與被導入模組對外介面的名稱相同。

因此,導出應使用:

// constants.js
export const PI = 3.14159;

那麼在導入的時候,必須使用與導出時相同的名稱:

import { PI } from './constants';

// This will throw an error

import { PiValue } from './constants';

如果想為輸入的變數重新命名,可以使用as關鍵字,語法如下:

import { PI as PIValue } from './constants';

我們以為PI重命名為PIValue,因此不能再使用PI變數名。

 

導出時也可使用下面的重命名語法:  

// constants.js
const PI = 3.14159; 
export { PI as PIValue };

  

然後在導入是,必須使用PIValue。

import { PIValue } from './constants';

命名導出某些內容之前必須先聲明它。

export 'hello'; // this will result in error
export const greeting = 'hello'; // this will work
export { name: 'David' }; // This will result in error
export const object = { name: 'David' }; // This will work

我們來看下面的validations.js 文件:  

// utils/validations.js

const isValidEmail = function(email) {
  if (/^[^@ ]+@[^@ ]+\.[^@ \.]{2,}$/.test(email)) {
    return "email is valid";
  } else {
    return "email is invalid";
  }
};

const isValidPhone = function(phone) {
  if (/^[\\(]\d{3}[\\)]\s\d{3}-\d{4}$/.test(phone)) {
    return "phone number is valid";
  } else {
    return "phone number is invalid";
  }
};

function isEmpty(value) { 
  if (/^\s*$/.test(value)) {
    return "string is empty or contains only spaces";
  } else {
    return "string is not empty and does not contain spaces";
  } 
}

export { isValidEmail, isValidPhone, isEmpty };

在index.js中,我們可以使用如下函數:

// index.js
import { isEmpty, isValidEmail } from "./utils/validations";

console.log("isEmpty:", isEmpty("abcd")); // isEmpty: string is not empty and does not contain spaces

console.log("isValidEmail:", isValidEmail("[email protected]")); // isValidEmail: email is valid

console.log("isValidEmail:", isValidEmail("ab@[email protected]")); // isValidEmail: email is invalid

JavaScript的默認導出

如上所述,單個文件中最多只能有一個默認導出。但是,你可以在一個文件中使用多個命名導出和一個默認導出。

要聲明一個默認導出,我們需要使用以下語法:

//constants.js
const name = 'David'; 
export default name;

在導入時就不需要再使用花括弧了。  

import name from './constants';

  

如下,我們有多個命名導出和一個默認導出:

// constants.js
export const PI = 3.14159; 
export const AGE = 30;

const NAME = "David";
export default NAME;

  

此時我們使用import導入時,只需要在大括弧之前指定默認導出的變數名。

// NAME is default export and PI and AGE are named exports here
import NAME, { PI, AGE } from './constants';

使用 export default 導出的內容,在導入的時候,import後面的名稱可以是任意的。

// constants.js
const AGE = 30;
export default AGE;

import myAge from 『./constants』; 
console.log(myAge); // 30

另外, export default的變數名稱從Age到myAge之所以可行,是因為只能存在一個export default。因此你可以隨意命名。還需注意的是,關鍵字不能在聲明變數之前。

// constants.js
export default const AGE = 30; // This is an error and will not work

因此,我們需要在單獨的一行使用關鍵字。

// constants.js 
const AGE = 30; 
export default AGE;

  

不過以下形式是允許的:

//constants.js
export default {
 name: "Billy",
 age: 40
};

並且需要在另一個文件中使用它

import user from './constants';
console.log(user.name); // Billy 
console.log(user.age); // 40

  

還有,可以使用以下語法來導入constants.js文件中導出的所有變數:

// test.js
import * as constants from './constants';

 

下面,我們將導入所有我們constants.js存儲在constants變數中的命名和export default。因此,constants現在將成為對象。

// constants.js
export const USERNAME = "David";
export default {
 name: "Billy",
 age: 40
};

在另一個文件中,我們按一下方式使用。

// test.js

import * as constants from './constants';

console.log(constants.USERNAME); // David
console.log(constants.default); // { name: "Billy", age: 40 }
console.log(constants.default.age); // 40

也可以使用以下方式組合使用命名導出和默認導出:

// constants.js
const PI = 3.14159; const AGE = 30;
const USERNAME = "David";
const USER = {
 name: "Billy",
 age: 40 
};
export { PI, AGE, USERNAME, USER as default };

import USER, { PI, AGE, USERNAME } from "./constants";

總而言之:

ES6中,一個模組就是一個獨立的文件,該文件內部的所有變數,外部都無法獲取。如果想從外部讀取模組內的某個變數,必須使用export關鍵字導出該變數,使用import關鍵字導入該變數。

 

JavaScript中的默認參數

ES6增加了一個非常有用的特性,即在定義函數時提供默認參數。

假設我們有一個應用程式,一旦用戶登錄系統,我們將向他們顯示一條歡迎消息,如下所示:

function showMessage(firstName) {
  return "Welcome back, " + firstName;
}
console.log(showMessage('John')); // Welcome back, John

但是,如果資料庫中沒有用戶名,那該怎麼辦呢?所以,我們首先需要檢查是否提供了firstName,然後再顯示相應的資訊。

在ES6之前,我們必須寫這樣的程式碼:  

function showMessage(firstName) {
  if(firstName) {
    return "Welcome back, " + firstName;
  } else {
    return "Welcome back, Guest";
  }
}

console.log(showMessage('John')); // Welcome back, John 
console.log(showMessage()); // Welcome back, Guest

但現在使用ES6提供的默認參數,我們可以這樣寫:

function showMessage(firstName = 'Guest') {
   return "Welcome back, " + firstName;
}

console.log(showMessage('John')); // Welcome back, John 
console.log(showMessage()); // Welcome back, Guest

函數的默認參數可以為任意值。

function display(a = 10, b = 20, c = b) { 
 console.log(a, b, c);
}

display(); // 10 20 20
display(40); // 40 20 20
display(1, 70); // 1 70 70
display(1, 30, 70); // 1 30 70

在上面的程式碼中,我們沒有提供函數的所有參數,實際程式碼等同於:

display(); // 等同於display(undefined, undefined, undefined)
display(40); 等同於display(40, undefined, undefined)
display(1, 70); 等同於display(1, 70, undefined)

因此,如果傳遞的參數是undefined,則對應的參數將使用默認值。

我們還可以將對象或計算值指定為默認值,如下:

const defaultUser = {
  name: 'Jane',
  location: 'NY',
  job: 'Software Developer'
};

const display = (user = defaultUser, age = 60 / 2 ) => {

 console.log(user, age);

};

display();

 

/* output

 

{

  name: ‘Jane’,

  location: ‘NY’,

  job: ‘Software Developer’

} 30

*/

  

ES5程式碼如下:

// ES5 Code
function getUsers(page, results, gender, nationality) {
  var params = "";
  if(page === 0 || page) {
   params += `page=${page}&`; 
  }
  if(results) {
   params += `results=${results}&`;
  }
  if(gender) {
   params += `gender=${gender}&`;
  }
  if(nationality) {
   params += `nationality=${nationality}`;
  }

  fetch('//randomuser.me/api/?' + params) 
   .then(function(response) {
     return response.json(); 
   })
   .then(function(result) { 
    console.log(result);
   }) 
   .catch(function(error) {
     console.log('error', error); 
   }); 
}

getUsers(0, 10, 'male', 'us');

  

在這段程式碼中,我們通過在getUsers函數中傳遞各種可選參數來進行API調用。在進行API調用之前,我們添加了各種if條件來檢查是否添加了參數,並基於此構造查詢字元串,如下所示:

//randomuser.me/api/? page=0&results=10&gender=male&nationality=us

 

使用ES6的默認參數則不必添這麼多if條件,如下所示:

function getUsers(page = 0, results = 10, gender = 'male',nationality = 'us') {
 fetch(`//randomuser.me/api/?page=${page}&results=${results}&gender=${gender}&nationality=${nationality}`)
 .then(function(response) { 
  return response.json();
 }) 
 .then(function(result) {
   console.log(result); 
 })
 .catch(function(error) { 
  console.log('error', error);
  }); 
}
getUsers();

這樣一來,程式碼得到了大量的簡化,即便我們不為getUsers函數提供任何參數時,它也能採用默認值。當然,我們也可以傳遞自己的參數:

getUsers(1, 20, 'female', 'gb');

它將覆蓋函數的默認參數。

null不等於未定義

注意: 定義默認參數時,null和undefined是不同的。

我們來看下面的程式碼:

function display(name = 'David', age = 35, location = 'NY'){
 console.log(name, age, location); 
}
display('David', 35); // David 35 NY
display('David', 35, undefined); // David 35 NY
// OR
display('David', 35, undefined); // David 35 NY
display('David', 35, null); // David 35 null
當我們傳遞null作為參數時,它實際是給location參數賦一個空值,與undefined不一樣。所以它不會取默認值「NY」。

Array.prototype.includes

ES7增加了數組的includes方法,用來判斷一個數組是否包含一個指定的值,如果是返回 true,否則false。
// ES5 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.indexOf("one") > -1); // true 
console.log(numbers.indexOf("five") > -1); // false

數組可以使用includes方法:

// ES7 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.includes("one")); // true 
console.log(numbers.includes("five")); // false
includes方法可以使程式碼簡短且易於理解,它也可用於比較不同的值。
const day = "monday";
if(day === "monday" || day === "tuesday" || day === "wednesday") {
  // do something
}

// 以上程式碼使用include方法可以簡化如下:
const day = "monday";
if(["monday", "tuesday", "wednesday"].includes(day)) {
  // do something
}
因此,在檢查數組中的值時,使用includes方法將會非常的方便。
結語
從ES6開始,JavaScript中發生許多變更。對於JavaScript,Angular,React或Vue開發人員都應該知道它們
了解這些變更可以使你成為更棒的開發者,甚至可以幫助您獲得更高的薪水。而且,如果你只是在學習React之類的庫以及Angular和Vue之類的框架,那麼您一定要掌握這些新特性。