初探 ES6(4)極簡的 Classes by fillano | CodeData
top

初探 ES6(4)極簡的 Classes

分享:

初探 ES6(3)Generator << 前情

ES6新增加了Classes語法的支援,不過對於Classes語法有所期望的人可能會失望…因為這是一個極其精簡的Classes語法(當初的提案叫做「maximally minimal classes」)。到底情況怎樣呢?

Classes in ES6

Javascript是一個物件導向的程式語言,基本上使用他的語法就可以實現封裝、繼承、多形等物件導向程式設計的特色。不過他的語法特性,往往會讓從Classical程式語言來的使用者困擾。例如我們要讓sub繼承base,在Javascript經典的做法是:

function base() {
    this.a = 'a';
}
base.prototype.getA = function() {return this.a};

function sub() {
    base.call(this);
    this.b = 'b';
}
sub.prototype = new base();

這樣做繼承,感覺有點饒舌…如果不想這樣使用prototype,那就需要在constructor函數中定義方法與屬性成員:

function base() {
    this.a = 'a';
    this.getA = function() {return this.a};
}

function sub() {
    base.call(this);
    this.b = 'b';
}

等等。除了new看起來有點像Classical程式語言下的使用方式,其他都很不一樣。

在ES6,新增了class語法,可以讓這個過程稍微簡單一點:

class base {
    constructor() {
        this.a = 'a';
    }
    getA() {return this.a}
}
class sub extends base {
    constructor() {
        super();
        this.b = 'b';
    }
}

比較舊的語法與ES6 Classes的語法,大致上可以看出:

  1. ES6 Classes的宣告,有稍微簡化一點
  2. 跟過去只定義constructor函數比起來,語意也比較明確
  3. 不過實際上跟舊的用法是一對一的,也可以說Classes是定義物件constructor的一個語法糖衣(sugar)

測試環境

在實際測試之前,需要注意:目前可以支援Class的測試環境,只有之前文章介紹過的Traceur Compiler。所以要執行這個範例,需要把他放在Traceur Compiler的環境中。例如:

<html>
<head>
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js"
    type="text/javascript"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
    type="text/javascript"></script>
<script>
traceur.options.experimental = true;
</script>
<script type="module">
class Base {
    constructor() {
        this.a = 'a';
    }
    getA() {return this.a}
}
class Sub extends base {
    constructor() {
        super();
        this.b = 'b';
    }
}
var a = new Base();
alert(a.a);
var b = new Sub();
alert(b.a+','+b.b);
</script>
</head>
<body>
</body>
</html>

 

ES6 Classes的語法

maximally minimal class,顧名思義,就是用最少的新增語法來支援classes。新增的東西真的很少,而且基本上只是一個定義constructor函數與prototype繼承的不同寫法而已。我猜這還是會對已經熟悉class的使用,而初次嘗試ES6 class的人帶來一些困擾。不過還是來看一下包含哪些語法上的更新

* class宣告及class表達式

class宣告:class [identifier] [extends 繼承表達式]opt {[class body]}

class表達式:class [identifier]opt [extends 繼承表達式]opt {[class body]}

如果有看過規格草案,會發現裡面的定義是,執行class宣告時,會在當前的context中產生跟identifier同名的constructor函數。所以基本上這跟過去Javascript的做法是相等的。至於宣告及表達式的差別,就跟函數的宣告與函數的表達式差不多。如果要把class指派給一個變數時,就可以使用class表達式。例如:

<html>
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js" 
    type="text/javascript"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
    type="text/javascript"></script>
<script>
traceur.options.experimental = true;
</script>
<script type="module">
var Base = class {
    constructor() {
        this.a = "I'm Base";
    }
}
var a = new Base();
console.log(a.a);
</script>
<body>
</body>
</html>

* class body

class body,是以class的「方法宣告」列表組成,不過跟物件實字不太一樣,只需要方法的identifier、參數、以及方法的body就可以組成列表中的元素。除了方法的宣告,列表元素可以包含分號「;」,不過這大概沒什麼實際作用。另外,也可以在方法宣告中使用在ES5新制定的getter / setter語法,間接定義成員屬性。

所以class body可以是:

  1. methodName (parameters…) {/*method body*/}
  2. generatorName * (parameters…) {/* generator function body*/}
  3. get propertyName () {/*return property*/}
  4. set propertyName (value) {/*set propery value*/}
  5. ;

 

* constructor與super

過去Javascript是直接使用函數來定義constructor,使用ES6的classes語法,則使用constructor這個特定的方法名稱來定義constructor。如果要在constructor中呼叫父類的constructor,可以透過呼叫super()函數來達成。

 

* static宣告

過去如果不透過prototype,直接用「.」加到constructor函數上的函數,就會變成一個「靜態」的方法,不會被繼承,只能透過consturctor名稱作為identifier來呼叫。同樣地,在classes語法中,可以為方法宣告加入static,達到一樣的效果。不過在ES6 classes規格中,除了static就沒有其他的scope宣告了(例如大家熟悉的private, public等等)。static方法必須透過class identifier來取用,透過實例是無法存取的。例如:

<html>
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js"
    type="text/javascript"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
    type="text/javascript"></script>
<script>
traceur.options.experimental = true;
</script>
<script type="module">
class Base {
    constructor() {
        this.a = '';
    }
    static sBase() {console.log('static base.sBase')}
}
class Sub extends Base {
    constructor() {
        super();
    }
    static sSub() {console.log('static sub.sSub')}
}
Base.sBase();
Sub.sSub();
Sub.sBase();
var a = new Sub();
a.sSub();
</script>
<body>
</body>
</html>

從traceur跑的這個例子可以看出來,用Sub的實例a來呼叫sSub()靜態方法時,就會出錯。在Chrome的Dev Console會看到「TypeError: undefined is not a function」的錯誤訊息。不過在traceur中,static方法可以被繼承…這好像不太對XD

* 繼承表達式(Inheritance Expression)

前面的例子已經示範過extends的使用。不過接在extends之後的是一個繼承表達式,並不只是一個reference。所以只要執行的結果是函數,都可以放在這裡。例如:

<html>
<head>
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js"
    type="text/javascript"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
    type="text/javascript"></script>
<script>
traceur.options.experimental = true;
</script>
<script type="module">
class A extends function(a, b) {
    this.a = a;
    this.b = b;
    this.avg = function() {return (this.a+this.b)/2;}
}
{
    constructor(a, b) {
        super(a, b);
    }
    sum() {return this.a+this.b;}
}
var a = new A(8, 6);
alert(a.avg());
alert(a.sum());
</script>
<body>
</body>
</html>

 

* 無法使用Classes宣告語法來定義Class的成員屬性

另外,在class宣告中,只能宣告成員方法或使用getter/setter來宣告成員屬性,這其實有一些不方便,如果不在constructor中建立成員屬性,那還是得用prototype。例如:

class Foo {}
Foo.prototype.Bar = '';

也因為這樣,雖然可以用getter/setter來宣告成員屬性,但是沒地方存…

class Foo {
    get Bar(){}//沒東西可以返回
    set Bar(v){}//沒地方存放
}

這時候,就只好走回Javascript的老路,利用scope來做出private變數:

<html>
<script src="https://traceur-compiler.googlecode.com/git/bin/traceur.js" 
type="text/javascript"></script>
<script src="https://traceur-compiler.googlecode.com/git/src/bootstrap.js"
type="text/javascript"></script>
<script>
traceur.options.experimental = true;
</script>
<script type="module">
function newFoo() {
    var _bar;
    return new (class {
        get Bar(){return _bar;}
        set Bar(v){_bar = v;}
    })();
}
var a = newFoo();
a.Bar = 'abc';
var b = newFoo();
b.Bar = 'def';
console.log(a.Bar);//印出abc
console.log(b.Bar);//印出def
</script>
<body>
</body>
</html>

這樣寫還是很煩人…而且不知道怎樣繼承XD

* 內建物件Subclassing

另外,雖然跟Classes的支援不一定直接相關,ES6對於內建的物件會有一個重大的修改,就是讓內建的物件支援subclassing。例如:

class MyArray extends Array {
    constructor(...args) {super(...args)}
    average() {//回傳陣列元素的平均值}
    middle() {//排序陣列元素後,回傳中位數}
}

不過目前的Javascript環境應該都還沒支援這個,所以還無法測試。(不過要支援這個,需要改很多東西…,有興趣的話,可以參考:Axel Rauschmayer的文章:Subclassing builtins in ECMAScript 6)。

總之

用幾句話來勾勒ES6 Classes的重點:

  1. Classes語法跟過去Javascript物件導向的做法,基本上是一對一的
  2. 只能宣告成員方法,無法宣告成員屬性,有點不方便
  3. 只有static、public,沒有private

不過至少在語法上,比過去利用constructor函數來說,是明確一些。

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

caterpillar05/19

基本上是讓過去模擬類別的作法有個標準方式 …

用 class 來模擬 private 的作法的話,這種封裝風格如何?

class Foo {
 constructor(bar) {
  this.__bar__ = bar;
  Object.defineProperty(this, '__bar__', {
   writable : true,
   enumerable : false
  });
 }
 get bar(){ return this.__bar__; }
 set bar(v){ this.__bar__ = v; }
}

從 Python 借過來的概念 … XD

熱門論壇文章

熱門技術文章