es6之class

1,921 阅读5分钟

class与构造函数

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

//类的写法
class Point{
    getName(){
        return 'lili';
    }
}

//构造函数的写法
function Point2() {}
Point2.prototype.getName = function () {
    return 'lili';
}

其实class也是函数,类里的方法也是定义在prototype上。类的方法都是不可枚举的,通过Object.keys不能获得,但是构造函数原型上的方法可以获得。

//类
typeof Point; //'function'
Point === Point.prototype.constructor  //true

Object.getOwnPropertyNames(Point.prototype) 
//[ 'constructor', 'getName' ]

Object.keys(Point.prototype)
//[]

//构造函数
Object.getOwnPropertyNames(Point2.prototype);//包括自身的,可枚举和不可枚举的
//[ 'constructor', 'getName' ]
Object.keys(Point2.prototype);
[ 'getName' ]

constructor方法

类中可以显示定义constructor方法,如果没有定义,则隐式创建一个空的constructor方法。

class point {}

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

constructor方法,默认返回值是类的实例对象(this)。可以自定义返回值 ,如果这样做改变this值了,最好不要这样做。

class Point{
    constructor(x, y){
        this.x = x;
        this.y = y;
        return {}; //不推荐用法
    }
}
let point = new Point(1, 2); //{}

类实例

注意:生成实例,类必须使用new关键字定义,否则报错。构造函数不是必须使用new

class Point{}
let point = Point.call({}); //报错


function Point2(x) { 
    this.x = x; 
    return this;
}
let point2 = Point2.call({}, 1);//ok {x: 1}

类的所有实例共享一个原型对象

class Point { 
    y = 10; //定义属性方式
    constructor(x){
        this.x = x; //定义属性方式
    }
    setZ(z){
        this.z = z; //定义属性
    }
    getX(){
        return this.x;
    }
}
let point = new Point(1); 
let point2 = new Point(2); 

point.hasOwnProperty('x'); //true
point.hasOwnProperty('getX'); //false

point.__proto__ === point2.__proto__ === Point.prototype //true

Object.getOwnPropertyNames(Point.prototype) 
//[ 'constructor', 'getX' ]

this是实例对象,定义在this上的属于实例的自身的属性,如x,y,z。getX方法是定义在原型对象上的,所有的实例共享的。

静态属性和静态方法

静态属性现在还没有

静态方法

  • 属于类,不被实例继承,直接通过类来调用。可以被子类继承
  • 内部的this,执行Point本身,而不是实例
  • 可以和非静态方法重名
class Point {
    static getX() {
        this.getY();
    }
    static getY(){
        console.log('y');
    }
    getY(){
        console.log('yy');
    }
}
let point = new Point();
Point.getX();
//'y'

私有属性和私有方法

私有属性和私有方法还没有实现。只能约定下划线打头方法为私有的。或利用Symbol值的唯一性生成

const bar = Symbol('bar');
class Point {
    [bar](){
        console.log('x')
    }
}

let point = new Point()
console.log(Reflect.ownKeys(Point.prototype));
//[ 'constructor', Symbol(bar) ]

一般情况下外界取不到bar的值,所以成了私有方法。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

new.target

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

class Point {
    constructor() {
        console.log(new.target === Point); //true
    }
}
let point = new Point();

只能在构造函数constructor中使用new.target。其他地方使用会报错

function Point2(name) {
    if (new.target === Point2) {
        this.name = name;
    } else {
        throw new Error('必须使用 new 命令生成实例');
    }
}
let point3 = new Point2('sha');
let point2 = Point2.call({}, 'li'); //抛出异常

利用new.target,限定构造函数创建实例,只能使用new操作符。

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类

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

继承

继承就是子类继承父类的属性和方法。注意以下几点:

  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错
  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。
  • 父类的静态方法,也会被子类继承
class Point {
    static hello() { //静态方法会被子类继承
        console.log('hello world');
    }
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    getX(){
        return this.x;
    }
}

class ColorPoint extends Point{
    constructor(x, y, color){
        this.color = color; //错误
        super(x, y);
        this.color = color; //正确
    }
    getX(){
        console.log(this); //ColorPoint { x: 10, y: 1, color: 'red' }
        return this.color + ' ' + super.getX();
    }
}

let cp = new ColorPoint(10, 1, 'red');
console.log(cp.getX()); //red 10
ColorPoint.hello(); //hello world

Object.getPrototypeOf(ColorPoint) === Point //true
cp instanceof Point //true
cp instanceof ColorPoint //true

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。如果不调用super方法,子类就得不到this对象。

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。

  • 第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。并且super函数只能用在子类的构造函数之中,用在其他地方就会报错。

  • 第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

    在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

class A {
  constructor() {
    console.log(new.target.name);
    this.x = 10;
  }
  getX(){
      return this.x;
  }
}
class B extends A {
  constructor() {
    super();
    this.x = 20;
  }
}
let a = new A() // A
let b = new B() // B

b.getX(); //20  

B中super()在这里相当于A.prototype.constructor.call(this)。new.target指向当前正在执行的函数

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 1  静态方法

var child = new Child();
child.myMethod(2); // instance 2  普通方法

类的 prototype 属性和__proto__属性

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类。

  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true  属性继承
B.prototype.__proto__ === A.prototype // true 方法继承

注:本文是读阮一峰老师《ECMAScript 6 入门》的学习笔记