详解ES6的类

基础

一个简单的示意图

ES5实现构造函数和实例对象

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

ES6的class可以看作一个语法糖,他的绝大部分功能ES5都可以做到,class写法只是让对象圆形的写法更加清晰,更像面向对象变成的语法

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
 }
  
  
typeof Point // "function"
Point === Point.prototype.constructor // true

constructor构造方法,this代表实例对象,ES5的构造函数对应ES6的constructor方法,类中的所有方法都定义在类的prototype上。

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

在类的实例上调用方法就是调用原型上的方法
Object.assign方法可以很方便地一次向类添加多个方法

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

类内部的所有方法都是不可枚举的,但是用ES5在prototype定义的方法可枚举

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

类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,默认返回实例对象(即this),完全可以指定返回另外一个对象。一个类必须有constructor方法,如果没有显式定义,
JavaScript 引擎会自动为它添加一个空的constructor方法

实例属性

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
  }}

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class Foo {}
Foo.prop = 1;
Foo.prop // 1

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性,
现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字

static 静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

如果静态方法包含this关键字,这个this指的是类,而不是实例。
静态方法可以与非静态方法重名
父类的静态方法,可以被子类继承
静态方法也是可以从super对象上调用的,super.staticFun()

类的实例

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。


var p1 = new Point(2, 3);
var p2 = new Point(3,2);
point.toString() // (2, 3)

//定义在this上
p1.hasOwnProperty('x') // true
p1.hasOwnProperty('y') // true

//定义在原型上
p1.hasOwnProperty('toString') // false
p1.__proto__.hasOwnProperty('toString') // true


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

_ _ proto_ _ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

getter setter

与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

存值函数和取值函数是设置在属性的 Descriptor 对象上的。


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'

class表达式

采用 Class 表达式,可以写出立即执行的 Class。
person是class实例

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

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

person.sayName();

私有方法和属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现

  1. 命名加以区分,加“_”表示私有方法,但不能从根本区分
  2. 把私有方法拿到类外定义
  3. 利用Symbol的唯一性,把私有方法的名字命名为一个symbol值,
    一般情况下无法获取到它们,但还是可以通过Reflect.ownKeys()拿到

目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示
详解ES6的类_第1张图片

new target 属性

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

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


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

class继承

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象

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

c父类的静态方法,也会被子类继承。

不管有没有显式定义,任何一个子类都有constructor方法。


class ColorPoint extends Point {}
// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }}

Object.getPrototypeOf

可以用来从子类上获取父类,
可以使用这个方法判断,一个类是否继承了另一个类

Object.getPrototypeOf(ChildPoint) === ParentPoint
// true

super关键字

当作函数调用时,代表调用父类的构造函数,但返回子类实例

super内部的this指向子类相当于A.prototype.constructor.call(this)。子类构造函数必须执行一次super函数, super函数只能用在子类构造函数里

当作对象调用,在普通方法中指向父类的原型对象,在静态方法中指向父类

在普通方法中:
super.p()就相当于A.prototype.p(),
由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的,super内部this指向当前子类实例,相当于
super.parentfun.call(this)

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

在静态方法中:
如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例


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); ![659be1c89defe3031f7e5beb4f365621.png](evernotecid://BC415947-3551-4894-9F72-1E5DB96250EB/appyinxiangcom/25723750/ENResource/p728)
// static 1
var child = new Child();
child.myMethod(2); // instance 2

类的 prototype 属性和__proto__属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

B.__proto__ = A

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

B.prototype.__proto__ = A.prototype

这样的结果是因为,类的继承是按照下面的模式实现的。

class A {}

class B {}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();

Object.setPrototypeOf方法的实现

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;}

可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例

实例的 proto 属性

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类实例的原型的原型,是父类实例的原型

因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。

p2.__proto__.__proto__.printName = function () {
  console.log('Ha');};

p1.printName() // "Ha"

原生构造函数的继承

ECMAScript 的原生构造函数大致有下面这些。
-Boolean()
-Number()
-String()
-Array()
-Date()
-Function()
-RegExp()
-Error()
-Object()

按照ES5的规则,先创建子类的this,在将父类的属性添加到子类上,但由于原生构造函数内部属性无法获取,导致无法继承

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。

extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构

参考链接:阮一峰ES6入门

你可能感兴趣的:(详解ES6的类)