Javascript ES6 特性逐步解析

甜而不膩的語法糖,寫JS效率提升的不二法門。

ES6的正式版出現,簡化了以往javascript比較繁瑣的處理,也新增了一些新的feature,可以視為是語法糖,雖然底層實作還是一樣。但對開發者可以省去更多的時間,也簡潔了語法。

Functional Programming

函式編程,是宣告式(declarative programing)的,如LISP, Scala, R…等等都是這類的代表,另外強型別語言也開始有類似的設計在裡面,如.net的lambda。最大的特點就是避免函式處理的過程中產生狀態變更或是資料異變,如redux中很強調的immutable,才能比對資料處理前後的狀態。每一個編程只要輸入是相同,輸出的結果就會一致,如果函式中有隨機的演算法,那它就是一個變異的程式。簡單來說,把每個函式當成是單純的數學計算,如此可以將一些複雜運算拆解成多個使用函式編程的過程。另外,就是避免side effect和容易實行單元測試等優點。來看以下費氏數列的例子:

費氏數列使用函式編程的方式實做:

function fib(n) {
if (n <= 0 ) return 0;
else if (n === 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
console.log(fib(10));
// 55

將費氏數列改成用Procedural的上來實做:

functional fib(n) {
var a = 0, b = 1;
for (var i = 0; i < n; i++) {
var tmp = a + b;
b = tmp;
}
}
console.log(a);

我們將費氏數列改成用es6的箭頭函示(arrow)來實作:

var fib = (n) = > n <= 0 ? 0 : n === 1 ? 1 : fib(n -1) + fib(n - 2);

Array Iteration Method: map

var square = ary => ary.map(i => i * i);
console.log(square([1, 2, 3]));
// [ 1, 4, 9 ]

Array Iteration Method: filter

var odd = ary => ary.filter(i => i % 2 === 1);
console.log(odd([1, 2, 3]));
// [ 1, 3 ]

Array Iteration Method: chaining

var squareOdd = ary => ary.filter(i => i % 2 === 1).map(i => i * i);
console.log(squareOdd([1, 2, 3]));
// [ 1, 9 ]

Array Iteration Method: reduce

var sum = ary => ary.reduce((prev, next) => prev + next, 0);
console.log(sum1([1, 2, 3]));
// 6

其他array的方法使用箭頭函示的寫法都大同小異,這邊就不贅述了。簡單來說,箭頭函式就是省略了function,並將函式的大括號前面多加上箭頭。

Object-oriented Programming

一般來說,物件導向具有以下特性:

  • Classes/Object
  • Methods/Properties
  • Encapsulation
  • Composition
  • Inheritance
  • Polymorphism

在還沒有ES6之前,物件導向的實作一直都是為人所垢病,寫法和其他OOP語言差異很大也不直覺,讓我們先來看ES5類別/物件的繼承作法,

var Shape = function(x, y){
this.x = x;
this.y = y;
};
var Circle = function(x, y, radius){
Shape.call(this, x, y);
this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
var shape = new Shape(1, 2);
var circle = new Circle(1, 2, 3);
console.log(shape instanceof Shape); 
// true
console.log(circle instanceof Shape);
// true
console.log(Object.getPrototypeOf(shape) == Shape.prototype);
// true
var circle1 = Circle(2, 3, 5);
console.log(circle1 instanceof Shape);
// false

如果用ES6寫出來的結果,就跟一般物件導向的寫法相去不遠,也較為直覺。

class Shape {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Circle extends Shape {
constructor(x, y, radius){
super(x, y);
this.radius = radius;
}
}
var shape = new Shape(1, 2);
var circle = new Circle(1, 2, 3);
console.log(shape);
console.log(circle);

不過在這之前,還是要先了解一下Javascript的prototype鏈的實作方式,Javascript原生並沒有實作繼承,實際上能達到繼承的效果主要是依靠prototype鏈,當要尋找一個屬性或函式時,會先從本身物件的屬性中找到是否滿足,再接著往__proto__ 中的物件裡尋找,若找不到再一直往上找,直到找到為止。從下面的例子中,descendant可以透過__proto__一直找到最上層的物件。但盡量使用 Object.getPrototypeOf取代__proto__ 的用法。

var user = {};
user.name = 'Anderson';
user.getName = function (){ console.log('name:' + this.name);}
user.getName();
// name: Anderson
var child = Object.create(user);
child.getName();
// name: Anderson
child.name = 'Baby';
child.getName();
// name: Baby
console.log(child.__proto__ === user);
// true
var descendant = Object.create(child);
descendant.getName();
// name: Baby
console.log(descendant.__proto__);
// { name: 'Baby' }
console.log(Object.getPrototypeOf(descendant));
// { name: 'Baby' }
console.log(descendant.__proto__ === Object.getPrototypeOf(descendant));
// true
console.log(descendant.__proto__.__proto__);
// { name: 'Anderson', getName: [Function] }

使用new User()或是Object.create(user)都可以產生一個物件,但前者會額外跑constructor函示。

Static Property

static的宣告主要有兩種方式,前者指向一個new出來的物件來將static的參數綁定,優點是所有呼叫該static屬性都會指到同一個記憶體位置。而後者則是使用static保留字,但每次都會new一個instance。

class Point { 
constructor(x, y) {
this.x = x;
this.y = y;
}
}
Point.ZERO = new Point(0, 0);
console.log(Point.ZERO);
// Point { x: 0, y: 0 }
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static get ZERO() {
return new Point(0, 0);
}
}
console.log(Point.ZERO);
// Point { x: 0, y: 0 }

Extends Property

class Animal{
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noice.');
}
}
class Dog extends Animal {
speak() {
console.log(this.name + ' barks');
}
}
var dog = new Dog("Anderson");
dog.speak();
// Anderson barks

如果再子類別中需要呼叫父類別的函示,可以使用super的方式來呼叫。或是super(…)的方式來呼叫父類別的constructor。

class Cat{
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ' makes a noice.');
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}
var lion = new Lion("Simba");
lion.speak();
// Simba makes a noice.
// Simba roars.

let and const

我們先來看下面的例子,因為var具有hoisting的特性,出現許多無法掌控的bug產生,使得var的值和我們預估的不同。

var x = 'global';
function f() {
console.log(x);
if (true) {
var x = 'local';
}
console.log(x);
}
f();
// undefined
// local

hoisting完實際執行變成如下,

var x = 'global';
function f() {
var x;
console.log(x);
if (true) {
x = 'local';
}
console.log(x);
}
f();
// undefined
// local

將var改為let,兩次印出的結果皆為global,而在if判斷式中的let則只在該區域內使用,local的值不會影響到外圍的x。

let x = 'global';
function f() {
console.log(x);
if (true) {
let x = 'local';
}
console.log(x);
}
f();
// global
// global

同理,換成const結果也是一樣

const x = 'global';
function f() {
console.log(x);
if (true) {
const x = 'local';
}
console.log(x);
}
f();
// global
// global

但如果我們把const的變數改值的話,就會產生錯誤,因為const的常數不允許被更動。

const x = 'global';
function f() {
if (true) {
const x = 'local';
}
x = "must error";
}
f();
// TypeError: Assignment to constant variable.

另外,let/const也有比var更多特性,在閉包(closure)中會自動綁定this。如果我們希望每隔一秒後依序印出1到5,如果我們用var,會hoisting到for的上面,所以實際 i 的值已經變為6每隔一秒才把i 的值印出。

function count1to5(){
for(var i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i);
}, i * 1000);
}
}
count1to5();
// 6 6 6 6 6

則會在記憶體中配置五個位置,並配置各自的閉包,讓我們預期的結果是正確的。

function count1to5(){
for(let i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i);
}, i * 1000);
}
}
count1to5();
// 1 2 3 4 5

雖然const理論上不可更改的,如下例子中,如果我們把註解那段移開,就會發生 TypeError,但我們如果只改這物件的屬性,執行卻是成功的。這就是弔詭的地方,所以使用const時千萬要小心。

const obj = {name: 'Anderson'};
// obj = {name: 'James'}; // TypeError: Assignment to constant variable
obj.name = 'AndersonJJ';
console.log(obj);

結論:

  1. 不會改變得物件使用const,會改變的就用let,不確定的話也用let,等到確定為唯讀時再改為const
  2. 不要再用var,太容易產生非預期的bug了。

String Interpolation

使用string interpolation取代原本只能+來做字串和變數串接的方式,那樣的缺點就是可讀性較差,有時還會忘記加了對應的空白。使用ES6除了可以明確的看出串接的字串,也可輕鬆的夾帶變數,使用上較為方便。

let customer = { name:"Anderson" };
let info = { type: "VIP", discount: 0.9 };
let message = `歡迎光臨 ${customer.name},
你是我們的${info.type}客戶,
您享有全館${info.discount * 10}折的優惠喔。`;
console.log(message);
\\ 歡迎光臨 Anderson,
\\ 你是我們的VIP客戶,
\\ 您享有全館9折的優惠喔。

Module

隨著前端業務複雜度的增加,模組化成為一個大的趨勢,不可能將所有功能都維護在同一隻檔案中,為了日後的可維護性,我們勢必會依照feature拆分模組。在還沒有ES6之前,主要是用CommonJS和AMD兩種解決方案,

  • CommonJS: NodeJS就是就是採用該方案,將需要用到的模組透過 require('xxx') 的方式使用模組,載入為同步。而模組則透過 modules.exports 的方式釋出給其他呼叫者使用。
  • AMD(Asynchronous Module Definition),使用到模組時才非同步的載入,不像CommonJS需要一開始就得載入。可以避免效能上的浪費,或是最佳化可用性。

而ES6使用import / export,前者為載入模組,而後者為輸出模組。export的對象可為變數或函示,也可使用大括號侷限只需使用到的子模組,例子如下,引用lib/math後面不需另外加上.js

// lib/math.js
export function suqare(x) { return x * x; }
export const pi = 3.14159;
// app1.js
import * as math from 'lib/math';
console.log(math.suqare(math.pi));
// app2.js
import { square, pi } from 'lib/math';
console.log(suqare(pi));

模組間可以再呼叫模組,經過處理後再提供新的函示並可暴露出給外界使用的方法。

// lib/math2.js
export * from 'lib/math';
export const e = 2.71828;
export default (x) => Math.exp(x);
// app.js
import exp, { pi } from 'lib/math2';
console.log(exp(pi));

Iterate Over Arrays

for 迴圈可以分別用in和of來取得陣列的索引和值,特別的是,ary.x既是陣列的宣告也同時是物件屬性的宣告方式,所以使用of 則物件不會顯示。

let ary = ['a', 'b', 'c'];
ary["4"] = "d";
ary.x = "e";
for(let v in ary){ console.log(v); } // 0 1 2 4 x
for(let v of ary){ console.log(v); } // a b c undefined d

Array Matching

兩陣列中的值能互相印射,簡化一些陣列間項目的操作和代換。

function* fib() {
let [prev, curr] = [0, 1];
while(true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for(let n of fib()){
if(n > 5) break;
console.log(n);
}

Object Matching

除了可以從模組中選用需使用的,物件也可同理套用。另外,以下為例,當屬性複製時,可以單獨寫name即可。

function getAccount() {
return {
name: 'Anderson',
sex: 'male',
age: '18'
}
}
const { name, age } = getAccount();
console.log(`Hi, ${name}`);
let account = { name, age };
console.log(account);
// Hi, Anderson
// { name: 'Anderson', age: '18' }

Spread Operator

再談展開運算子(spread operator)之前,JS物件複製如果沒經過特殊處理,通常都是call by reference,也就是你修改複製的物件時,同時也污染到原本的值,而在我們學到Redux時,有個很重要的觀念immutable,就是避免一再的污染到原始值。通常不經意的使用也會引發難以追訴的bug。所以看以下例子,combine的值初始不是先由空物件去覆蓋,而是直接覆蓋在obj1的物件上,當然也就污染到obj1的值。

var obj1 = { a: 1 };
var obj2 = { a: 2 };
var obj3 = { a: 3 };
var copy = Object.assign({}, obj1);
var combine = Object.assign(obj1, obj2, obj3);
console.log(copy);
console.log(combine);
console.log(obj1);
// { a: 1 }
// { a: 3 }
// { a: 3 }

以下例子中就使用其餘參數(Rest parameters)來避免函式需規範參數的數量,當然你會懷疑這和arguments有何不同,arguments其實並非真正的array,而Rest parameters是。

function f(x, y=1, z=2) {
return x + y + z;
}
console.log(f(1, 2));
function g(x, y, ...z) {
return (x + y) * z.length;
}
console.log(g(1, 2, 3, 4, 5));
const rest = [3, 4, 5];
console.log([1, 2, param]);
console.log([1, 2, ...param]);
console.log(g(1, 2, 3, ...param));
// 5
// 9
// [ 1, 2, [ 3, 4, 5 ] ]
// [ 1, 2, 3, 4, 5 ]
// 12

透過以下類似redux的寫法,我們就可以利用展開運算子傳回一個新的物件。

function getNewValue(state, action) {
return {
state,
...action.data
}
}

特別的是,該特性在目前比較新的瀏覽器和程式執行環境(Node.js等),都已經有支援了,但其餘參數(Rest parameters)的支援度沒有那麼足夠,仍須依靠babel的loader來幫忙處理。

Like what you read? Give Anderson a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.