ES6 Class 的基本語法

2020-06-11 14:42 更新

1. 簡介

類的由來

JavaScript 語言中,生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù)。下面是一個例子。

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return '(' + this.x + ', ' + this.y + ')';
  7. };
  8. var p = new Point(1, 2);

上面這種寫法跟傳統(tǒng)的面向?qū)ο笳Z言(比如 C++ 和 Java)差異很大,很容易讓新學(xué)習(xí)這門語言的程序員感到困惑。

ES6 提供了更接近傳統(tǒng)語言的寫法,引入了 Class(類)這個概念,作為對象的模板。通過 class 關(guān)鍵字,可以定義類。

基本上,ES6 的 class 可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的 class 寫法只是讓對象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已。上面的代碼用 ES6 的 class 改寫,就是下面這樣。

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. toString() {
  7. return '(' + this.x + ', ' + this.y + ')';
  8. }
  9. }

上面代碼定義了一個“類”,可以看到里面有一個 constructor 方法,這就是構(gòu)造方法,而 this 關(guān)鍵字則代表實例對象。也就是說,ES5 的構(gòu)造函數(shù) Point ,對應(yīng) ES6 的 Point 類的構(gòu)造方法。

Point 類除了構(gòu)造方法,還定義了一個 toString 方法。注意,定義“類”的方法的時候,前面不需要加上 function 這個關(guān)鍵字,直接把函數(shù)定義放進去了就可以了。另外,方法之間不需要逗號分隔,加了會報錯。

ES6 的類,完全可以看作構(gòu)造函數(shù)的另一種寫法。

  1. class Point {
  2. // ...
  3. }
  4. typeof Point // "function"
  5. Point === Point.prototype.constructor // true

上面代碼表明,類的數(shù)據(jù)類型就是函數(shù),類本身就指向構(gòu)造函數(shù)。

使用的時候,也是直接對類使用 new 命令,跟構(gòu)造函數(shù)的用法完全一致。

  1. class Bar {
  2. doStuff() {
  3. console.log('stuff');
  4. }
  5. }
  6. var b = new Bar();
  7. b.doStuff() // "stuff"

構(gòu)造函數(shù)的 prototype 屬性,在 ES6 的“類”上面繼續(xù)存在。事實上,類的所有方法都定義在類的 prototype 屬性上面。

  1. class Point {
  2. constructor() {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. toValue() {
  9. // ...
  10. }
  11. }
  12. // 等同于
  13. Point.prototype = {
  14. constructor() {},
  15. toString() {},
  16. toValue() {},
  17. };

在類的實例上面調(diào)用方法,其實就是調(diào)用原型上的方法。

  1. class B {}
  2. let b = new B();
  3. b.constructor === B.prototype.constructor // true

上面代碼中, b 是 B 類的實例,它的 constructor 方法就是 B 類原型的 constructor 方法。

由于類的方法都定義在 prototype 對象上面,所以類的新方法可以添加在 prototype 對象上面。 Object.assign 方法可以很方便地一次向類添加多個方法。

  1. class Point {
  2. constructor(){
  3. // ...
  4. }
  5. }
  6. Object.assign(Point.prototype, {
  7. toString(){},
  8. toValue(){}
  9. });

prototype 對象的 constructor 屬性,直接指向“類”的本身,這與 ES5 的行為是一致的。

  1. Point.prototype.constructor === Point // true

另外,類的內(nèi)部所有定義的方法,都是不可枚舉的(non-enumerable)。

  1. class Point {
  2. constructor(x, y) {
  3. // ...
  4. }
  5. toString() {
  6. // ...
  7. }
  8. }
  9. Object.keys(Point.prototype)
  10. // []
  11. Object.getOwnPropertyNames(Point.prototype)
  12. // ["constructor","toString"]

上面代碼中, toString 方法是 Point 類內(nèi)部定義的方法,它是不可枚舉的。這一點與 ES5 的行為不一致。

  1. var Point = function (x, y) {
  2. // ...
  3. };
  4. Point.prototype.toString = function() {
  5. // ...
  6. };
  7. Object.keys(Point.prototype)
  8. // ["toString"]
  9. Object.getOwnPropertyNames(Point.prototype)
  10. // ["constructor","toString"]

上面代碼采用 ES5 的寫法, toString 方法就是可枚舉的。

constructor 方法

constructor 方法是的默認(rèn)方法,通過 new 命令生成對象實例時,自動調(diào)用該方法。一個類必須有 constructor 方法,如果沒有顯式定義,一個空的 constructor 方法會被默認(rèn)添加。

  1. class Point {
  2. }
  3. // 等同于
  4. class Point {
  5. constructor() {}
  6. }

上面代碼中,定義了一個空的類 Point ,JavaScript 引擎會自動為它添加一個空的 constructor 方法。

constructor 方法默認(rèn)返回實例對象(即 this ),完全可以指定返回另外一個對象。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. new Foo() instanceof Foo
  7. // false

上面代碼中, constructor 函數(shù)返回一個全新的對象,結(jié)果導(dǎo)致實例對象不是 Foo 類的實例。

類必須使用 new 調(diào)用,否則會報錯。這是它跟普通構(gòu)造函數(shù)的一個主要區(qū)別,后者不用 new 也可以執(zhí)行。

  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. Foo()
  7. // TypeError: Class constructor Foo cannot be invoked without 'new'

類的實例

生成類的實例的寫法,與 ES5 完全一樣,也是使用 new 命令。前面說過,如果忘記加上 new ,像函數(shù)那樣調(diào)用 Class ,將會報錯。

  1. class Point {
  2. // ...
  3. }
  4. // 報錯
  5. var point = Point(2, 3);
  6. // 正確
  7. var point = new Point(2, 3);

與 ES5 一樣,實例的屬性除非顯式定義在其本身(即定義在 this 對象上),否則都是定義在原型上(即定義在 class 上)。

  1. //定義類
  2. class Point {
  3. constructor(x, y) {
  4. this.x = x;
  5. this.y = y;
  6. }
  7. toString() {
  8. return '(' + this.x + ', ' + this.y + ')';
  9. }
  10. }
  11. var point = new Point(2, 3);
  12. point.toString() // (2, 3)
  13. point.hasOwnProperty('x') // true
  14. point.hasOwnProperty('y') // true
  15. point.hasOwnProperty('toString') // false
  16. point.__proto__.hasOwnProperty('toString') // true

上面代碼中, x 和 y 都是實例對象 point 自身的屬性(因為定義在 this 變量上),所以 hasOwnProperty 方法返回 true ,而 toString 是原型對象的屬性(因為定義在 Point 類上),所以 hasOwnProperty 方法返回 false 。這些都與 ES5 的行為保持一致。

與 ES5 一樣,類的所有實例共享一個原型對象。

  1. var p1 = new Point(2,3);
  2. var p2 = new Point(3,2);
  3. p1.__proto__ === p2.__proto__
  4. //true

上面代碼中, p1 和 p2 都是 Point 的實例,它們的原型都是 Point.prototype ,所以 proto 屬性是相等的。

這也意味著,可以通過實例的 proto 屬性為“類”添加方法。

proto 并不是語言本身的特性,這是各大廠商具體實現(xiàn)時添加的私有屬性,雖然目前很多現(xiàn)代瀏覽器的 JS 引擎中都提供了這個私有屬性,但依舊不建議在生產(chǎn)中使用該屬性,避免對環(huán)境產(chǎn)生依賴。生產(chǎn)環(huán)境中,我們可以使用 Object.getPrototypeOf 方法來獲取實例對象的原型,然后再來為原型添加方法/屬性。

  1. var p1 = new Point(2,3);
  2. var p2 = new Point(3,2);
  3. p1.__proto__.printName = function () { return 'Oops' };
  4. p1.printName() // "Oops"
  5. p2.printName() // "Oops"
  6. var p3 = new Point(4,2);
  7. p3.printName() // "Oops"

上面代碼在 p1 的原型上添加了一個 printName 方法,由于 p1 的原型就是 p2 的原型,因此 p2 也可以調(diào)用這個方法。而且,此后新建的實例 p3 也可以調(diào)用這個方法。這意味著,使用實例的 proto 屬性改寫原型,必須相當(dāng)謹(jǐn)慎,不推薦使用,因為這會改變“類”的原始定義,影響到所有實例。

取值函數(shù)(getter)和存值函數(shù)(setter)

與 ES5 一樣,在“類”的內(nèi)部可以使用 getset關(guān)鍵字,對某個屬性設(shè)置存值函數(shù)取值函數(shù),攔截該屬性的存取行為。

  1. class MyClass {
  2. constructor() {
  3. // ...
  4. }
  5. get prop() {
  6. return 'getter';
  7. }
  8. set prop(value) {
  9. console.log('setter: '+value);
  10. }
  11. }
  12. let inst = new MyClass();
  13. inst.prop = 123;
  14. // setter: 123
  15. inst.prop
  16. // 'getter'

上面代碼中, prop 屬性有對應(yīng)的存值函數(shù)和取值函數(shù),因此賦值和讀取行為都被自定義了。

存值函數(shù)和取值函數(shù)是設(shè)置在屬性的 Descriptor 對象上的。

  1. class CustomHTMLElement {
  2. constructor(element) {
  3. this.element = element;
  4. }
  5. get html() {
  6. return this.element.innerHTML;
  7. }
  8. set html(value) {
  9. this.element.innerHTML = value;
  10. }
  11. }
  12. var descriptor = Object.getOwnPropertyDescriptor(
  13. CustomHTMLElement.prototype, "html"
  14. );
  15. "get" in descriptor // true
  16. "set" in descriptor // true

上面代碼中,存值函數(shù)和取值函數(shù)是定義在 html 屬性的描述對象上面,這與 ES5 完全一致。

屬性表達(dá)式

類的屬性名,可以采用表達(dá)式。

  1. let methodName = 'getArea';
  2. class Square {
  3. constructor(length) {
  4. // ...
  5. }
  6. [methodName]() {
  7. // ...
  8. }
  9. }

上面代碼中, Square 類的方法名 getArea ,是從表達(dá)式得到的。

Class 表達(dá)式

與函數(shù)一樣,類也可以使用表達(dá)式的形式定義。

  1. const MyClass = class Me {
  2. getClassName() {
  3. return Me.name;
  4. }
  5. };

上面代碼使用表達(dá)式定義了一個類。需要注意的是,這個類的名字是 Me ,但是 Me 只在 Class 的內(nèi)部可用,指代當(dāng)前類。在 Class 外部,這個類只能用 MyClass 引用。

  1. let inst = new MyClass();
  2. inst.getClassName() // Me
  3. Me.name // ReferenceError: Me is not defined

上面代碼表示, Me 只在 Class 內(nèi)部有定義。

如果類的內(nèi)部沒用到的話,可以省略 Me ,也就是可以寫成下面的形式。

  1. const MyClass = class { /* ... */ };

采用 Class 表達(dá)式,可以寫出立即執(zhí)行的 Class。

  1. let person = new class {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. sayName() {
  6. console.log(this.name);
  7. }
  8. }('張三');
  9. person.sayName(); // "張三"

上面代碼中, person 是一個立即執(zhí)行的類的實例。

注意點

(1)嚴(yán)格模式

類和模塊的內(nèi)部,默認(rèn)就是嚴(yán)格模式,所以不需要使用 use strict 指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴(yán)格模式可用??紤]到未來所有的代碼,其實都是運行在模塊之中,所以 ES6 實際上把整個語言升級到了嚴(yán)格模式。

(2)不存在提升

不存在變量提升(hoist),這一點與 ES5 完全不同。

  1. new Foo(); // ReferenceError
  2. class Foo {}

上面代碼中, Foo 類使用在前,定義在后,這樣會報錯,因為 ES6 不會把類的聲明提升到代碼頭部。這種規(guī)定的原因與下文要提到的繼承有關(guān),必須保證子類在父類之后定義。

  1. {
  2. let Foo = class {};
  3. class Bar extends Foo {
  4. }
  5. }

上面的代碼不會報錯,因為 Bar 繼承 Foo 的時候, Foo 已經(jīng)有定義了。但是,如果存在 class 的提升,上面代碼就會報錯,因為 class 會被提升到代碼頭部,而 let 命令是不提升的,所以導(dǎo)致 Bar 繼承 Foo 的時候, Foo 還沒有定義。

(3)name 屬性

由于本質(zhì)上,ES6 的類只是 ES5 的構(gòu)造函數(shù)的一層包裝,所以函數(shù)的許多特性都被Class繼承,包括 name 屬性。

  1. class Point {}
  2. Point.name // "Point"

name 屬性總是返回緊跟在 class 關(guān)鍵字后面的類名。

(4)Generator 方法

如果某個方法之前加上星號( * ),就表示該方法是一個 Generator 函數(shù)。

  1. class Foo {
  2. constructor(...args) {
  3. this.args = args;
  4. }
  5. * [Symbol.iterator]() {
  6. for (let arg of this.args) {
  7. yield arg;
  8. }
  9. }
  10. }
  11. for (let x of new Foo('hello', 'world')) {
  12. console.log(x);
  13. }
  14. // hello
  15. // world

上面代碼中, Foo 類的 Symbol.iterator 方法前有一個星號,表示該方法是一個 Generator 函數(shù)。 Symbol.iterator 方法返回一個 Foo 類的默認(rèn)遍歷器, for...of 循環(huán)會自動調(diào)用這個遍歷器。

(5)this 的指向

類的方法內(nèi)部如果含有 this ,它默認(rèn)指向類的實例。但是,必須非常小心,一旦單獨使用該方法,很可能報錯。

  1. class Logger {
  2. printName(name = 'there') {
  3. this.print(`Hello ${name}`);
  4. }
  5. print(text) {
  6. console.log(text);
  7. }
  8. }
  9. const logger = new Logger();
  10. const { printName } = logger;
  11. printName(); // TypeError: Cannot read property 'print' of undefined

上面代碼中, printName 方法中的 this ,默認(rèn)指向 Logger 類的實例。但是,如果將這個方法提取出來單獨使用, this 會指向該方法運行時所在的環(huán)境(由于 class 內(nèi)部是嚴(yán)格模式,所以 this 實際指向的是 undefined ),從而導(dǎo)致找不到 print 方法而報錯。

一個比較簡單的解決方法是,在構(gòu)造方法中綁定 this ,這樣就不會找不到 print 方法了。

  1. class Logger {
  2. constructor() {
  3. this.printName = this.printName.bind(this);
  4. }
  5. // ...
  6. }

另一種解決方法是使用箭頭函數(shù)。

  1. class Obj {
  2. constructor() {
  3. this.getThis = () => this;
  4. }
  5. }
  6. const myObj = new Obj();
  7. myObj.getThis() === myObj // true

箭頭函數(shù)內(nèi)部的 this 總是指向定義時所在的對象。上面代碼中,箭頭函數(shù)位于構(gòu)造函數(shù)內(nèi)部,它的定義生效的時候,是在構(gòu)造函數(shù)執(zhí)行的時候。這時,箭頭函數(shù)所在的運行環(huán)境,肯定是實例對象,所以 this 會總是指向?qū)嵗龑ο蟆?/p>

還有一種解決方法是使用 Proxy ,獲取方法的時候,自動綁定 this 。

  1. function selfish (target) {
  2. const cache = new WeakMap();
  3. const handler = {
  4. get (target, key) {
  5. const value = Reflect.get(target, key);
  6. if (typeof value !== 'function') {
  7. return value;
  8. }
  9. if (!cache.has(value)) {
  10. cache.set(value, value.bind(target));
  11. }
  12. return cache.get(value);
  13. }
  14. };
  15. const proxy = new Proxy(target, handler);
  16. return proxy;
  17. }
  18. const logger = selfish(new Logger());

2. 靜態(tài)方法

類相當(dāng)于實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上 static 關(guān)鍵字,就表示該方法不會被實例繼承,而是直接通過類來調(diào)用,這就稱為“靜態(tài)方法”。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. Foo.classMethod() // 'hello'
  7. var foo = new Foo();
  8. foo.classMethod()
  9. // TypeError: foo.classMethod is not a function

上面代碼中, Foo 類的 classMethod 方法前有 static 關(guān)鍵字,表明該方法是一個靜態(tài)方法,可以直接在 Foo 類上調(diào)用( Foo.classMethod() ),而不是在 Foo 類的實例上調(diào)用。如果在實例上調(diào)用靜態(tài)方法,會拋出一個錯誤,表示不存在該方法。

注意,如果靜態(tài)方法包含 this 關(guān)鍵字,這個 this 指的是類,而不是實例。

  1. class Foo {
  2. static bar() {
  3. this.baz();
  4. }
  5. static baz() {
  6. console.log('hello');
  7. }
  8. baz() {
  9. console.log('world');
  10. }
  11. }
  12. Foo.bar() // hello

上面代碼中,靜態(tài)方法 bar 調(diào)用了 this.baz ,這里的 this 指的是 Foo 類,而不是 Foo 的實例,等同于調(diào)用 Foo.baz 。另外,從這個例子還可以看出,靜態(tài)方法可以與非靜態(tài)方法重名。

父類的靜態(tài)方法,可以被子類繼承。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. }
  8. Bar.classMethod() // 'hello'

上面代碼中,父類 Foo 有一個靜態(tài)方法,子類 Bar 可以調(diào)用這個方法。

靜態(tài)方法也是可以從 super 對象上調(diào)用的。

  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. static classMethod() {
  8. return super.classMethod() + ', too';
  9. }
  10. }
  11. Bar.classMethod() // "hello, too"

3. 實例屬性的新寫法

實例屬性除了定義在constructor()方法里面的 this 上面,也可以定義在的最頂層。

  1. class IncreasingCounter {
  2. constructor() {
  3. this._count = 0;
  4. }
  5. get value() {
  6. console.log('Getting the current value!');
  7. return this._count;
  8. }
  9. increment() {
  10. this._count++;
  11. }
  12. }

上面代碼中,實例屬性 this._count 定義在 constructor() 方法里面。另一種寫法是,這個屬性也可以定義在類的最頂層,其他都不變。

  1. class IncreasingCounter {
  2. _count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this._count;
  6. }
  7. increment() {
  8. this._count++;
  9. }
  10. }

上面代碼中,實例屬性 _count 與取值函數(shù) value() 和 increment() 方法,處于同一個層級。這時,不需要在實例屬性前面加上 this 。

這種新寫法的好處是,所有實例對象自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些實例屬性。

  1. class foo {
  2. bar = 'hello';
  3. baz = 'world';
  4. constructor() {
  5. // ...
  6. }
  7. }

上面的代碼,一眼就能看出, foo 類有兩個實例屬性,一目了然。另外,寫起來也比較簡潔。

4. 靜態(tài)屬性

靜態(tài)屬性指的是Class本身的屬性,即 Class.propName,而不是定義在實例對象( this )上的屬性。

  1. class Foo {
  2. }
  3. Foo.prop = 1;
  4. Foo.prop // 1

上面的寫法為 Foo 類定義了一個靜態(tài)屬性 prop 。

目前,只有這種寫法可行,因為 ES6 明確規(guī)定,Class內(nèi)部只有靜態(tài)方法,沒有靜態(tài)屬性?,F(xiàn)在有一個提案提供了類的靜態(tài)屬性,寫法是在實例屬性的前面,加上 static 關(guān)鍵字。

  1. class MyClass {
  2. static myStaticProp = 42;
  3. constructor() {
  4. console.log(MyClass.myStaticProp); // 42
  5. }
  6. }

這個新寫法大大方便了靜態(tài)屬性的表達(dá)。

  1. // 老寫法
  2. class Foo {
  3. // ...
  4. }
  5. Foo.prop = 1;
  6. // 新寫法
  7. class Foo {
  8. static prop = 1;
  9. }

上面代碼中,老寫法的靜態(tài)屬性定義在類的外部。整個類生成以后,再生成靜態(tài)屬性。這樣讓人很容易忽略這個靜態(tài)屬性,也不符合相關(guān)代碼應(yīng)該放在一起的代碼組織原則。另外,新寫法是顯式聲明(declarative),而不是賦值處理,語義更好。

5. 私有方法和私有屬性

現(xiàn)有的解決方案

私有方法和私有屬性,是只能在類的內(nèi)部訪問的方法和屬性,外部不能訪問。這是常見需求,有利于代碼的封裝,但 ES6 不提供,只能通過變通方法模擬實現(xiàn)。

一種做法是在命名上加以區(qū)別。

  1. class Widget {
  2. // 公有方法
  3. foo (baz) {
  4. this._bar(baz);
  5. }
  6. // 私有方法
  7. _bar(baz) {
  8. return this.snaf = baz;
  9. }
  10. // ...
  11. }

上面代碼中, _bar 方法前面的下劃線,表示這是一個只限于內(nèi)部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調(diào)用到這個方法。

另一種方法就是索性將私有方法移出模塊,因為模塊內(nèi)部的所有方法都是對外可見的。

  1. class Widget {
  2. foo (baz) {
  3. bar.call(this, baz);
  4. }
  5. // ...
  6. }
  7. function bar(baz) {
  8. return this.snaf = baz;
  9. }

上面代碼中, foo 是公開方法,內(nèi)部調(diào)用了 bar.call(this, baz) 。這使得 bar 實際上成為了當(dāng)前模塊的私有方法。

還有一種方法是利用Symbol值的唯一性,將私有方法的名字命名為一個 Symbol 值。

  1. const bar = Symbol('bar');
  2. const snaf = Symbol('snaf');
  3. export default class myClass{
  4. // 公有方法
  5. foo(baz) {
  6. this[bar](baz);
  7. }
  8. // 私有方法
  9. [bar](baz) {
  10. return this[snaf] = baz;
  11. }
  12. // ...
  13. };

上面代碼中, bar 和 snaf 都是 Symbol 值,一般情況下無法獲取到它們,因此達(dá)到了私有方法和私有屬性的效果。但是也不是絕對不行, Reflect.ownKeys() 依然可以拿到它們。

  1. const inst = new myClass();
  2. Reflect.ownKeys(myClass.prototype)
  3. // [ 'constructor', 'foo', Symbol(bar) ]

上面代碼中,Symbol 值的屬性名依然可以從類的外部拿到。

私有屬性的提案

目前,有一個提案,為 class 加了私有屬性。方法是在屬性名之前,使用 # 表示。

  1. class IncreasingCounter {
  2. #count = 0;
  3. get value() {
  4. console.log('Getting the current value!');
  5. return this.#count;
  6. }
  7. increment() {
  8. this.#count++;
  9. }
  10. }

上面代碼中, #count 就是私有屬性,只能在類的內(nèi)部使用( this.#count )。如果在類的外部使用,就會報錯。

  1. const counter = new IncreasingCounter();
  2. counter.#count // 報錯
  3. counter.#count = 42 // 報錯

上面代碼在類的外部,讀取私有屬性,就會報錯。

下面是另一個例子。

  1. class Point {
  2. #x;
  3. constructor(x = 0) {
  4. this.#x = +x;
  5. }
  6. get x() {
  7. return this.#x;
  8. }
  9. set x(value) {
  10. this.#x = +value;
  11. }
  12. }

上面代碼中, #x 就是私有屬性,在 Point 類之外是讀取不到這個屬性的。由于井號 # 是屬性名的一部分,使用時必須帶有 # 一起使用,所以 #x 和 x 是兩個不同的屬性。

之所以要引入一個新的前綴 # 表示私有屬性,而沒有采用 private 關(guān)鍵字,是因為 JavaScript 是一門動態(tài)語言,沒有類型聲明,使用獨立的符號似乎是唯一的比較方便可靠的方法,能夠準(zhǔn)確地區(qū)分一種屬性是否為私有屬性。另外,Ruby 語言使用 @ 表示私有屬性,ES6 沒有用這個符號而使用 # ,是因為 @ 已經(jīng)被留給了 Decorator。

這種寫法不僅可以寫私有屬性,還可以用來寫私有方法。

  1. class Foo {
  2. #a;
  3. #b;
  4. constructor(a, b) {
  5. this.#a = a;
  6. this.#b = b;
  7. }
  8. #sum() {
  9. return #a + #b;
  10. }
  11. printSum() {
  12. console.log(this.#sum());
  13. }
  14. }

上面代碼中, #sum() 就是一個私有方法。

另外,私有屬性也可以設(shè)置 getter 和 setter 方法。

  1. class Counter {
  2. #xValue = 0;
  3. constructor() {
  4. super();
  5. // ...
  6. }
  7. get #x() { return #xValue; }
  8. set #x(value) {
  9. this.#xValue = value;
  10. }
  11. }

上面代碼中, #x 是一個私有屬性,它的讀寫都通過 get #x() 和 set #x() 來完成。

私有屬性不限于從 this 引用,只要是在類的內(nèi)部,實例也可以引用私有屬性。

  1. class Foo {
  2. #privateValue = 42;
  3. static getPrivateValue(foo) {
  4. return foo.#privateValue;
  5. }
  6. }
  7. Foo.getPrivateValue(new Foo()); // 42

上面代碼允許從實例 foo 上面引用私有屬性。

私有屬性和私有方法前面,也可以加上 static 關(guān)鍵字,表示這是一個靜態(tài)的私有屬性或私有方法。

  1. class FakeMath {
  2. static PI = 22 / 7;
  3. static #totallyRandomNumber = 4;
  4. static #computeRandomNumber() {
  5. return FakeMath.#totallyRandomNumber;
  6. }
  7. static random() {
  8. console.log('I heard you like random numbers…')
  9. return FakeMath.#computeRandomNumber();
  10. }
  11. }
  12. FakeMath.PI // 3.142857142857143
  13. FakeMath.random()
  14. // I heard you like random numbers…
  15. // 4
  16. FakeMath.#totallyRandomNumber // 報錯
  17. FakeMath.#computeRandomNumber() // 報錯

上面代碼中, #totallyRandomNumber 是私有屬性, #computeRandomNumber() 是私有方法,只能在 FakeMath 這個類的內(nèi)部調(diào)用,外部調(diào)用就會報錯。

6. new.target 屬性

new是從構(gòu)造函數(shù)生成實例對象的命令。ES6 為 new 命令引入了一個 new.target 屬性,該屬性一般用在構(gòu)造函數(shù)之中,返回 new 命令作用于的那個構(gòu)造函數(shù)。如果構(gòu)造函數(shù)不是通過 new 命令或 Reflect.construct() 調(diào)用的, new.target 會返回 undefined ,因此這個屬性可以用來確定構(gòu)造函數(shù)是怎么調(diào)用的。

  1. function Person(name) {
  2. if (new.target !== undefined) {
  3. this.name = name;
  4. } else {
  5. throw new Error('必須使用 new 命令生成實例');
  6. }
  7. }
  8. // 另一種寫法
  9. function Person(name) {
  10. if (new.target === Person) {
  11. this.name = name;
  12. } else {
  13. throw new Error('必須使用 new 命令生成實例');
  14. }
  15. }
  16. var person = new Person('張三'); // 正確
  17. var notAPerson = Person.call(person, '張三'); // 報錯

上面代碼確保構(gòu)造函數(shù)只能通過 new 命令調(diào)用。

Class 內(nèi)部調(diào)用 new.target ,返回當(dāng)前 Class。

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. this.length = length;
  5. this.width = width;
  6. }
  7. }
  8. var obj = new Rectangle(3, 4); // 輸出 true

需要注意的是,子類繼承父類時, new.target 會返回子類。

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. // ...
  5. }
  6. }
  7. class Square extends Rectangle {
  8. constructor(length, width) {
  9. super(length, width);
  10. }
  11. }
  12. var obj = new Square(3); // 輸出 false

上面代碼中, new.target 會返回子類。

利用這個特點,可以寫出不能獨立使用、必須繼承后才能使用的類。

  1. class Shape {
  2. constructor() {
  3. if (new.target === Shape) {
  4. throw new Error('本類不能實例化');
  5. }
  6. }
  7. }
  8. class Rectangle extends Shape {
  9. constructor(length, width) {
  10. super();
  11. // ...
  12. }
  13. }
  14. var x = new Shape(); // 報錯
  15. var y = new Rectangle(3, 4); // 正確

上面代碼中, Shape 類不能被實例化,只能用于繼承。

注意,在函數(shù)外部,使用 new.target 會報錯。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號