ES的那些新特性

流汗去發表於2018-06-26

前段時間換工作,面試了幾家公司,有一道題發現基本是必問的,就是說一說平時用到的那些 es6 7 8 9 等的特性。一直沒有做總結,現在就整理一下平時自己在工作中用到的比較多的那些新特性。

const and let

宣告變數新增的兩個關鍵詞,與 var 不同的一點在於,在 JS 函式中的 var 宣告,其作用域是函式體的全部,而 const 和 let 是有塊作用域的。

看一個經典的閉包問題,現在用 let 就可以輕易地避免了。

// before
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // => 3, 3, 3
  });
}

// now
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  });
}
複製程式碼

const 是常量的宣告,表示的是變數不可被再次賦值,而變數的值是可以改變的

const obj = { a: 1 };

obj = 'a'; // => Uncaught TypeError: Assignment to constant variable.

obj.b = 2;
複製程式碼

Template Literals

允許嵌入表示式的字串字面量,可以使用多行字串和字串插值功能。

const name = 'laohan';
const grettings = `hello ${name}`;

console.log(grettings); // => hello laohan

const html = `
  <div>
    利用字面量,可以很方便的實現多行字串
  </div>
`;
複製程式碼

String padding

用另一個字串填充當前字串(重複,如果需要的話),以便產生的字串達到給定的長度。

String.prototype.padStart

str.padStart(targetLength [, padString])

'abc'.padStart(10);         // '       abc'
'abc'.padStart(10, "foo");  // 'foofoofabc'
複製程式碼

String.prototype.padEnd

str.padEnd(targetLength [, padString])

'abc'.padEnd(10);          // 'abc       '
'abc'.padEnd(10, "foo");   // 'abcfoofoof'
複製程式碼

padStart 與 padEnd 的不同在於填充的字串是在當前字串的左邊還是右邊。padStart 在左,padEnd 在右。

Exponentiation infix operator (指數運算子)

// before
Math.pow(2, 3); // => 8

// now
2 ** 3; // => 8
複製程式碼

Array and Object destructing

解構賦值

const arr = [1, 2, 3];
// before
var a = arr[0];
var b = arr[1];
var c = arr[2];

// now
const [a, b, c] = arr;
console.log(a, b, c); // => 1, 2, 3

const obj = {
  a: 1,
  b: 2,
  c: 3,
};

// before
var a = obj.a;
var b = obj.b;
var c = obj.c;

// now
const { a, b, c } = obj;
console.log(a, b, c); // => 1, 2, 3
複製程式碼

Arrow Functions

更簡短的函式並且不繫結 this。 箭頭函式沒有自己的 this,適用於那些本來需要匿名函式的地方。

// before
function log(name) {
  console.log(name);
}
// now
// 當引數只有一個的時候,()是可以省略的
const fn = name => {
  console.log(name);
};
複製程式碼

箭頭函式寫起來更簡短,但也不能到處用。有些不該用的地方使用了箭頭函式,會得到不是你想要的結果

const obj = {
  name: 'laohan',
  getName() {
    console.log(this.name);
  },
};

obj.getName(); // => laohan

const obj = {
  name: 'laohan',
  getName: () => {
    console.log(this.name);
  },
};
obj.getName(); // => undefined
複製程式碼

Object.assign()

將所有可列舉屬性的值從一個或多個源物件複製到目標物件

const obj1 = { a: 1, b: 2 };
const obj2 = { c: { d: 3 } };

const obj3 = Object.assign({}, obj1, obj2);
// 這裡也可以利用解構來實現
const obj4 = { ...obj1, ...obj2 };
console.log(obj3); // => {a: 1, b: 2, c: {d: 3}}

// ps: Object.assign是淺複製的
obj2.c.d = 4;
console.log(obj3); // => {a: 1, b: 2, c: {d: 4}}
複製程式碼

Object.keys()

返回一個由一個給定物件的自身可列舉屬性組成的陣列。

const obj = {
  a: 1,
  b: 2,
  c: 3,
};
const keys = Object.keys(obj);
console.log(keys); // => ['a', 'b', 'c']
複製程式碼

Object.values()

返回一個給定物件自己的所有可列舉屬性值的陣列。

const obj = {
  a: 1,
  b: 2,
  c: 3,
};

const values = Object.values(obj);
console.log(values); // => [1, 2, 3]
複製程式碼

Object.entries()

返回一個給定物件自身可列舉屬性的鍵值對陣列

const obj = {
  a: 1,
  b: 2,
  c: 3,
};
const entries = Object.entries(obj);
console.log(entries); // => [['a', 1], ['b', 2], ['c', 3]]
複製程式碼

Array.prototype.includes

用於判斷陣列中是否包含某一個指定的值,根據情況,如果包含則返回 true,否則返回 false。

const arr = [1, 2, 3, NaN];

// before
if (arr.indexOf(3) !== -1) {
  console.log(true);
}

// now
if (arr.includes(3)) {
  console.log(true);
}

// ps: indexOf 對於無法判斷是否包含 NaN
arr.includes(NaN); // => true
arr.indexOf(NaN); // => -1
複製程式碼

Promise

Promise 物件用於表示一個非同步操作的最終狀態(完成或失敗),以及其返回的值。 Promise 的出現可以解決‘回撥 - 地獄’的問題,通過 then 的鏈式呼叫,使得我們的程式碼看起來更加美觀一些。

function asyncFunc() {
  return new Promise((resolve, reject) => {
    ...
    resolve(result)
    ...
    reject(error)
  })
}

asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });
複製程式碼

這篇文章 We have a problem with promises 可以看一看,理解裡面的問題就算懂了。

Map

跟 Object 類似,用於儲存鍵值對。但還是有一些區別

  • Object 的 key 只能是字串或者 Symbols, Map 的 key 可以是任意值,包含數字、物件等
  • Map 可以通過 size 獲得鍵值對的個數,而 Object 不行
  • Map 是可迭代的,而 Object 的迭代需要先獲取它的鍵陣列然後再進行迭代
const m = new Map();
const obj = {};
const fn = () => {};
m.set(obj, 'object');
m.set(fn, 'function');
m.set(1, 'number');

m.size; // => 3

m.get(obj); // => object
m.get(fn); // => function

const mm = new Map();
mm.set(1, 'a');
mm.set(2, 'b');

for (var [key, value] of mm) {
  console.log(key + ' = ' + value);
}
// output
// 1 = a
// 2 = b
複製程式碼

Set

與 Array 類似,不同的在於 Set 中的元素只會出現一次,即元素是唯一的

const s = new Set();
mySet.add(1); // Set(1) {1}
mySet.add(5); // Set(2) {1, 5}
mySet.add(5); // Set { 1, 5 }
複製程式碼

我們經常看到陣列去重的方法是利用 Set 來完成的

const arr = [1, 2, 3, 3, 4, 4, 5];
const uniArr = Array.from(new Set(arr));
console.log(uniArr); // => [1, 2, 3, 4, 5]
複製程式碼

for...of

新的迴圈方式,代替之前的 for...in 和 forEach 方法。 可在可迭代物件(包括 Array,Map,Set,String,arguments 物件等等)進行迭代迴圈

const arr = ['a', 'b'];
for (const x of arr) {
  console.log(x);
}
// Output
// a
// b
複製程式碼

break 和 continue 也可以用於 for...of 迴圈

const arr = ['a', '', 'b'];
for (const x of arr) {
  if (x.length === 0) break;
  console.log(x);
}

// Output:
// a
複製程式碼

如果需要在迴圈中同時獲取 index 索引的時候,可以這麼寫

const arr = ['a', 'b'];
for (const [index, element] of arr.entries()) {
  console.log(`${index}. ${element}`);
}

// Output:
// 0. a
// 1. b
複製程式碼

需要注意的是,for...of 僅可用於可迭代物件

// Array-like, but not iterable!
const arrayLike = { length: 2, 0: 'a', 1: 'b' };

for (const x of arrayLike) {
  console.log(x);
}
// => TypeError: arrayLike is not iterable

for (const x of Array.from(arrayLike)) {
  console.log(x); // OK
}
複製程式碼

上面的 arrayLike 雖然跟 Array 相似,有 length 屬性,也有索引等,為什麼沒有辦法像 Array 那樣實現 for...of 的迴圈呢? 其實,Array、String 能使用 for...of 是由於其原型中有一個[Symbol.iterator]的屬性,如果我們給上面的 arrayLike 也新增這個屬性,那麼也是可以實現 for...of 操作的。

const arrayLike = {
  length: 2,
  0: 'a',
  1: 'b',
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        if (i < arrayLike.length) {
          return { done: false, value: arrayLike[i++] };
        }
        return { done: true };
      },
    };
  },
};
for (const x of arrayLike) {
  console.log(x);
}
複製程式碼

Generator Functions

function* genFunc() {
  console.log('First');
  yield;
  console.log('Second');
}

const genObj = genFunc();

genObj.next();
// Output: First
genObj.next();
// output: Second
複製程式碼

function* 是一個新的'關鍵詞',標識 generator 函式。

生成器物件是由一個 generator function 返回的,並且它符合可迭代協議和迭代器協議,主要有兩點:

  1. 為迭代提供更高層次的抽象
  2. 提供新的控制流程來幫助解決“回撥 - 地獄”問題。

在之前,我們是通過新增[Symbol.iterator]屬性來實現可迭代的。其實還可以通過 generator 來實現

const obj = {
  length: 2,
  0: 'a',
  1: 'b',
  // generator 會返回一個 iterator
  *getIterator() {
    let i = 0;
    while (i < this.length) {
      yield this[i++];
    }
  },
};

for (const x of obj.getIterator()) {
  console.log(x);
}
複製程式碼

我們之前寫非同步函式的時候,經常都是利用 callback 的形式去完成。利用 generator,就可以有一種新的方式了。

const fs = require('fs');
const path = require('path');

function* logFiles(dir) {
  for (const fileName of fs.readdirSync(dir)) {
    const filePath = path.resolve(dir, fileName);
    yield filePath;
    const stats = fs.statSync(filePath);
    if (stats.isDirectory()) {
      yield* logFiles(filePath);
    }
  }
}
for (const p of logFiles(process.argv[2])) {
  console.log(p);
}
複製程式碼

async / await

async / await 的出現是為了幫我們解決“回撥 - 地獄”的問題,讓我們可以編寫出看似同步的程式碼來實現非同步呼叫。

async 關鍵詞告訴 js 編譯器區別對待這個函式。 當呼叫一個 async 函式時,會返回一個 Promise 物件,async 函式中可能也會有 await 表示式,這會使 async 函式暫停執行,等待表示式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。

function getUserName() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('laohan');
    }, 1000);
  });
}

function getUserPhone() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1234567);
    }, 1000);
  });
}

async function getUserInfo() {
  const name = await getUserName();
  const phone = await getUserPhone();

  console.log(name); // => 'laohan'
  console.log(phone); // => 1234567
}
複製程式碼

雖然使用 async 可以讓我們的函式看起來像同步一樣的,但對於一些新手來說,如果不小心的話,會犯一些小錯誤。 如上面的程式碼,getUserName 和 getUserPhone 兩者是沒有關聯的,並不需要等到 getUserName 完成之後才去呼叫 getUserPhone,這兩個是可以並行執行的。

async function getUserInfo() {
  const [name, phone] = await Promise.all([getUserName(), getUserPhone()]);
  console.log(name); // => 'laohan'
  console.log(phone); // => 1234567
}
複製程式碼

歡迎討論

討論地址 ES的那些新特性 Issue

參考資料

JavaScript Symbols, Iterators, Generators, Async/Await, and Async Iterators — All Explained Simply Here are examples of everything new in ECMAScript 2016, 2017, and 2018 Coding recipe: extracting loop functionality (via callbacks and generators)

相關文章