ES6-Class

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ù)浪蹂。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市告材,隨后出現(xiàn)的幾起案子坤次,更是在濱河造成了極大的恐慌,老刑警劉巖创葡,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浙踢,死亡現(xiàn)場離奇詭異,居然都是意外死亡灿渴,警方通過查閱死者的電腦和手機洛波,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來骚露,“玉大人蹬挤,你說我怎么就攤上這事〖遥” “怎么了焰扳?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長误续。 經常有香客問我吨悍,道長,這世上最難降的妖魔是什么蹋嵌? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任育瓜,我火速辦了婚禮,結果婚禮上栽烂,老公的妹妹穿的比我還像新娘躏仇。我一直安慰自己,他們只是感情好腺办,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布焰手。 她就那樣靜靜地躺著,像睡著了一般怀喉。 火紅的嫁衣襯著肌膚如雪书妻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天躬拢,我揣著相機與錄音躲履,去河邊找鬼。 笑死估灿,一個胖子當著我的面吹牛崇呵,可吹牛的內容都是我干的。 我是一名探鬼主播馅袁,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼域慷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汗销?” 一聲冷哼從身側響起犹褒,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弛针,沒想到半個月后叠骑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡削茁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年宙枷,在試婚紗的時候發(fā)現(xiàn)自己被綠了掉房。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡慰丛,死狀恐怖卓囚,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情诅病,我是刑警寧澤哪亿,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站贤笆,受9級特大地震影響蝇棉,放射性物質發(fā)生泄漏。R本人自食惡果不足惜芥永,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一篡殷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恤左,春花似錦贴唇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巧鸭,卻和暖如春瓶您,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纲仍。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工呀袱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人郑叠。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓夜赵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乡革。 傳聞我的和親對象是個殘疾皇子寇僧,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355