ECMAScript6(17):Class类

class声明

class 是 ES6 模拟面向对象言语(C++, Java)提出的定义类的要领。情势相似 C++ 和 Java (各取所长), 下面例子展现了 class 是怎样定义组织函数、对象属性和对象动/静态要领的:

class Point{
  constructor(x, y){    //定义组织函数
    this.x = x;         //定义属性x
    this.y = y;         //定义属性y
  }                     //这里没有逗号
  toString(){           //定义动态要领,不须要 function 关键字
    return `(${this.x},${this.y})`;
  }
  static show(){        //应用 static 关键字定义静态要领
    console.log("Static function!");
  }
}

var p = new Point(1,4);
console.log(p+"");               //(1,4)
console.log(typeof Point);       //"function"
console.log(Point.prototype.constructor === Point);    //true
console.log(Point.prototype.constructor === p.constructor);    //true
Point.show();      //"Static function!"

相当于传统写法:

function Point(x, y){
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function(){
  return `(${this.x},${this.y})`;
}
Point.show = function(){
  console.log("Static function!");
}
var p = new Point(1,4);
console.log(p+"");   //(1,4)

这里不难看出,class 的类名就是 ES5 中的组织函数名,静态要领就定义在其上,而类的实质依然是个函数。而 class 中除了 constructor 是定义的组织函数之外,其他的要领都定义在类的 prototype 上,这都和 ES5 是一致的,这就意味着,ES5 华夏有的那些要领都能够用, 包含但不限于:

  • Object.keys(), Object.assign() 等等
  • 而且 class 也一样支撑表达式做属性名,比方 Symbol
  • ES5 函数具有的属性/要领:length、name、apply、call、bind、arguments 等等

但有些细节照样有区分的,比方:

class Point{
  constructor(x, y){    //定义组织函数
    this.x = x;         //定义属性x
    this.y = y;         //定义属性y
  }                     //这里没有逗号
  toString(){           //定义动态要领,不须要 function 关键字
    return `(${this.x},${this.y})`;
  }
  getX(){
    return this.x;
  }
  getY(){
    return this.y;
  }
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys);        //[]
console.log(ownKeys);     //["constructor", "toString", "getX", "getY"]
console.log(p.hasOwnProperty("toString"));                  //false
console.log(p.__proto__.hasOwnProperty("toString"));        //true
//ES5
function Point(x, y){
  this.x = x;
  this.y = y;
}
Point.prototype = {
  toString(){
    return `(${this.x},${this.y})`;
  },
  getX(){
    return this.x;
  },
  getY(){
    return this.y;
  }
}
var p = new Point(1,4);
var keys = Object.keys(Point.prototype);
var ownKeys = Object.getOwnPropertyNames(Point.prototype);
console.log(keys);        //["toString", "getX", "getY"]
console.log(ownKeys);     //["toString", "getX", "getY"]
console.log(p.hasOwnProperty("toString"));                  //false
console.log(p.__proto__.hasOwnProperty("toString"));        //true

这个例子说明,class 中定义的动态要领是不可枚举的,而且 constructor 也是其自有要领中的一个。

运用 class 注重一下几点:

  • class 中默许是严厉形式,纵然不写"use strict。关于严厉形式能够看:Javascript基本(2) – 严厉形式特征
  • 同名 class 不可反复声明
  • class 相当于 object 而不是 map,不具有 map 属性,也不具有默许的 Iterator。
  • constructor 要领在 class 中是必需的,假如没有以为指定,体系会默许天生一个空的 constructor
  • 挪用 class 定义的类必需有 new 关键字,像一般函数那样挪用会报错。ES5 不限定这一点。
TypeError: Class constructor Point cannot be invoked without 'new'
  • constructor 要领默许返回值为 this,能够以为修正返回其他的值,但这会致使一系列新鲜的题目:
class Point{
  constructor(x,y){
    return [x, y];
  }
}
new Point() instanceof Point;    //false
  • class 声明类不存在变量提拔
new Point();     //ReferenceError: Point is not defined
class Point{}

class 表达式

这个和面向对象不一样了,js 中函数能够有函数声明情势和函数表达式2种体式格局定义,那末 class 一样有第二种2种定义体式格局:class 表达式

var className1 = class innerName{
  //...
};
let className2 = class innerName{
  //...
};
const className3 = class innerName{
  //...
};

class 表达式由许多特征和 ES5 一样:

  • 和函数表达式相似,这里的innerName能够省略,而且innerName只要类内部可见,现实的类名是赋值号前面的 className。
  • 如许定义的类的作用域,由其所在位置和声明关键字(var, let, const)决议
  • const说明的类是个常量,不能修正。
  • 其变量声明存在提拔,但初始化不提拔
  • class 表达式也不能和 class 说明重名

ES5 中有马上实行函数,相似的,这里也有马上实行类:

var p = new class {
  constructor(x, y){
    this.x = x;
    this.y = y;
  }
  toString(){
    return `(${this.x},${this.y})`;
  }
}(1,5);   //马上天生一个对象
console.log(p+"");    //(1,5)

getter, setter 和 Generator 要领

getter 和 setter 运用体式格局和 ES5 一样, 这里不多说了,举个例子一看就懂:

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}
var p = new Person("Bob", 18, "13211223344");
console.log(p.id);                //undefined
p.id = '30010219900101009X';
console.log(p.id);                //'30010219900101009X'

var descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'id');
console.log('set' in descriptor);       //true
console.log('get' in descriptor);       //true

p.id = '110';                     //TypeError: Id is read-only

Generator 用法也和 ES6 Generator 部份一样:

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  *[Symbol.iterator](){
    var keys = Object.keys(this);
    keys = keys.filter(function(item){
      if(/^_/.test(item)) return false;
      else return true;
    });
    for(let item of keys){
      yield this[item];
    }
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}
var p = new Person("Bob", 18, "13211223344");
p.id = '30010219900101009X';
for(let info of p){
  console.log(info);   //顺次输出: "Bob", 18, "13211223344"
}

class 的继续

这里我们只重点讲继续,关于多态没有新的修正,和 ES5 中一样,在函数内推断参数即可。关于多态能够浏览Javascript对象、类与原型链中关于多态重构的部份。

另外,class 继续属于 ES5 中多种继续体式格局的同享原型,关于同享原型也在上面这篇文章中解说过。

class 完成继续能够简朴的经由过程 extends 关键字完成, 而运用 super 关键字挪用父类要领:

//定义 '有色点'' 继续自 '点'
class ColorPoint extends Point{    //这里延用了上面定义的 Point 类
  constructor(x, y, color){
    super(x, y);     //应用 super 函数挪用父类的组织函数
    this.color = color;
  }
  toString(){
    return `${super.toString()},${this.color}`;     //应用 super 挪用父类的动态要领
  }
}
var cp = new ColorPoint(1, 5, '#ff0000');
console.log(cp+"");      //(1,5),#ff0000
ColorPoint.show();       //"Static function!"     静态要领一样被继续了
cp instanceof ColorPoint;   //true
cp instanceof Point;   //true

运用 extends 继续的时刻须要注重一下几点:

  • super 不能零丁运用,不能接见父类属性,只能要领父类要领和组织函数(super自身)
  • 子类没有本身的 this,须要借助 super 挪用父类组织函数后加工获得从父类获得的 this,子类组织函数必需挪用 super 函数。这一点和 ES5 完整差别。
  • 子类假如没有手动定义组织函数,会自动天生一个组织函数,以下:
constructor(...args){
  super(...args);
}
  • 子类中运用 this 关键字之前,必需先挪用 super 组织函数
  • 由于继续属于同享原型的体式格局,所以不要在实例对象上修正原型(Object.setPrototypeOf, obj.__proto__等)
  • super 也能够用在一般是对象字面量中:
var obj = {
  toString(){
    return `MyObj ${super.toString()}`;
  }
}
console.log(obj+"");    //MyObj [object Object]

prototype__proto__

在 class 的继续中

  • 子类的 __proto__ 指向其父类
  • 子类 prototype 的 __proto__ 指向其父类的 prototype
class Point{
  constructor(x, y){
    this.x = x;
    this.y = y;
  }
}
class ColorPoint extends Point{
  constructor(x, y, color){
    super(x, y);
    this.color = color;
  }
}
ColorPoint.__proto__  === Point;   //true
ColorPoint.prototype.__proto__ === Point.prototype;   //true

其等价的 ES5 是如许的:

function Point(){
  this.x = x;
  this.y = y;
}
function ColorPoint(){
  this.x = x;
  this.y = y;
  this.color = color;
}
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype);    //继续动态要领属性
Object.setPrototypeOf(ColorPoint, Point);                        //继续静态要领属性

ColorPoint.__proto__  === Point;                      //true
ColorPoint.prototype.__proto__ === Point.prototype;   //true

这里我们应当明白一下3种继续的 prototype 和 __proto__

  1. 没有继续

class A{}
A.__proto__  === Function.prototype;          //true
A.prototype.__proto__ === Object.prototype;   //true
  1. 继续自 Object
class A extends Object{}
A.__proto__  === Object;                      //true
A.prototype.__proto__ === Object.prototype;   //true
  1. 继续自 null
class A extends null{}
A.__proto__  === Function.prototype;        //true
A.prototype.__proto__ === undefined;        //true

推断类的继续关联:

class A{}
class B extends A{}
Object.getPrototypeOf(B) === A;     //true

子类的实例的 __proto____proto__ 指向其父类实例的 __proto__

class A{}
class B extends A{}
var a = new A();
var b = new B();
B.__proto__.__proto__ === A.__proto__;        //true

因而,能够经由过程修正子类实例的 __proto__.__proto__ 转变父类实例的行动。发起:

  • 老是用 class 庖代须要 prototype 的操纵。由于 class 的写法更简约,更易于明白。
  • 运用 extends 完成继续,由于如许更简朴,不会有损坏 instanceof 运算的风险。

另外存取器和 Generator 函数都能够很抱负的被继续:

class Person{
  constructor(name, age, tel){
    this.name = name;
    this.age = age;
    this.tel = tel;
    this._self = {};
  }
  *[Symbol.iterator](){
    var keys = Object.keys(this);
    keys = keys.filter(function(item){
      if(/^_/.test(item)) return false;
      else return true;
    });
    for(let item of keys){
      yield this[item];
    }
  }
  get id(){
    return this._self.id;
  }
  set id(str){
    if(this._self.id){
      throw new TypeError("Id is read-only");
    } else {
      this._self.id = str;
    }
  }
}

class Coder extends Person{
  constructor(name, age, tel, lang){
    super(name, age, tel);
    this.lang = lang;
  }
}

var c = new Coder("Bob", 18, "13211223344", "javascript");
c.id = '30010219900101009X';
for(let info of c){
  console.log(info);   //顺次输出: "Bob", 18, "13211223344", "javascript"
}
console.log(c.id);     //'30010219900101009X'
c.id = "110";          //TypeError: Id is read-only

多继续

多继续指的是一个新的类继续自已有的多个类,JavaScript 没有供应多继续的体式格局,所以我们运用 Mixin 形式手动完成:

function mix(...mixins){
  class Mix{}
  for(let mixin of mixins){
    copyProperties(Mix, mixin);                         //继续静态要领属性
    copyProperties(Mix.prototype, mixin.prototype);     //继续动态要领属性
  }

  return Mix;

  function copyProperties(target, source){
    for(let key of Reflect.ownKeys(source)){
      if(key !== 'constructor' && key !== "prototype" && key !== "name"){
        if(Object(source[key]) === source[key]){
          target[key] = {};
          copyProperties(target[key], source[key]);       //递归完成深拷贝
        } else {
          let desc = Object.getOwnPropertyDescriptor(source, key);
          Object.defineProperty(target, key, desc);
        }
      }
    }
  }
}

//运用要领:
class MultiClass extends mix(superClass1, superClass2, /*...*/){
  //...
}

由于 mixin 形式运用了拷贝组织,组织出的子类的父类是 mix 函数返回的 class, 因而 prototype 和 __proto__ 与任一 superClass 都没有直接的联络,instanceof 推断其属于 mix 函数返回类的实例,一样和任一 superClass 都没有关联。能够这么说:我们为了完成功用损坏了理论应当具有的原型链。

原生组织函数继续

在 ES5 中,原生组织函数是不能继续的,包含: Boolean(), Number(), Date(), String(), Object(), Error(), Function(), RegExp()等,比方我们如许完成:

function SubArray(){}
Object.setPrototypeOf(SubArray.prototype, Array.prototype);    //继续动态要领
Object.setPrototypeOf(SubArray, Array);                        //继续静态要领

var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length);     //1  应当是2
arr.length = 0;
console.log(arr);            //[0:5,1:10]  应当为空

很明显这已不是谁人我们熟习的数组了!我们能够用 class 尝尝:

class SubArray extends Array{}
var arr = new SubArray();
arr.push(5);
arr[1] = 10;
console.log(arr.length);     //2
arr.length = 0;
console.log(arr);            //[]

照样熟习的滋味,对吧!这就和之前提到的继续差别有关了,子类没有本身的 this,须要借助 super 挪用父类组织函数后加工获得从父类获得的 this,子类组织函数必需挪用 super 函数。而 ES5 中先天生子类的 this,然后把父类的 this 中的属性要领拷贝过来,我们都晓得,有的属性是不可枚举的,而有的属性是 Symbol 名的,这些属性不能很好的完成拷贝,就会致使题目,比方 Array 组织函数的内部属性 [[DefineOwnProperty]]

应用这个特征,我们能够定义本身的适宜的类, 比方一个新的毛病类:

class ExtendableError extends Error{
  constructor(message){
    super(message);
    this.stack = new Error().stack;
    this.name = this.constructor.name;
  }
}
throw new ExtendableError("test new Error");   //ExtendableError: test new Error

静态属性

为什么静态属性须要零丁写,而静态要领直接简朴带过。由于这是个兼容性题目,现在 ES6 的静态要领用 static 关键字,然则静态属性和 ES5 一样,须要零丁定义:

class A{}
A.staticProperty = "staticProperty";
console.log(A.staticProperty);      //"staticProperty"

不过 ES7 提出能够在 class 内部完成定义,惋惜现在不支撑,然则还好有 babel 支撑:

class A{
  static staticProperty = "staticProperty";   //ES7 静态属性
  instanceProperty = 18;                      //ES7 实例属性
}
console.log(A.staticProperty);                //"staticProperty"
console.log(new A().instanceProperty);        //18

new.target 属性

new 本来是个关键字,但 ES6 给它添加了属性——target。该属性只能在组织函数中运用,用来推断组织函数是不是作为组织函数挪用的, 假如组织函数被 new 挪用返回组织函数自身,不然返回 undefined:

function Person(){
  if(new.target){
    console.log("constructor has called");
  } else {
    console.log("function has called");
  }
}

new Person();     //"constructor has called"
Person();         //"function has called"

如许我们能够完成一个组织函数,只能运用 new 挪用:

function Person(name){
  if(new.target === Person){
    this.name = name;
  } else {
    throw new TypeError("constructor must be called by 'new'");
  }
}

new Person('Bob');     //"constructor has called"
Person();              //TypeError: constructor must be called by 'new'
Person.call({});       //TypeError: constructor must be called by 'new'

这里须要注重:父类组织函数中的 new.target 会在挪用子类组织函数时返回子类,因而运用了该属性的类不该当被实例化,只用于继续,相似于 C++ 中的笼统类。

class Person{
  constructor(name){
    if(new.target === Person){
      this.name = name;
    } else {
      throw new TypeError("constructor must be called by 'new'");
    }
  }
}
class Coder extends Person{}
new Coder('Bob');     //TypeError: constructor must be called by 'new' 这不是我们想要的
//笼统类完成
class Person{
  constructor(name){
    if(new.target === Person){
      throw new TypeError("This class cannot be instantiated");
    }
    this.name = name;
  }
}
class Coder extends Person{}
var c = new Coder('Bob');
console.log(c.name);   //'Bob'
new Person('Bob');     //TypeError: This class cannot be instantiated

关于笼统类这里解释一下,要一个类不能实例化只能继续用什么用?

在继续中发生歧义的缘由有多是继续类继续了基类屡次,从而发生了多个拷贝,即不止一次的经由过程多个途径继续类在内存中创建了基类成员的多份拷贝。笼统类的基本原则是在内存中只要基类成员的一份拷贝。举个例子,一个类叫”动物”,尚有多各种继续自动物,比方”胎生动物”、”卵生动物”,又有多个类继续自哺乳动物, 比方”人”, “猫”, “狗”,这个例子彷佛庞杂了,不过很明显,被实例化的一定是一个个别,比方”人”, “猫”, “狗”。而”胎生动物”,不该当被实例化为一个个别,它仅仅是人类在学问范畴,为了分类人间万物而笼统的一个分类。然则面向对象设想请求我们把共性放在一同以削减代码,因而就有了笼统类。所以胎生动物都邑活动,都能够发出声音,这些就应当是共性放在”胎生动物”类中,而所以动物都邑呼吸,会推陈出新,这些共性就放在动物内里,如许我们就不须要在”人”, “猫”, “狗”如许的详细类中一遍遍的完成这些共有的要领和属性。

    原文作者:Faremax
    原文地址: https://segmentfault.com/a/1190000016286340
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞