在ES5及以前的版本中,並無類的概念,那時候建立類的方法是你們都很熟悉,以下是一個最簡單的例子:java
// 通常咱們約定以大寫字母開頭來表示一個構造函數
function Person (name) {
this.name = name;
}
// 在構造函數的原型鏈上定義不一樣的方法
Person.prototype.getName = function () {
return this.name;
}
// 實例化一個對象,擁有構造函數原型鏈上的方法
var people = new Person('lilei');
people.getName(); // lilei
複製代碼
可是這種寫法和傳統的面嚮對象語言(例如java)有較大區別,不太好理解。因此在ES6中引入了class關鍵字,它是一個讓對象原型的寫法更像面向對象編程語法的一個語法糖。node
在node開發以及一些組件的開發中,會常常用到class語法,本文將先介紹class的一些基礎用法,而後深刻剖析class的繼承,以及原型鏈相關的知識。總結一下本身學習class的過程,也但願經過一些示例能幫助到部分同窗,由淺及深,逐步理解class的用法以及原理。es6
咱們先來看一個class的常規寫法:編程
class Person {
_myName = 'initial-name';
constructor (name = '') {
this._myName = name;
}
getName () {
return this._myName;
}
static toUpperCaseName () {
return this.name.toUpperCase();
}
get name () {
return this._myName;
}
set name (name) {
this._myName = name;
}
}
let people = new Person('lilei');
console.log(people.name); // lilei
console.log(people.getName()); // lilei
console.log(Person.toUpperCaseName()); // PERSON
people.name = 'lilei2';
console.log(people.name); // lilei2
複製代碼
下面咱們把這個示例拆分開來,逐一瞭解class的基礎用法。bash
一、class在定義時,必須定義一個構造方法(constructor),若是不寫,Javascript引擎會自動添加一個空的constructor方法。函數
class Person {}
// 實際上是
class Person {
constructor () {}
}
複製代碼
二、constructor方法是class的構造方法,在class實例化對象時(即new一個對象時)會自動調用構造方法,而且默認返回實例對象(this),固然你徹底能夠返回另一個對象。學習
class Person {
constructor (name) {
console.log(name);
}
}
new Person('lilei'); // lilei
複製代碼
一、定義class的非靜態方法不用加上function關鍵字,也不須要用逗號隔開,用了反而會報錯。而這些普通方法在實例化時,就會被實例繼承。 二、定義class的靜態方法,則是在方法名前加static關鍵字,這樣定義的方法,就不會被實例繼承,可是它能夠直接被類調用,以下:ui
class Person {
func1 () {
console.log('func1');
}
static func2 () {
console.log('func2');
}
}
let people = new Person();
people.func1(); // func1
people.func2(); // people.func2 is not a function
Person.func1(); // Person.func1 is not a function
Person.func2(); // func2
複製代碼
三、靜態方法能夠與非靜態方法重名;靜態方法中,this指向的是類,不是實例。this
class Person {
static func1() {
console.log(this.prototype.func2());
console.log(this.func2());
}
static func2() {
console.log('static');
}
func2() {
console.log('function');
}
}
Person.func1();
// function
// static
複製代碼
四、非靜態方法中,不能直接使用this關鍵字來訪問靜態方法,而是要用類名來調用。若是靜態方法包含this關鍵字,這個this指的是類,而不是實例。spa
class Person {
constructor() {
console.log(Person.func1());
console.log(this.constructor.func1());
}
static func1() {
console.log('static');
}
}
複製代碼
五、另外提一點,class的方法名能夠用變量來命名,以下:
let funcName = 'getName';
class Person {
[funcName] () {}
}
複製代碼
六、類的全部方法都定義在類的prototype屬性上面
一、在ES6中,屬性定義的常規寫法是定義在類的constructor方法裏的this上,示例見本文基礎用法中第一個代碼段,這裏就再也不寫了。 二、在ES7中定義了一種新的屬性定義規範,能夠將屬性定義在類的最頂層,這樣使代碼看上去更加整潔,很清晰的看到類中定義的屬性。
class Person {
_name = '';
constructor (name) {
this._name = name;
}
}
複製代碼
一、在class裏還能夠給屬性添加get和set關鍵字,攔截屬性的存取行爲。寫法見基礎用法中第一個代碼段。 二、當一個屬性只有getter沒有setter的時候,咱們是沒法進行賦值操做的,第一次初始化也不行。若是把變量定義在類的外面,就能夠只使用getter不使用setter。
let data = {};
class Person{
constructor() {
this.name = 'lilei';
this.age = 20;
}
get age(){
return this._age;
}
// 若是沒有這個set,則會報錯:Cannot set property width of #<GetSet> which has only a getter
set age(age){
this._age = age;
}
get data(){
return data;
}
}
let people = new Person();
console.log(people.age);
people.age = 30;
console.log(people.age);
console.log(people._age);
console.log(people.data);
複製代碼
三、不能在set方法中設置本身的值,否則會陷入無限遞歸,致使棧溢出。
set age (v) {
this.age = v;
}
// Uncaught RangeError: Maximum call stack size exceeded
複製代碼
一、類能夠用表達式的方式來定義,須要注意的是P只能在class內部使用,用於指代當前的類;Person只能在外部使用。
const Person = class P {
getName () {
return P.name // p
}
}
Person.name // P
複製代碼
二、和let同樣,class不存在變量提高,即不能先使用後聲明。 三、class擁有name屬性,返回的是class關鍵字後面的名字,見上面的例子。
實例的屬性及方法除非顯式定義在其自己(即定義在this對象上),不然都是定義在原型上(即定義在class上)。ES7中約定的新的定義屬性的規範也是定義在實例上的。
class P {
a = 1;
constructor(b) {
this.b = b;
}
c() {
console.log(this.a, this.b);
}
}
var p1 = new P(2);
p1.hasOwnProperty('a') // true
p1.hasOwnProperty('b') // true
p1.hasOwnProperty('c') // false
p1.__proto__.hasOwnProperty('c') // true
複製代碼
咱們來看這樣一段代碼:
class P {
constructor(id) {
this.id = id;
}
id() {
console.log( "Id: " + this.id );
}
}
var p1 = new P( "p1" );
p1.id(); // TypeError -- p1.id 如今是字符串"p1"
複製代碼
在構造函數中定義的屬性會屏蔽同名的方法。注意這裏是屏蔽,不是覆蓋、前面說到經過this定義的屬性是定義在實例上的,而方法是定義在類上的,因此實例在讀取id的時候會率先讀取到定義在實例上的屬性id,也就讀不到方法id了。
class引入目的就是讓類定義起來更直觀接單,經過extends關鍵字實現繼承的寫法也更便於理解。其中class A extends B 中A稱爲派生類,派生類是指繼承自其它類的新類。
一、派生類沒有本身的this對象,而是繼承父類的this對象,因此須要在構造函數中先調用super()初始化this對象,以後才能在構造函數中訪問this。ES6 要求,子類的構造函數必須執行一次super函數。 二、super既能夠當函數使用,也能夠當對象使用。 1)當函數使用時,它表明的是父類的構造函數,而且只能在子類的構造函數中使用,否則就會報錯。 2)當對象使用時,若是是在普通方法中,指向對象的原型對象;若是是在靜態方法中,指向的是父類。
class Person {
static getMethod() {
console.log('static');
}
getMethod() {
console.log('instance');
}
}
class Man extends Person {
static getMethod() {
super.getMethod();
}
getMethod(msg) {
super.getMethod();
}
}
Man.getMethod(); // static
var lilei = new Man();
lilei.getMethod(); // instance
複製代碼
三、super指向的是父類的原型對象,因此定義在父類實例上的方法或屬性,是沒法經過super調用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m; // undefined
複製代碼
四、經過super調用父類的方法時,super會綁定子類的this
class A {
constructor() {
this.color = 'red';
}
draw() {
console.log(this.color);
}
}
class B extends A {
constructor() {
super();
this.color = 'yellow';
}
draw() {
super.draw(); // 這裏是會綁定B的this,也就是super.draw.call(this)
}
}
let b = new B();
b.draw() // yellow
複製代碼
由於class繼承的邏輯是先新建一個父類的this對象,而後在子類的構造函數中對this進行加工,因此父類的全部行爲均可以被繼承。也就是class能夠繼承原生構造函數來定義子類。
class MyArray extends Array {}
let arr = new MyArray(1,2);
console.log(arr); // [1, 2]
複製代碼