文章目錄
Class的基本語法
1.0 簡介
ES6之前, JavaScript 語言中, 生成實例對象的方法是 通過構造函數,
下面是一個例子:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2);
ES6 提供了更接近傳統語言的方法, 引入 Class(類)這個概念, 作爲對象的模板, 通過
class
關鍵字, 可以定義類.
基本上,ES6 的
class
可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class
寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用 ES6 的class
改寫,就是下面這樣。class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } }
上面代碼定義了一個“類”,可以看到裏面有一個
constructor
方法,這就是構造方法,而this
關鍵字則代表實例對象。也就是說,ES5 的構造函數Point
,對應 ES6 的Point
類的構造方法。
ES6的類, 完全可以看做構造函數的另一種寫法.
class Point { // ... } typeof Point // "function" Point === Point.prototype.constructor // true
1.1 constructor 方法
constructor
方法是類的默認方法,通過new
命令生成對象實例時,自動調用該方法。一個類必須有constructor
方法,如果沒有顯式定義,一個空的constructor
方法會被默認添加。class Point { } // 等同於 class Point { constructor() {} }
上面代碼中,定義了一個空的類
Point
,JavaScript 引擎會自動爲它添加一個空的constructor
方法。
constructor
方法默認返回實例對象(即this
),完全可以指定返回另外一個對象class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false
上面代碼中,
constructor
函數返回一個全新的對象,結果導致實例對象不是Foo
類的實例。
類必須使用
new
調用,否則會報錯。這是它跟普通構造函數的一個主要區別,後者不用new
也可以執行。class Foo { constructor() { return Object.create(null); } } Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'
1.2 類的實例
生成類的實例的寫法, 與ES5完全一樣, 也就是使用
new
命令.前面說過, 如果忘記加上new
, 像函數那樣調用Class
, 將會報錯.class Point { // ... } // 報錯 var point = Point(2, 3); // 正確 var point = new Point(2, 3);
與 ES5 一樣,實例的屬性除非顯式定義在其本身(即定義在
this
對象上),否則都是定義在原型上(即定義在class
上)。//定義類 class Point { constructor(x, y) { this.x = x; // 實例的屬性顯示定義在其本身即: this 上 this.y = y; // 實例的屬性顯示定義在其本身即: this 上 } toString() { // 沒有顯示定義在實例本身即 this上, 定義在了類 class上, 那麼這個方法就定義在了原型上 return '(' + this.x + ', ' + this.y + ')'; } } var point = new Point(2, 3); point.toString() // (2, 3) point.hasOwnProperty('x') // true point.hasOwnProperty('y') // true point.hasOwnProperty('toString') // false point.__proto__.hasOwnProperty('toString') // true console.log(point.__proto__ === Point.prototype); // true
與 ES5 一樣,類的所有實例共享一個原型對象。
1.3 取值函數(getter)和存值函數(setter)
與 ES5 一樣,在“類”的內部可以使用
get
和set
關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。class MyClass { get a() { // 取值 return 'getter'; } set a(value) { // 存值 console.log('setter: ' + value); } } let inst = new MyClass(); inst.a = 123; // 存值 setter: 123 console.log(inst.a); // 取值 getter
上面代碼中,
prop
屬性有對應的存值函數和取值函數,因此賦值和讀取行爲都被自定義了。
1.4 屬性表達式
類的屬性名, 可以採用表達式寫法表示
let methodName = 'getArea'; class Square { constructor(length) { // ... } [methodName]() { // ... } }
上面代碼中,
Square
類的方法名getArea
,是從表達式得到的。
1.5 Class 表達式
與函數一樣, 類也可以使用表達式的形式定義
const MyClass = class Me { getClassName() { return Me.name; } };
上面代碼使用表達式定義了一個類。需要注意的是,這個類的名字是
Me
,但是Me
只在 Class 的內部可用,指代當前類。在 Class 外部,這個類只能用MyClass
引用。let inst = new MyClass(); inst.getClassName() // Me Me.name // ReferenceError: Me is not defined
上面代碼表示,
Me
只在 Class 內部有定義。
如果類內部沒有用到的話, 也可以省略
Me
, 也就是可以寫成下面格式const MyClass = class {}
採用 Class 表達式,可以寫出立即執行的 Class。
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('張三'); person.sayName(); // "張三"
上面代碼中,
person
是一個立即執行的類的實例。
1.6 注意點
-
嚴格模式
類和模塊的內部,默認就是嚴格模式,所以不需要使用
use strict
指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴格模式可用。考慮到未來所有的代碼,其實都是運行在模塊之中,所以 ES6 實際上把整個語言升級到了嚴格模式。 -
不存在提升
類不存在變量提升(hoist),這一點與 ES5 完全不同。
new Foo(); // ReferenceError class Foo {}
上面代碼中,
Foo
類使用在前,定義在後,這樣會報錯,因爲 ES6 不會把類的聲明提升到代碼頭部。這種規定的原因與下文要提到的繼承有關,必須保證子類在父類之後定義。{ let Foo = class {}; class Bar extends Foo { } }
- name屬性
由於本質上,ES6 的類只是 ES5 的構造函數的一層包裝,所以函數的許多特性都被
Class
繼承,包括name
屬性。class Point {} Point.name // "Point"
name
屬性總是返回緊跟在class
關鍵字後面的類名。
- Generator 方法
如果某個方法之前加上星號(
*
),就表示該方法是一個 Generator 函數。class Foo { constructor(...args) { this.args = args; } * [Symbol.iterator]() { for (let arg of this.args) { yield arg; } } } for (let x of new Foo('hello', 'world')) { console.log(x); } // hello // world
- this的指向
類的方法內部如果含有
this
, 它默認指向類的實例, 但是必須非常小心, 一旦單獨使用該方法, 很可能會報錯class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const {print, printName} = logger; print('Gene123123') // Gene123123 printName('123123'); // TypeError: Cannot read property 'print' of undefined 第三行 this調用報錯
上面代碼中,
printName
方法中的this
,默認指向Logger
類的實例。但是,如果將這個printName
方法提取出來單獨使用,this
會指向該方法運行時所在的環境(由於 class 內部是嚴格模式,所以 this 實際指向的是undefined
),從而導致找不到
一個比較簡單的解決方法是,在構造方法中綁定
this
,這樣就不會找不到class Logger { constructor() { this.printName = this.printName.bind(this); } // ... }
另一種解決方法是使用箭頭函數。
lass Obj { constructor() { this.getThis = () => this; } } const myObj = new Obj(); myObj.getThis() === myObj // true
箭頭函數內部的
this
總是指向定義時所在的對象。上面代碼中,箭頭函數位於構造函數內部,它的定義生效的時候,是在構造函數執行的時候。這時,箭頭函數所在的運行環境,肯定是實例對象,所以this
會總是指向實例對象。還有一種解決方法是使用
Proxy
,獲取方法的時候,自動綁定this
。function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; } const logger = selfish(new Logger());
2.0 靜態方法
類相當於實例的yuanxing,所有在類中定義的方法, 都會被實例繼承, 如果在一個方法前, 加上
static
關鍵字, 就表示該方法不會被實例繼承, 而是直接通過類來調用, 這種方法稱爲 “靜態方法”.class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
注意: 如果靜態方法包含
this
關鍵字, 這個this
指的是類, 而不是實例.class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); } } Foo.bar() // hello
上面代碼中,靜態方法
bar
調用了this.baz
,這裏的this
指的是Foo
類,而不是Foo
的實例,等同於調用Foo.baz
。另外,從這個例子還可以看出,靜態方法可以與非靜態方法重名。
父類的靜態方法可以被子類繼承
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod() // 'hello'
上面代碼中,父類
Foo
有一個靜態方法,子類Bar
可以調用這個方法。靜態方法也是可以從
super
對象上調用的。class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"
3.0 實例屬性的最新寫法
實例屬性出來可以定義在
constructor()
方法裏面的this
上, 也可以定義在類的最頂層class IncreasingCounter { constructor() { this._count = 0; } get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
上面代碼中,實例屬性
this._count
定義在constructor()
方法裏面。另一種寫法是,這個屬性也可以定義在類的最頂層,其他都不變。class IncreasingCounter { _count = 0; get value() { console.log('Getting the current value!'); return this._count; } increment() { this._count++; } }
上面代碼中,實例屬性
_count
與取值函數value()
和increment()
方法,處於同一個層級。這時,不需要在實例屬性前面加上this
。這種新寫法的好處是,所有實例對象自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些實例屬性。
class foo { bar = 'hello'; baz = 'world'; constructor() { // ... } }
上面的代碼,一眼就能看出,
foo
類有兩個實例屬性,一目瞭然。另外,寫起來也比較簡潔。
4.0 靜態屬性
靜態屬性指的是 Class本身的屬性, 即
Class.propName
, 而不是定義在 實例對象(this
) 上的屬性class Foo { } Foo.prop = 1; Foo.prop // 1
上面的寫法爲
Foo
類定義了一個靜態屬性prop
。目前,只有這種寫法可行,因爲 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。現在有一個提案提供了類的靜態屬性,寫法是在實例屬性的前面,加上
static
關鍵字。class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 } }
這個新寫法大大方便了靜態屬性的表達。
// 老寫法 class Foo { // ... } Foo.prop = 1; // 新寫法 class Foo { static prop = 1; }
5.0 私有方法和私有屬性
-
現有的解決方案
私有方法和私有屬性, 是只能在類的內部訪問的方法和屬性, 外部不能訪問, 這是常見的需求, 有利於代碼的封裝, 但ES6不提供, 只能通過 變通方法模擬實現
-
一種方法是在命名上加以區別
class Widget { // 公有方法 foo (baz) { this._bar(baz); } // 私有方法 _bar(baz) { return this.snaf = baz; } // ... }
上面代碼中,
_bar
方法前面的下劃線,表示這是一個只限於內部使用的私有方法。但是,這種命名是不保險的,在類的外部,還是可以調用到這個方法 -
另一種方法是索性將私有方法移出模塊, 因爲模塊內部所有方法對外可見的
class Widget { foo (baz) { bar.call(this, baz); } // ... } function bar(baz) { return this.snaf = baz; }
上面代碼中,
foo
是公開方法,內部調用了bar.call(this, baz)
。這使得bar
實際上成爲了當前模塊的私有方法。 -
-
私有屬性的提案
目前,有一個提案,爲
class
加了私有屬性。方法是在屬性名之前,使用#
表示。class IncreasingCounter { #count = 0; get value() { console.log('Getting the current value!'); return this.#count; } increment() { this.#count++; } }
6.0 new.target屬性
new
是從構造函數生成實例對象的命令。ES6 爲new
命令引入了一個new.target
屬性,該屬性一般用在構造函數之中,返回new
命令作用於的那個構造函數。如果構造函數不是通過new
命令或Reflect.construct()
調用的,new.target
會返回undefined
,因此這個屬性可以用來確定構造函數是怎麼調用的。function Person(name) { if (new.target !== undefined) { this.name = name; } else { throw new Error('必須使用 new 命令生成實例'); } } // 另一種寫法 function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('必須使用 new 命令生成實例'); } } var person = new Person('張三'); // 正確 var notAPerson = Person.call(person, '張三'); // 報錯
上面代碼確保構造函數只能通過
new
命令調用。
Class 內部調用
new.target
,返回當前 Class。class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); this.length = length; this.width = width; } } var obj = new Rectangle(3, 4); // 輸出 true
總結:
交流學習添加微信(備註技術交流學習):
Gene199302
該博客爲學習阮一峯 ES6入門課所做的筆記記錄, 僅用來留作筆記記錄和學習理解