Class 的基本語法
ECMAScript 的原生構造函數(shù)大致有下面這些:
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES6 的 class 可以看作只是一個語法糖亡驰,它的絕大部分功能爹脾,ES5 都可以做到垒在,新的 class 寫法只是讓對象原型的寫法更加清晰饲趋、更像面向對象編程的語法而已矫渔。
class Point {
// 實例屬性也可以定義在類的最頂層
x = 0;
y = 0;
// 一個類必須有 constructor 方法,如果沒有顯式定義秆吵,一個空的 constructor 方法會被默認添加
constructor(x, y) {
this.x = x;
this.y = y;
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: ' + value);
}
// 類的屬性名可以采用表達式
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// 等同于
// Point.prototype = {
// constructor() {},
// toString() {},
// };
// 類必須使用 new 調用
var point = new Point(2, 3);
point.prop = 123; // setter: 123
point.prop; // 'getter'
point.toString(); // (2,3)
// ES6 的類调煎,完全可以看作構造函數(shù)的另一種寫法
Point === Point.prototype.constructor // true
// 在類的實例上面調用方法,其實就是調用原型上的方法
point.constructor === Point.prototype.constructor // true
// prototype 對象的 constructor 屬性行您,直接指向“類”的本身铭乾,與 ES5 是一致的
Point.prototype.constructor === Point // true
// 類的內部所有定義的方法,都是不可枚舉的娃循,與 ES5 不一致
Object.keys(Point.prototype) // []
Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString"]
// 實例的屬性除非顯式定義在其本身(this對象上)炕檩,否則都是定義在原型上(class上)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
// 類的所有實例共享一個原型對象
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__ // true
// 存值函數(shù)和取值函數(shù)是設置在屬性的 Descriptor 對象上的
var descriptor = Object.getOwnPropertyDescriptor(
Point.prototype, "prop"
);
"get" in descriptor // true
"set" in descriptor // true
類的所有實例共享一個原型對象,這也意味著,可以通過實例的 __proto__
屬性為“類”添加方法笛质。__proto__
并不是語言本身的特性泉沾,這是各大廠商具體實現(xiàn)時添加的私有屬性,雖然目前很多現(xiàn)代瀏覽器的 JS 引擎中都提供了這個私有屬性妇押,但依舊不建議在生產中使用該屬性跷究,避免對環(huán)境產生依賴。使用實例的 __proto__
屬性改寫原型敲霍,必須相當謹慎俊马,不推薦使用,因為這會改變“類”的原始定義肩杈,影響到所有實例柴我。生產環(huán)境中,我們可以使用 Object.getPrototypeOf
方法來獲取實例對象的原型扩然,然后再來為原型添加方法/屬性艘儒。
與函數(shù)一樣,類也可以使用表達式的形式定義夫偶,如下界睁。需要注意的是,這個類的名字是 Me
兵拢,但是 Me
只在 Class
的內部可用翻斟,指代當前類。在 Class
外部卵佛,這個類只能用 MyClass
引用杨赤。如果類的內部沒用到的話敞斋,可以省略 Me
截汪。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
// 采用 Class 表達式,可以寫出立即執(zhí)行的 Class
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
注意:
- 類和模塊的內部植捎,默認就是嚴格模式衙解,考慮到未來所有的代碼,其實都是運行在模塊之中焰枢,所以 ES6 實際上把整個語言升級到了嚴格模式
- 類不存在變量提升(hoist)蚓峦,這一點與 ES5 完全不同
-
name
屬性總是返回緊跟在class
關鍵字后面的類名 - 如果某個方法之前加上星號(*),就表示該方法是一個
Generator
函數(shù) - 類的方法內部如果含有
this
济锄,它默認指向類的實例暑椰。但是,一旦單獨使用該方法荐绝,很可能報錯一汽,可以在構造方法中綁定this
或者使用箭頭函數(shù)來解決
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
如果在一個方法前,加上 static
關鍵字,就表示該方法不會被實例繼承召夹,而是直接通過類來調用岩喷,這就稱為“靜態(tài)方法”。
class Foo {
// 提案监憎,靜態(tài)屬性
static myStaticProp = 42;
// 靜態(tài)方法
static classMethod() {
return 'hello';
}
// 如果靜態(tài)方法包含this關鍵字纱意,這個this指的是類,而不是實例鲸阔,靜態(tài)方法可以與非靜態(tài)方法重名
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function
Foo.bar() // hello
// 父類的靜態(tài)方法偷霉,可以被子類繼承
class Bar extends Foo {
static classMethod() {
// 靜態(tài)方法也是可以從 super 對象上調用的
return super.classMethod() + ', too';
}
}
Bar.bar() // 'hello'
Bar.classMethod() // "hello, too"
// 靜態(tài)屬性
Foo.prop = 1;
Foo.prop // 1
私有方法和私有屬性, ES6 不提供褐筛,只能通過變通方法模擬實現(xiàn)腾它。私有屬性和私有方法前面,也可以加上 static
關鍵字死讹,表示這是一個靜態(tài)的私有屬性或私有方法瞒滴,只能在類的內部調用。
class IncreasingCounter {
// 在命名上加以區(qū)別
_count = 0;
// 提案
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
// 私有屬性不限于從 this 引用赞警,只要是在類的內部妓忍,實例也可以引用私有屬性
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // 42
ES6 為 new
命令引入了一個 new.target
屬性,該屬性一般用在構造函數(shù)之中愧旦,返回 new
命令作用于的那個構造函數(shù)世剖。如果構造函數(shù)不是通過 new
命令或 Reflect.construct()
調用的,new.target
會返回 undefined
笤虫,因此這個屬性可以用來確定構造函數(shù)是怎么調用的旁瘫。
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必須使用 new 命令生成實例');
}
}
var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三'); // 報錯
// 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
// 子類繼承父類時琼蚯,new.target 會返回子類酬凳,利用這個特點,可以寫出不能獨立使用遭庶、必須繼承后才能使用的類
class Square extends Rectangle {
constructor(length, width) {
super(length, width);
}
}
var obj = new Square(3); // 輸出 false
Class 的繼承
Class 可以通過 extends
關鍵字實現(xiàn)繼承宁仔,這比 ES5 的通過修改原型鏈實現(xiàn)繼承,要清晰和方便很多峦睡。父類的靜態(tài)方法翎苫,也會被子類繼承。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的 constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調用父類的 toString()
}
}
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
// Object.getPrototypeOf 方法可以用來從子類上獲取父類榨了,可以使用這個方法判斷煎谍,一個類是否繼承了另一個類
Object.getPrototypeOf(ColorPoint) === Point // true
子類必須在 constructor
方法中調用 super
方法,否則新建實例時會報錯龙屉。這是因為子類自己的 this
對象呐粘,必須先通過父類的構造函數(shù)完成塑造,得到與父類同樣的實例屬性和方法,然后再對其進行加工事哭,加上子類自己的實例屬性和方法漫雷。如果不調用 super
方法,子類就得不到 this
對象鳍咱。ES5 的繼承降盹,實質是先創(chuàng)造子類的實例對象 this
,然后再將父類的方法添加到 this
上面 (Parent.apply(this))
谤辜。ES6 的繼承機制完全不同蓄坏,實質是先將父類實例對象的屬性和方法,加到 this
上面(所以必須先調用super
方法)丑念,然后再用子類的構造函數(shù)修改 this
涡戳。
如果子類沒有定義 constructor
方法,這個方法會被默認添加脯倚,代碼如下渔彰。也就是說,不管有沒有顯式定義推正,任何一個子類都有 constructor
方法恍涂。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
// 只有調用 super 之后,才可以使用 this 關鍵字
super(...args);
}
}
super
這個關鍵字植榕,既可以當作函數(shù)使用再沧,也可以當作對象使用。在這兩種情況下尊残,它的用法完全不同炒瘸。使用 super
的時候,必須顯式指定是作為函數(shù)寝衫、還是作為對象使用顷扩,否則會報錯。第一種情況竞端,super
作為函數(shù)調用時屎即,代表父類的構造函數(shù),super()
只能用在子類的構造函數(shù)之中事富。ES6 要求,子類的構造函數(shù)必須執(zhí)行一次 super
函數(shù)乘陪,super
雖然代表了父類的構造函數(shù)统台,但是返回的是子類的實例。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
第二種情況啡邑,super
作為對象時贱勃,在普通方法中,指向父類的原型對象;在靜態(tài)方法中贵扰,指向父類仇穗。ES6 規(guī)定,在子類普通方法中通過 super
調用父類的方法時戚绕,方法內部的 this
指向當前的子類實例纹坐。由于 this
指向子類實例,所以如果通過 super
對某個屬性賦值舞丛,這時 super
就是 this
耘子,賦值的屬性會變成子類實例的屬性。在子類的靜態(tài)方法中通過 super
調用父類的方法時球切,方法內部的 this
指向當前的子類谷誓,而不是子類的實例。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
m() {
super.print();
}
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
let b = new B();
b.m() // 2
B.myMethod(1); // static 1
var b = new B();
b.myMethod(2); // instance 2
大多數(shù)瀏覽器的 ES5 實現(xiàn)之中吨凑,每一個對象都有 __proto__
屬性捍歪,指向對應的構造函數(shù)的 prototype
屬性。Class 作為構造函數(shù)的語法糖鸵钝,同時有 prototype
屬性和 __proto__
屬性费封,因此同時存在兩條繼承鏈:
- 子類的
__proto__
屬性,表示構造函數(shù)的繼承蒋伦,總是指向父類 - 子類
prototype
屬性的__proto__
屬性弓摘,表示方法的繼承,總是指向父類的prototype
屬性
這兩條繼承鏈痕届,可以這樣理解:作為一個對象韧献,子類(B)的原型(__proto__
屬性)是父類(A);作為一個構造函數(shù)研叫,子類(B)的原型對象(prototype
屬性)是父類的原型對象(prototype
屬性)的實例锤窑。
class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
// B 繼承 A 的靜態(tài)屬性
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;
extends
關鍵字后面可以跟多種類型的值,只要是一個有 prototype
屬性的函數(shù)嚷炉,就能被繼承.由于函數(shù)都有 prototype
屬性(除了 Function.prototype
函數(shù))渊啰,因此可以繼承任意函數(shù),還可以用來繼承原生的構造函數(shù)申屹。
// 子類繼承 Object 類绘证,A其實就是構造函數(shù) Object 的復制,A的實例就是 Object 的實例
class A extends Object {}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
// 不存在任何繼承哗讥,A作為一個基類(即不存在任何繼承)嚷那,就是一個普通函數(shù),所以直接繼承 Function.prototype杆煞。但是魏宽,A調用后返回一個空對象(即Object實例)腐泻,所以 A.prototype.__proto__ 指向構造函數(shù)(Object)的 prototype 屬性
class A {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
子類實例的 __proto__
屬性的 __proto__
屬性,指向父類實例的 __proto__
屬性队询。也就是說派桩,子類的原型的原型,是父類的原型蚌斩。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
以前铆惑,這些原生構造函數(shù)是無法繼承的,比如凳寺,不能自己定義一個 Array
的子類鸭津,因為這個類的行為與 Array
完全不一致,子類無法獲得原生構造函數(shù)的內部屬性肠缨,通過 Array.apply()
或者分配給原型對象都不行逆趋。原生構造函數(shù)會忽略 apply
方法傳入的 this
,也就是說晒奕,原生構造函數(shù)的 this
無法綁定闻书,導致拿不到內部屬性。ES6 允許繼承原生構造函數(shù)定義子類脑慧,因為 ES6 是先新建父類的實例對象 this
魄眉,然后再用子類的構造函數(shù)修飾 this
,使得父類的所有行為都可以繼承闷袒,這是 ES5 無法做到的坑律。
注意,繼承 Object
的子類囊骤,有一個行為差異晃择。下面代碼中,NewObj
繼承了 Object
也物,但是無法通過 super
方法向父類 Object
傳參宫屠。這是因為 ES6 改變了 Object
構造函數(shù)的行為,一旦發(fā)現(xiàn) Object
方法不是通過 new Object()
這種形式調用滑蚯,ES6 規(guī)定 Object
構造函數(shù)會忽略參數(shù)浪蹂。