学习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); // 正确














相关文章
相关标签/搜索