學習es6中class——整合阮一峯教程、MDN

導語

class只是語法糖,並無爲js 引入一種新的對象繼承模式,以前經過原型鏈同樣能夠實現class的功能;
    
    
    
    
//定義類class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; }}

定義class 

class 就像是特殊的函數,和建立函數同樣,有 類聲明(class declarations),和 類表達式( class expressions )兩種建立類的方式:

類聲明

使用class 關鍵字,後面跟上類名(class 關鍵字後面的是類名)
    
    
    
    
class Rectangle { constructor(height, width) { this.height = height; this.width = width; }}
類聲明普通函數聲明不一樣的地方在於,類聲明並無函數提高(下面要介紹的類表達式也沒有函數提高):
    
    
    
    
var obj = new Myclass(); //報錯class Myclass (){ }
類聲明不能函數提高是爲了保證,子類繼承父類的那個語句,不會提高至頭部,不然將會出現父類尚未定義,子類就要繼承, 看下面的例子:
    
    
    
    
{ let B = class {}; // let 聲明 不存在函數提高 class A extends B { //若是存在類哈函數提高的話,這行會提高到第一行,父親還沒聲明,兒子怎麼繼承? } }
類聲明不能和已經存在的類重名,(無論這個類以前是經過類聲明的方式聲明仍是經過類表達式的方式聲明的), 不然將會報錯;
     
     
     
     
class f1 {}; class f1 {}; var f2 = class {}; class f2 {}; // 報錯了 class f3 {}; var f3 = class {}; // 報錯了 var f4 = class {}; var f4 = class {}; // 若是兩個函數表達式重名了,那麼不會報錯

類表達式

類表達式是定義類的另一種方式
    
    
    
    
var myClass = class [className] [extends] { // class body}
就像函數表達式同樣,在類表達式中,類名是無關緊要的。若定義的類名,則該類名只有的類的內部才能夠訪問到。
    
    
    
    
// 方式一const MyClass = class {};// 方式二:給出類名const MyClass = class Me { getClassName() { return Me.name; }};
若是class 後面沒有名字,那麼該類.name  就是 函數表達式的名字:
     
     
     
     
var Foo = class { constructor() {} bar() { return 'Hello World!'; }};var instance = new Foo();instance.bar(); // "Hello World!"Foo.name; // "Foo"
若是 class 後面有名字,那麼該名字只能在函數內被訪問到,同時該類 . name 就是class 後面的名字:
     
     
     
     
var Foo = class NamedFoo { constructor() {} whoIsThere() { return NamedFoo.name; }}var bar = new Foo();bar.whoIsThere(); // "NamedFoo"NamedFoo.name; // ReferenceError: NamedFoo is not definedFoo.name; // "NamedFoo"

採用類表達式,能夠寫出當即執行的Class。以下:
    
    
    
    
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); }}('Zhang San');person.sayName(); // Zhang San

類體和方法定義

類的成員須要定義在一對大括號內{},大括號內的代碼的大括號自己組成了類體。類成員包括 類構造器 類方法  (包括靜態方法和實例方法)。
類體中的代碼都強制在嚴格模式中執行,即默認」use strict」。考慮到將來全部的代碼,其實都是運行在模塊之中,因此ES6實際上把整個語言升級到了嚴格模式。
構造器(constructor方法)

一個類只能擁有一個名爲constructor的方法(不然會報錯),一個類的 constructor 方法只有在實例化的時候被調用。javascript

若是沒有顯式定義constructor方法,這個方法會被默認添加,即,無論有沒有顯示定義,任何一個類都有constructor方法。
html

子類必須在constructor方法中調用super方法,不然新建實例時會報錯。由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工,若是不調用super方法,子類就得不到this對象。
java

    
    
    
    
class Point {}class ColorPoint extends Point { constructor() {}}let cp = new ColorPoint(); // ReferenceError
上面代碼中, ColorPoint 繼承了父類 Point ,可是它的構造函數沒有調用 super 方法,致使新建實例時報錯。

原型方法

定義類的方法時,方法名前面不須要加上function關鍵字。另外,方法之間不須要用逗號分隔,加了會報錯。es6

    
    
    
    
class Bar { constructor() {} doStuff() {} toString() {} toValue() {}}
上面的寫法就等同於下面:
     
     
     
     
Bar.prototype = { doStuff() {}, toString() {}, toValue() {}};
因此,在類的實例上調用方法,實際上就是調用原型上的方法。既然 類的方法都是定義在 prototype 上面,因此類的新方法能夠添加在 prototype 對象上面。 Object.assign 方法能夠很方便地一次向類添加多個方法。
      
      
      
      
class Point { constructor() { // ... }}Object.assign(Point.prototype, { toString() {}, toValue() {}});
另外,類的內部全部定義的方法,都是不可枚舉的(non-enumerable)。
        
        
        
        
class Point { constructor(x, y) { // ... } toString() { return '(' + x + ', ' + y + ')'; }}Object.keys(Point.prototype); // []Object.getOwnPropertyNames(Point.prototype); // ["constructor", "toString"]Object.getOwnPropertyDescriptor(Point, 'toString');// Object {writable: true, enumerable: false, configurable: true}

靜態方法
static 關鍵字用來定義類的靜態方法。靜態方法是指那些不須要對類進行實例化,使用類名就能夠直接訪問的方法。靜態方法常常用來做爲工具函數。
     
     
     
     
class Point { constructor(x, y) { this.x = x; this.y = y; } static distance(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx*dx + dy*dy); }}const p1 = new Point(5, 5);const p2 = new Point(10, 10);console.log(Point.distance(p1, p2));

靜態方法不能夠被實例繼承,是經過類名直接調用的。可是,父類的靜態方法能夠被子類繼承。express

      
      
      
      
class Foo { static classMethod() { return 'hello'; }}class Bar extends Foo {}Bar.classMethod(); // "hello"

extends關鍵字

extends關鍵字用於實現類之間的繼承。子類繼承父類,就繼承了父類的全部屬性和方法。 extends後面只能夠跟一個父類。babel

    
    
    
    
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調用父類的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 調用父類的toString() }}
extends 關鍵字不能用於繼承一個對象,若是你想繼承自一個普通的對象,你必須使用 Object.setPrototypeof ( )

es5 的繼承和 es6 的繼承
es5中的原型鏈繼承,就是經過將子類構造函數的原型做爲父類構造函數的實例(sub.prototype=new super),這樣就連通了子類-子類原型-父類;
      
      
      
      
//先來個父類,帶些屬性 function Super(){ this.flag = true; } //爲了提升複用性,方法綁定在父類原型屬性上 Super.prototype.getFlag = function(){ return this.flag; } //來個子類 function Sub(){ this.subFlag = false; } //實現繼承 Sub.prototype = new Super; //給子類添加子類特有的方法,注意順序要在繼承以後 Sub.prototype.getSubFlag = function(){ return this.subFlag; } //構造實例 var es5 = new Sub;
可是這樣的原型鏈繼承有問題:咱們的目標是構造函數的屬性私有化,方法複用化,因此咱們把屬性放在函數內,把方法放到原型上;可是原型鏈繼承顯然,父類的屬性和方法都放到了子類的原型上;
爲了解決上面的作法,咱們在es5中混合使用 構造函數call 繼承;
        
        
        
        
function Super(){ this.flag = true; } Super.prototype.getFlag = function(){ return this.flag; //繼承方法 } function Sub(){ this.subFlag = flase Super.call(this) //繼承屬性 } Sub.prototype = new Super; var obj = new Sub(); Sub.prototype.constructor = Sub; Super.prototype.getSubFlag = function(){ return this.flag; }
可是還有個小問題是,子類.prototype = new 父類,子類.prototype的constructor 就指向了父類,因此咱們要重寫一下:
         
         
         
         
Sub.prototype.constructor = Sub;
ES6的繼承實現方法,其內部其實也是ES5組合繼承的方式,經過call構造函數,在子類中繼承父類的屬性,經過原型鏈來繼承父類的方法。
咱們將 extend 用babel 進行轉碼:
          
          
          
          
function _inherits(subClass, superClass) { // 確保superClass爲function if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } // 把子類.prototype 繼承了父類.prototype(new 父類), 同時把子類prototype的constructor進行了重寫; // 給subClass添加constructor這個屬性 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // 將父類設爲子類的prototype if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;}
裏面  子類. prototype = Object.create ( 父類.prototype )這句話,實際上和下面的代碼相似:
           
           
           
           
子類.prototype = new 父類
不一樣的是,   子類. prototype = Object.create ( 父類.prototype )不會繼承父類的constructor裏面的屬性,只會繼承父類prototype上的方法;
一個關於 Object.create(car.prototype) 用法的代碼:
           
           
           
           
function Car (desc) { this.desc = desc; this.color = "red";} Car.prototype = { getInfo: function() { return 'A ' + this.color + ' ' + this.desc + '.'; }};//instantiate object using the constructor functionvar car = Object.create(Car.prototype);car.color = "blue";alert(car.getInfo()); //displays 'A blue undefined.' ??! // 看見了吧,只會繼承方法,不能繼承屬性
當你只想繼承類的原型,而不想繼承類的constructor的時候,使用Object.create 是很棒的選擇;
若是咱們想子類繼承父類的prototype ,同時子類也要有本身的屬性,請看下面的代碼:
           
           
           
           
var Car2 = Object.create(null); //this is an empty object, like {}Car2.prototype = { getInfo: function() { return 'A ' + this.color + ' ' + this.desc + '.'; }}; var car2 = Object.create(Car2.prototype, { //value properties color: { writable: true, configurable:true, value: 'red' }, //concrete desc value rawDesc: { writable: false, configurable:true, value: 'Porsche boxter' }, // data properties (assigned using getters and setters) desc: { configurable:true, get: function () { return this.rawDesc.toUpperCase(); }, set: function (value) { this.rawDesc = value.toLowerCase(); } }}); car2.color = 'blue';alert(car2.getInfo()); //displays 'A RED PORSCHE BOXTER.'
每個屬性又是一堆屬性的集合,又稱descriptor, 分爲 data descriptor 和 accessor(訪問 ) descriptor
總之,extends作了兩件事情,一個是經過Object.create()把子類的原型賦值爲父類的實例, 實現了繼承方法,子類.prototype.__proto__也自動指向父類的原型,一個是手動修改了子類的__proto__, 修改成指向父類,(原本在es5 中應該是指向Function.prototype);

在子類中必須執行的super()方法,其實是用call 方法:
           
           
           
           
var _this = _possibleConstructorReturn(this, (b.__proto__ || Object.getPrototypeOf(b)).call(this));
父類被當成普通函數來執行,從而將this綁定到子類上;

同時,extend後面能夠跟多種類型的值:
第一種特殊狀況,子類繼承Object類。
         
         
         
         
class A extends Object {}A.__proto__ === Object // trueA.prototype.__proto__ === Object.prototype // true
第二種特殊狀況,不存在任何繼承。
         
         
         
         
class A {}A.__proto__ === Function.prototype // trueA.prototype.__proto__ === Object.prototype // true
第三種特殊狀況,子類繼承null。
          
          
          
          
class C extends null { constructor() { return Object.create(null); }}


兩條繼承鏈
一個繼承語句同時存在兩條繼承鏈:一條實現屬性繼承,一條實現方法繼承.
     
     
     
     
class A extends B {}A.__proto__ === B; //繼承屬性A.prototype.__proto__ === B.prototype; //繼承方法
ES6的子類的__proto__是父類,子類的原型的__proto__是父類的原型
第二條繼承鏈理解起來沒有什麼問題,es6 自己就是對es5 混合模式繼承的封裝,在原型繼承上,es6使用的是 
        
        
        
        
子類.prototype = Object.create (父類.prototype // 至關於 new 父類
子類的原型是父類的實例(暫時這樣理解,其實子類並不能繼承父類的屬性,只能繼承方法),因此子類.prototype(父類的實例)指向父類的prototype。

可是第一個繼承鏈就很差理解了,在ES5中 子類.__proto__是指向Function.prototype的,由於每個構造函數其實都是Function這個對象構造的。在ES6的繼承中,有這樣一句話:
       
       
       
       
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
es6 的 extends 把子類的__proto__指向父類能夠實現屬性的繼承,在ES5中在沒有用借用繼承的時候因爲父類屬性被子類原型繼承,全部的子類實例實際上都是同一個屬性引用。

能夠這麼說,在ES5繼承和構造實例,ES6構造實例的時候能夠理解__proto__指向構造函數的原型的,可是在ES6繼承中,__proto__指繼承自哪一個類或原型。也就是說,兩條繼承鏈只存在於兩個類之間的關係,實例與構造函數之間的關係,仍是es5的模式;
       
       
       
       
var p1 = new Point(2,3);var p2 = new Point(3,2);p1.__proto__ === p2.__proto__
這也意味着,能夠經過實例的__proto__屬性爲Class添加方法。
        
        
        
        
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"
可是咱們不推薦這樣作

super 關鍵字

super關鍵字能夠用來調用其父類的構造器或方法。super 做爲方法的時候,必須在 constructor 中調用,而且只能在 constructor 裏面被調用app

    
    
    
    
class Cat { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); }}class Lion extends Cat { speak() { super.speak(); console.log(this.name + ' roars.'); }}
super雖然表明了父類A的構造函數,可是返回的是子類B的實例,即super內部的this指的是B,所以super()在這裏至關於A.prototype.constructor.call(this)。


第二種狀況,super做爲對象時,在普通方法中,指向父類的原型對象,能夠調用原型上的方法(可是父類的私有屬性和方法就調用不到了);在靜態方法中,指向父類。
      
      
      
      
class A { p() { return 2; }}class B extends A { constructor() { super(); console.log(super.p()); // 2 }}let b = new B();
ES6 規定,經過super調用父類的方法時,super會綁定子類的this。
      
      
      
      
class A { constructor() { this.x = 1; } print() { console.log(this.x); }}class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); }}let b = new B();b.m() // 2
經過super對某個屬性賦值,super 的this 指向子類,若是要訪問,super 的 this 就變成了父類的prototype:
       
       
       
       
class A { constructor() { this.x = 1; }}class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 }}let b = new B();
若是super做爲對象,用在靜態方法之中,這時super將指向父類,而不是父類的原型對象。
        
        
        
        
class Parent { static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); }}class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); }}Child.myMethod(1); // static 1var child = new Child();child.myMethod(2); // instance 2
注意,使用super的時候,必須顯式指定是做爲函數、仍是做爲對象使用,不然會報錯。
        
        
        
        
class A {}class B extends A { constructor() { super(); console.log(super); // 報錯 }}

 





ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。
只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。




類的Getter和Setter方法

與ES5同樣,在類內部可使用getset關鍵字,對某個屬性設置取值和賦值方法。函數

    
    
    
    
class Foo { constructor() {} get prop() { return 'getter'; } set prop(val) { console.log('setter: ' + val); }}let foo = new Foo();foo.prop = 1;// setter: 1foo.prop;// "getter"
上面代碼中, prop 屬性有對應 的賦值和取值方法,所以賦值和讀取行爲都被自定義了。 
存值和取值方法是設置在屬性的descriptor對象上的。
    
    
    
    
var descriptor = Object.getOwnPropertyDescriptor(Foo.prototype, 'prop');"get" in descriptor // true"set" in descriptor // true
上面代碼中,存值和取值方法是定義在 prop 屬性的描述對象上的,這與ES5一致。

類的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
上面代碼中,Foo類的Symbol.iterator方法前有一個星號,表示該方法是一個Generator函數。Symbol.iterator方法返回一個Foo類的默認遍歷器, for...of 循環會自動調用這個遍歷器。



new.target屬性
ES6爲new命令引入了一個new.target屬性,(在構造函數中)返回new命令做用於的那個構造函數。若是構造函數不是經過new命令調用的(好比說calll),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, '張三'); // 報錯

Class內部調用new.target,在new 一個實例的時候 ,返回當前Class。然而當子類繼承父類時,new.target會返回子類。
        
        
        
        
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); // ... }}class Square extends Rectangle { constructor(length) { super(length, length); // 至關於執行父類中的constructor, }}var obj = new Square(3); // 輸出 false
利用這個特色,能夠寫出不能獨立使用、必須繼承後才能使用的類。
          
          
          
          
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); // 正確














相關文章
相關標籤/搜索