ES6中Class的基本語法及與ES5中Cass的區(qū)別

Class

目錄

  • 簡介
  • 靜態(tài)方法
  • 實例屬性的新寫法
  • 靜態(tài)屬性
  • 私有方法和私有屬性
  • new.target屬性

一柜去、簡介

類的由來

ES6以前的生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù):

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);

上面這種寫法與傳統(tǒng)的面向?qū)ο笳Z言(比如 C++ 和 Java)差異很大件甥,所以ES6中引入了Class關(guān)鍵字籍救,可以用來定義類,但是其大部分功能都可以用ES5實現(xiàn)晋柱,其更像一個語法糖蛇损。新的Class寫法只是讓對象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已挑势。

上面的例子用ES6改寫成如下:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

ES5 的構(gòu)造函數(shù)Point,對應(yīng) ES6 的Point類的構(gòu)造方法啦鸣。
ES6 的類潮饱,完全可以看作構(gòu)造函數(shù)的另一種寫法:

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

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

使用的時候也是直接new一下,和構(gòu)造函數(shù)的用法完全一致:

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff() // "stuff"

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

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

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

class B {}
let b = new B();

b.constructor === B.prototype.constructor // true

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

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

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

Point.prototype.constructor === Point // true
  • ES6中類的內(nèi)部所有定義的方法赐写,都是不可枚舉的(non-enumerable):
class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
  • 但是ES5中是可以枚舉的:
var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function() {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

constructor 方法

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

class Point {
}

// 等同于
class Point {
  constructor() {}
}

constructor方法默認返回實例對象(即this)端铛,完全可以指定返回另外一個對象:

class Foo {
  constructor() {
    return Object.create(null);
  }
}

new Foo() instanceof Foo
// false

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

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

class Foo {
  constructor() {
    return Object.create(null);
  }
}

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'

類的實例

必須使用new實例化:

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.y = y;
  }

  toString() {
    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

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

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__
//true

p1p2都是Point的實例讯屈,它們的原型都是Point.prototype,所以__proto__屬性是相等的县习,這也意味著涮母,可以通過實例的__proto__屬性為“類”添加方法谆趾。

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__.printName = function () { return 'Oops' };

p1.printName() // "Oops"
p2.printName() // "Oops"

var p3 = new Point(4,2);
p3.printName() // "Oops"

使用實例的__proto__屬性改寫原型,必須相當(dāng)謹慎叛本,不推薦使用沪蓬,因為這會改變“類”的原始定義,影響到所有實例来候。

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

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

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

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

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

屬性表達式

類的屬性名查排,可以采用表達式:

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

Class 表達式

與函數(shù)一樣城榛,類也可以使用表達式的形式定義:

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

需要注意的是感混,這個類的名字是Me奇唤,但是Me只在Class 的內(nèi)部可用,指代當(dāng)前類峭拘。在 Class 外部俊庇,這個類只能用MyClass引用:

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

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

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

采用 Class 表達式辉饱,可以寫出立即執(zhí)行的 Class:

let person = new class {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}('張三');

person.sayName(); // "張三"

注意

1. 嚴格模式
類和模塊的內(nèi)部,默認就是嚴格模式拣展,所以不需要使用use strict指定運行模式彭沼,ES6 實際上把整個語言升級到了嚴格模式。
2. 不存在變量提升
與ES5不同备埃,類不存在變量提升(hoist):

new Foo(); // ReferenceError
class Foo {}

上面代碼中姓惑,Foo類使用在前,定義在后按脚,這樣會報錯于毙,因為 ES6 不會把類的聲明提升到代碼頭部。

{
  let Foo = class {};
  class Bar extends Foo {
  }
}

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

name 屬性

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

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

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

Generator 方法

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

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

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

this指向

類的方法內(nèi)部如果含有this,默認指向類的實例担忧。

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

上面代碼中芹缔,printName方法中的this,默認指向Logger類的實例瓶盛。但是最欠,如果將這個方法提取出來單獨使用,this會指向該方法運行時所在的環(huán)境(由于 class 內(nèi)部是嚴格模式惩猫,所以 this 實際指向的是undefined)芝硬,從而導(dǎo)致找不到print方法而報錯。
可以在構(gòu)造方法中綁定this來解決:

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

還可以使用箭頭函數(shù)來解決:

class Obj {
  constructor() {
    this.getThis = () => this;
  }
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭頭函數(shù)內(nèi)部的this總是指向定義時所在的對象轧房。上面代碼中拌阴,箭頭函數(shù)位于構(gòu)造函數(shù)內(nèi)部,它的定義生效的時候奶镶,是在構(gòu)造函數(shù)執(zhí)行的時候迟赃。
還有一種是使用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());

二捺氢、靜態(tài)方法

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

靜態(tài)方法只能在類上面調(diào)用拭荤,而不能在實例上調(diào)用。

如果靜態(tài)方法包含this關(guān)鍵字疫诽,這個this指的是類舅世,而不是實例:

class Foo {
  static bar() {
    this.baz();
  }
  static baz() {
    console.log('hello');
  }
  baz() {
    console.log('world');
  }
}

Foo.bar() // hello

從上面可以看出靜態(tài)方法可以與非靜態(tài)方法重名旦委。

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

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

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

三雏亚、實例屬性的新寫法

定義在constructor里:

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

定義在頂部:

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}
class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
  }
}

優(yōu)點是不需要寫this缨硝,簡單明了,一目了然罢低。

四查辩、靜態(tài)屬性

class Foo {
}

Foo.prop = 1;
Foo.prop // 1

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

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
// 老寫法
class Foo {
  // ...
}
Foo.prop = 1;

// 新寫法
class Foo {
  static prop = 1;
}

五网持、私有方法和私有屬性

現(xiàn)有方案

ES6 不提供私有方法和私有屬性宜岛,只能通過變通方法模擬實現(xiàn),一種做法是在命名上加以區(qū)別功舀。

class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

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

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

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

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

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

const inst = new myClass();

Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]

六、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)用的涝滴。

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 命令生成實例');
  }
}

Class 內(nèi)部調(diào)用new.target绣版,返回當(dāng)前 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 Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, width);
  }
}

var obj = new Square(3); // 輸出 false

上面代碼中韩脏,new.target會返回子類缩麸。

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

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本類不能實例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 報錯
var y = new Rectangle(3, 4);  // 正確

上面代碼中阅仔,Shape類不能被實例化,只能用于繼承弧械。
注意八酒,在函數(shù)外部,使用new.target會報錯梦谜。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市袭景,隨后出現(xiàn)的幾起案子唁桩,更是在濱河造成了極大的恐慌,老刑警劉巖耸棒,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荒澡,死亡現(xiàn)場離奇詭異,居然都是意外死亡与殃,警方通過查閱死者的電腦和手機单山,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幅疼,“玉大人米奸,你說我怎么就攤上這事∷瘢” “怎么了悴晰?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逐工。 經(jīng)常有香客問我铡溪,道長,這世上最難降的妖魔是什么泪喊? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任棕硫,我火速辦了婚禮,結(jié)果婚禮上袒啼,老公的妹妹穿的比我還像新娘哈扮。我一直安慰自己,他們只是感情好蚓再,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布灶泵。 她就那樣靜靜地躺著,像睡著了一般对途。 火紅的嫁衣襯著肌膚如雪赦邻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天实檀,我揣著相機與錄音惶洲,去河邊找鬼按声。 笑死,一個胖子當(dāng)著我的面吹牛恬吕,可吹牛的內(nèi)容都是我干的签则。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼铐料,長吁一口氣:“原來是場噩夢啊……” “哼渐裂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钠惩,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤柒凉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后篓跛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膝捞,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年愧沟,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔬咬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡沐寺,死狀恐怖林艘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情混坞,我是刑警寧澤北启,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站拔第,受9級特大地震影響咕村,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚊俺,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一懈涛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泳猬,春花似錦批钠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忙上,卻和暖如春拷呆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工茬斧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腰懂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓项秉,卻偏偏與公主長得像绣溜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子娄蔼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Class 的基本語法 簡介 JavaScript 語言中怖喻,生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù)。下面是一個例子岁诉。...
    huilegezai閱讀 517評論 0 0
  • class的基本用法 概述 JavaScript語言的傳統(tǒng)方法是通過構(gòu)造函數(shù)锚沸,定義并生成新對象。下面是一個例子: ...
    呼呼哥閱讀 4,070評論 3 11
  • 簡介 JavaScript語言中唉侄,生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù)咒吐。 ES6引入了Class(類)這個概念野建,作...
    oWSQo閱讀 365評論 0 0
  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔属划,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 2,732評論 2 9
  • 基本語法 簡介 JavaScript語言中,生成實例對象的傳統(tǒng)方法是通過構(gòu)造函數(shù). ES6提供更接近傳統(tǒng)語言的寫法...
    JarvanZ閱讀 874評論 0 0