JavaScript 常用陣列方法

August 7, 2015

文章內容:

在開始之前

我們在執行 js 檔案的時候,都可以在 CLI 下直接執行 node yourFile.js 並看到檔案輸出的結果, 但是如果直接透過 nodejs 來執行 ES6 語法的檔案,會出現錯誤,所以我們必須先:

  1. 透過全域安裝 babel$ npm install babel -g

  2. 或者透過線上的輔助工具來看我們的執行結果:ES6 Fiddle

當然我自己是推薦使用前者囉,畢竟在本地開發使用比較方便呀!

安裝完成 babel 之後,接下來我們就可以開始寫我們的 js 檔案囉,順帶提一下: 透過 babel 是將 ES6 的語法轉譯成 ES5,因為現在瀏覽器還 不完全 支援 ES6 語法,所以透過轉譯可以解決這個問題。

更多細節可以參考:ECMAScript compatibility table  

如果要在 CLI 直接看到輸出的結果,請使用 babel-node yourFile.js 來執行 js 檔案。 那麼,接下來,就開始介紹 ES6 的一些 Feature 囉!

const, let

以往我們在宣告變數時,都是透過 var 來做宣告,但是這其實有許多隱藏的問題,舉例一下:

var a = 1;

function getValue() {
    // a = 3;
    console.log(a);
}

getValue() // 1

上面的例子,你會發現到,在 function 內部我沒有宣告出 a 這個變數,但是執行過後結果會出現 1, 這是 Scope 的問題,因為在 function 內他找不到 a 這個變數,他就會往外再去找,所以 在外部有一個宣告是 var a = 1,所以最後的結果會是 1,反之如果在內部事先宣告,結果則不同。

在 ES6 可以透過 const, let,因為 const, let 屬於 Block Scopeconst常數let變數。 他們的作用域只會在他們的區塊內 ,如果在區塊以外去呼叫這個變數,會出現錯誤。

var number = 1;

if (true) {
    var number = 2; // same variable
    console.log(number); // 2
}

console.log(number) // 2
let number = 1;

if (true) {
    let number = 2; // different variable
    console.log(number); // 2
}

console.log(number) // 1

可以比較上面的兩個範例,可以看到他們之間的差別。

const myName = 'Peng-jie';
let age = 21;

// Change
age = 22;
myName = 'pengjie';

console.log(age);
console.log(myName); // SyntaxError, "myName" is read-only

Arrow Function

Arrow function (箭頭函式) 是 ES6 很重要的一個 feature 之一,它讓我們在表達上可以更簡潔, 所有的 Arrow Function 都是匿名函式 (Anonymous function), 而且 Arrow Function 還自動了幫我們綁定 (bind) 了 scope 目前的 this 變數。

const numArr = [1, 2, 3];

let newNumArr = numArr.map(n => n + 1);

console.log(newNumArr); // [2, 3, 4]

我們可以看到在 map function 的地方我們寫得非常簡化,以往我們都會寫成:

numArr.map(function (n) {
    return n + 1;
});

透過 Arrow Function 可以讓匿名函式變得更簡單,更直覺。 另外一個綁定 this 變數的問題,在 Arrorw Function 也可以讓我們更容易解決,先看下面的一個範例:

function Person() {
  this.age = 0;

  setInterval(function growUp() {
    console.log(this.age++);
  }, 1000);
}

var p = new Person();

在上面這個範例當中,會發現結果一直都是 NaN,原因是因為 setInterval 無法參考到外部的 this.age 所以通常會改變作法,如下方:

function Person() {
  var self = this;
  self.age = 0;

  setInterval(function growUp() {
    console.log(self.age++);
  }, 1000);
}

var p = new Person();

我們先將目前 Person 的 scope 中的 this 存進一個叫 self 的變數,接下來在 setInterval scope 內 透過我們事先存好的 self,這時候就可以參考到外部的 age 了,如果沒注意到,就是一個地雷呀! 以前剛開始看別人寫這樣的 code,都不理解為什麼要寫一個 var self = this,直到自己踩過雷才理解~ 但是 Arrow Function 幫助我們避免犯這種錯誤,我們看看下方的寫法:

function Person(){
  this.age = 0;

  setInterval(() => {
    console.log(this.age++);
  }, 1000);
}

var p = new Person();

WOW!It can Work! 可以發現到 setInterval 內部是寫 this.age,而不是再透過 self 的方式了,整個變好簡潔,真的很棒! reference from: MDN JavaScript Functions - Arrow Function

Destructuring Assignment

解構賦值其實可以把它想像成是一個 拆解 的動作,就是將你結構中的東西拆成小區塊, 對象可以是對陣列 (Array) 或者是物件 (Object)

const myName = {
    firstName: 'Peng',
    lastName: 'Jie'
};

let { firstName, lastName } = myName;

console.log(firstName, lastName); // Peng Jie

// or use
// let { firstName: first, lastName: last } = myName;
// console.log(first, last);
const arr = ['ECMAScript', 'ES6', '2015'];

let [one, two, three] = arr;

console.log(one, two , three);

陣列解構的部分,還可以像這樣使用:

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log({a, b}); // { a: 2, b: 1 }

// 或者可以選取指定的 value
let [,, three] = [1, 2, 3];
console.log(three); // 3

// 使用 spread operator
let [first, ...other] = [1, 2, 3, 4, 5, 6, 7];
console.log(first); // 1
console.log(other); // [2, 3, 4, 5, 6, 7]

Template Strings

我們在顯示字串都是透過單引號,或者是雙引號,但是每次再加入變數的時候就要在寫個 + 來串接, 實在不是很方便,在 ES6 的 Template Strings 可以讓這些方法變得更簡單。

const myName = 'Peng-Jie';

console.log(`Hello, My name is ${myName}`);

更有趣一點的,你可以定義一個 css style,再透過 Template Strings 把值帶入:

const style = {
    color: 'red',
    size: 4
};

let temp = `
    <span>
        <font size="${style.size}" color="${style.color}">Yolo~</font>
    </span>
`;
console.log(temp);

Object literals

在這個部分,介紹的是更簡短的寫法。

function aboutMe(first, last) {
    return {
        first,
        last,
    }
}

console.log(aboutMe('P', 'J')); // { first: 'P', last: 'J' }

// {first: first, last: last} <=> { first, last } 這兩個是一樣的意思。

// ---- 分隔線 ----

const circle = {
    radius: 4,
    calculate() { // 以往寫成 calculate: function() {}
        return this.radius * this.radius * 3.14
    }
}

console.log(circle.calculate()); // 50.24

Default Parameters

Default Parameters 其實就是字面上的意思,我們可以事先給參數一些預設值。

function getGitHubUrl(account, base='https://github.com') {
    return `${base}/${account}`
}

console.log(getGitHubUrl('neighborhood999')); // https://github.com/neighborhood999

Spread Operator

Spread Operator 是一個展開運算符,可以讓我們很方便來操作陣列。

const arr = ['a', 'b', 'c', 'd', 'e'];

let [first, second, ...other] = arr;

console.log(first, second, other); // a, b, ['c', 'd', 'e']

Iterator

Iterator 可以讓物件去定義他們本身的迭代行為,則物件可以透過 for...of 來進行迭代。 Iterator 物件實作了 next() 方法,他會回傳 { value: value, done: boolean }

// next()
const myName = 'Peng';
const iterator = myName[Symbol.iterator]();

console.log(iterator.next()); // { value: 'P', done: false }
console.log(iterator.next()); // { value: 'e', done: false }
console.log(iterator.next()); // { value: 'n', done: false }
console.log(iterator.next()); // { value: 'g', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

// for...of
const arr = ['Hello', 'ECMAScript 6'];
const es6 = 'I\'m learning ECMAScript 6.';

for (let item of arr) {
    console.log(item); // Hello, ECMAScript 6
}

for (let j in arr) { // 這邊用到的是 for...in, for...in 取得的是 key 值。
    console.log(`${j}: ${arr[j]}`); // 0: Hello, 1: ECMAScript 6
}

for (let s of es6) {
    console.log(s);
}

Classes

我們在 javascript 中,在某種程度上我們其實可以把 function 當作一個 class, 事實上,物件導向有兩個分支 Class 繼承Prototype 繼承,只是 Class 繼承比較普遍。

  • 可以參考 本文 有更詳細的解說。

下面的方法是透過 Prototype 繼承的方式:

function Person() {
    this.name = 'Yo';
}

Person.prototype.sayHi = function() {
    console.log(`Hi, ${this.name}`);
};

const p = new Person();
console.log(p.name); // Yo

p.sayHi(); // Hi, Yo

以下是透過 ES6 Class 的方法來建立一個 Class:

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getPerson() {
        return `My name is ${this.firstName}-${this.lastName}`
    }
}

const aboutMe = new Person('Peng', 'Jie');
console.log(aboutMe.getPerson()); // My name is Peng-Jie

// Extended
class SuperMan extends Person {
    constructor(firstName, lastName, power) {
        super(firstName, lastName);
        this.power = power;
    }

    getPerson() {
        return `${super.getPerson()}. ${this.power}% power!`
    }
}

const s = new SuperMan('Iron', 'Man', 100);
console.log(s.getPerson()); // My name is Iron-Man. 100% power!

在 class 中我們可以看到有一個 constructor,就是我們常用的建構方法, 而 class 也可以透過繼承的方式,透過 super() 的方法,將父類別透過重新定義把 method override。

Generator

Generator 和 Iterator 關係很密切。

Generator 可以把它理解成一個狀態機,在內部封裝了不同的狀態。 Generator 函數的形式:function* gen(),在 function 後多了一個 * 來表示, 我們可以這樣想,其實 Generator 是在函數內中,增加了其他額外的進入點,當你執行函數的時候, 剛開始,Generator 並不會執行內部的函數,而是回傳一個 Generator Iterator,

遇到 yield 會暫停執行,並將會回傳 yield 後面的表達式的值,而 next() 會指向下一個狀態。

function* getValue() {
    let value = 0;
    while (value < 2) {
        yield value += 1;
    }
}

const generator = getValue(); // 第一次是回傳一個 generator 物件

console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: undefined, done: true }

在透過 generator.next(),可以看到返回的 value,以及 done,在第三次的時候會發現 value 已經是 undefined 了,而 done 代表已經完成了。

Modules

Javascript 內建的的模組,讓我們可以將許多東西拆成許多小區塊,模組化的好處是可以將這些不同的模組, 透過組合的方式,來達成不同的功能,好比我們再組樂高一樣,模組化可以讓我們可以更容易維護和擴充。 透過 export (輸出)import (引入) 的方式,來達成模組化。

// --- test.js ---
export const myName = 'Peng-Jie';
export let age = 21;
export function aboutMe(myName, age) {
    return `My name is ${myName}, I'm ${age} years old.`
}

// --- index.js ---
import { myName, age, aboutMe } from 'test';

console.log(myName, age); // Pengjie 21
console.log(aboutMe('Ho', 22)); // My name is Ho, I'm 22 years old.

Promise

Promsie 使用的場景通常都是在非同步的計算,包含了三種狀態:

  • pending -> 初始狀態,還沒有肯定或者是否定的結果。
  • fulfilled -> 操作成功。
  • rejected -> 操作失敗。
JavaScript Promise Status
Photo via JavaScript Promise Book

Promise 簡單說就是一個狀態機,從原先的 pending 狀態轉變成 fulfilled 或者是 rejected, 當狀態決定之後,不能再去修改他的狀態。 當 pending 狀態轉為成功的時候,會使用 resolve,反之為 reject。 透過鏈結式的方法,我們可以任意的將方法連結在一起做為一個方法鏈結(Method Chain),例如:

promise
    .then(function A(res) {
        //
    })
    .then(function B(res) {
        //
    })
    .catch(function (err) {
        //
    });

而一個 Promise 基本的使用方法大概像是下方的範例:

const promise = new Promise((resolve, reject) => {
  let value = 7;

  if (value > 5) {
        resolve('well done.'); // success
  } else {
        reject(Error('Failed')); // failed
  }
});

promise
    .then(function(res) {
        console.log(res);
    })
    .catch(function(err) {
        console.log(err);
    });

Map、Set、WeakMap、WeakSet

Map

Map 其實就是一個簡單的鍵/值(key-value pair),但是比較特別的地方是 key 和 value 可以是任意值。 我們可以透過以下的方式來操作:

  • get(key) -> 透過 key 來取得值,如果沒有則回傳 undefined
  • set(key, value) -> 設定 key 和 value。
  • has(key) -> 判斷 key 是否存在,回傳一個 boolean
  • delete(key) -> 刪除指定的 key 以及對應的 value。
  • clear() -> 清除所有的 key 和 value。
var m = new Map();

console.log(m.get('Yo')); // 尚未設定,回傳 undefined

m.set('name', 'Peng');
console.log(m.get('name')); // Peng

m.set('info', {name: 'Pengjie', age: 22, school: 'CYUT'});
console.log(m.get('info')); // { name: 'Pengjie', age: 22, school: 'CYUT' }
console.log(m.has('info')); // true

console.log(JSON.stringify(m)); // 查看整個 m 的結構

Set

Set 可以讓你儲存任意類型的值,但是不能重複,我們可以透過以下的方式來操作:

  • add() -> 加入值。
  • has() -> 判斷值是否存在。
  • size -> 判斷 Set 的長度大小。
  • delete() -> 刪除指定的值。
  • clear() -> 清除所有的值。
var s = new Set();
const arr = [1, 2, 2, 3, 3, 4, 5];

arr.map(x => s.add(x));
console.log(s); // {1, 2, 3, 4, 5}, 唯一的值,不能重複。

// ---- 分隔線 ----

const arr2 = [1, 2, 2, 3, 3, 4, 5];
s.add(arr2);
console.log(s); // { 1, 2, 3, 4, 5, [ 1, 2, 2, 3, 3, 4, 5 ] };整個陣列當成是一個值。

WeakMap、WeakSet

MapWeakMap 結構上相似,而 SetWeakSet 也是結構上相似,差別在於 WeakMapWeakSetWeakMapkey 值只接受 Object,而 WeakSet 的值只接受 Object

// WeakMap
var wm = new WeakMap();

const Person = {
    name: 'hi'
}

wm.set(Person, 'ho');
console.log(wm.has(Person)); // true

// ---- 分隔線 ----

// WeakSet
var ws = new WeakSet();

const Person = {
    name: 'hi'
}

ws.add(Person);

console.log(ws.has(Person)); // true

特別感謝 C. T. Lin 協助及指導!

資料參考來源: