class类

78 阅读12分钟

1_class的由来与使用

class是es6提供的一种新的写法,其目的只是让js的写法更加倾向于传统的,面向对象的写法罢了。

js构造函数

 //构造函数
 function Point (x , y){
     this.x = x;
     this.y = y;
 }
 //给构造函数原型上添加方法
 point.prototype.toString = function(){
     return '(' + this.x + ',' + this.y + ')';
 }
 ​
 let p = new Point('x','y');
 //对象p上有x,y属性,其原型上有toString方法

通过class类实现上述代码

 class Point {
     constructor(x,y){
         this.x = x;
         this.y = y;
     }
     toString(){
         return '(' + this.x + ',' + this.y + ')';
     }
 }
 let p = new Point('a','b');
 ​
 p.toString();//(a,b)
 ​
 typeOf Point === 'function'//true
 ​
 Point === Point.prototype.constructor;//类本身指向构造函数
  • 上述定义了一个 构造函数constructor,和 toString方法;并新建一个p实例对象

    ES6的类,完全可以看成构造函数的另一种写法。

class类的原型

  • 每一个定义在类里面的方法,都放在了类的prototype里面。每一个由类新建的对象,它的prototype指向类的prototype。
  • 使用时:p.toString() === Point.prototype.toString()
  • 类上的cunstructor属性直接指向类本身(Point.prototype.cunstructor === Point)

不可枚举

  • 类上的方法是不可枚举的(即类的属性不可以遍历出来)
 class Point {
     cunstructor(){}
     toString(){}
 }
 Object.keys(Point.prototype) //枚举实例对象自身的属性
 //[]
 Object.getOwnPropertyNames(Point.prototype) //枚举自身对象的属性名(包括不可枚举的)
 //['constructor','toString']
  • ES5写法(es5添加在prototype上的的方法是可以枚举的)
 var Point = function(){} 
 ​
 Point.prototype.toString(){}
 ​
 Object.keys(Point.prototype)
 //['toString']
 Object.getOwnPropertyNames(Point.prototype)
 //['constructor','toString']

2_constructor方法与实例

cunstructor方法

  • cunstructor是类的默认方法,会在new命令的时候自动调用这个方法。而每个类都必须要有constructor方法,如果没有显示定义的话,就会自动生成一个空的constructor方法,并默认返回实例对象this(也可以指定返回另一个对象)
 class Foo(){
     constructor(){
         return Object.create(null)
     }
 }
 ​
 new Foo() instanceof Foo
 //false
 //说明后者类的prototype 不在 前者实例对象上的原型链
 //object instanceof constructor 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

实例对象

实例上的属性,一般都定义在原型上(即定义在class上),除非显式定义在其本身上的(即定义在this上)、

 class Point{
     constructor(x,y){
         this.x = x;
         this.y = y;
     }
     toString(){
         console.log('toString')
     }
 }
 ​
 let p = new Point(1,2)
 ​
 p.hasOwnProperty('x');//true
 p.hasOwnProperty('y');//true
 p.hasOwnProperty('toString');//false
 p.__proto__.hasOwnProperty('toString');//true
 ​
 //x和y是显示定义在实例对象上的,是实例对象自身的属性
 //而toString属性是定义在原型上的:p.__proto__ === Point.prototype

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

 class Point{
     constructor(x,y){
         this.x = x;
         this.y = y;
     }
     toString(){
         console.log('toString')
     }
 }
 ​
 let p1 = new Point('1','2');
 let p2 = new Point('3','4');
 let p3 = new Point('5','6');
 ​
 p1.__proto__ === p2.__proto__ === p3.__proto__ //true
 //因为实例上的__proto__都指向Point.prototype,所以三者相等
 //由此也可得,可以通过修改实例上的__proto__,从而更改到原型上的prototype

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

3_取值、存值、表达式

取值函数(getter)和存值函数(setter)

对某个属性设置存值函数和取值函数,拦截该属性的存取行为

  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'

属性表达式

类里面的属性名,还可以写成表达式;需要给表达式添加中括号[ ]

let methodName = 'toString';

class Point {
    constructor(){}
    
    [methodName](){}
}

class表达式

类也可以和函数一样,用表达式的形式定义。

const MyClass = class Me{
    constructor(){}
    
    getClassName(){
        return Me.name
    }
}

let m = new MyClass();
m.getClassName();//Me
Me.name;//Me is not defined

//上面代码用表达式定义了一个叫MyClass的类,而Me不是定义的类名,Me只在class内部的代码里起作用

class表达式可以写出立即执行的class

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

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

person.sayName()//'张三'

4_注意点

不存在变量提升

类不存在变量提升

let p = new Point()
class Foo{}
//报错
//es6不会把类的定义提到使用前,原因与继承有关,保证子类在父类之后定义。
{
    let Foo = class{}
    class Bar extends Foo{} 
}
//上述不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

name属性

class Point {}
Point.name//'Point'
//由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
//name属性总是返回紧跟在class关键字后面的类名

generator方法(todo)

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

this指向

类的方法内部如果有this,他会默认指向实例。但单独使用该方法容易报错

class Logger{
    printName(name = 'there'){
        this.print(`hello ${name}`)
    }
    print(text){
		console.log(text)
    }
}

const  logger = new Logger();//创建一个实例
const {printName} = logger;//将logger里面的printName函数赋值
printName();//报错
logger.printName()//正常执行:hello there

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
      //这里将printName方法添加到实例上,同时绑定了this的指向,为创建的实例
  }
 
  // ...
}

另一种解决方法是使用箭头函数。因为使用箭头函数使,函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }

  // ...
}

5_静态方法和实例属性的新方法

静态方法(static)

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

静态方法会被直接定义在类上,而不是类的prototype上

class Foo{
    static classMethod(){
        return 'Hello'
    }
}

Foo.calssMethod();//hello

let f = new Foo()
f.classMethod()//报错:f.classMethod is not a function

如果静态方法包含this关键字,这个this指的是类,而不是实例。

class Foo{
    static bar(){
        this.baz();
    }
    static baz(){
        console.log('static baz')
    }
    baz(){
        console.log('baz')
    }
}
Foo.bar()//'static baz'

父类的静态方法,可以被子类继承。

class Foo{
    static classMethod(){
        return 'Hello'
    }
}
class child extends Foo{}

child.classMethod()//'Hello'

静态方法也是可以从super对象上调用的。

class Foo{
    static classMethod(){
        return 'Hello'
    }
}
class child extends Foo{
    static classMethod(){
        return super.classMethid()+',too'
    }
}

child.classMethod()//'Hello'

实例属性的新方法

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

class Foo {
    count = 0;//直接写在类的最顶层
	increment(){
        this.count++;
    }
}
//因为属性count和函数increment处于同一层级,所以定义的时候不需要写成this.count = 0

这种写法好处是能让实例上的属性定义起来清晰明了,一目了然,也比较简洁

6_静态属性、私有方法、new.target属性

静态属性

class Foo{}
Foo.prop = 1;
console.log(Foo.prop);//1

//新写法
class Foo{
    static prop = 1
}
console.log(Foo.prop);//1

静态属性是直接定义在类上面的属性,而非在prototype上,实例上也不会有这个属性

私有方法和私有属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。

在命名上加以区分

class Widget {
  // 公有方法
  foo (baz) {
    this._bar(baz);
  }
  // 私有方法:将私有的方法名字前添加下划线
  _bar(baz) {
    return this.snaf = baz;
  }
  // ...
}

这种命名是不保险的,在类的外部,还是可以调用到这个方法。

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }
 
  // ...
}
 
function bar(baz) {
  return this.snaf = baz;
}
//foo是公开的方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

const bar = Symbol('bar');
const snaf = Symbol('snaf');

export default class myClass{

  // 公有方法
  foo(baz) {
    this[bar](baz);
  }

  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }

  // ...
};

barsnaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。

但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

const inst = new myClass();
 
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
//静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。

new.target属性

ES6为new命令引入的新的属性,返回的是new命令作用于的那个构造函数;如果构造函数没有通过new命令调用,则会返回undefined。所以这个属性可以用来确定构造函数是怎么被调用的

//构造函数的写法
function Person(name) {
    if (new.target !== undefined) {
        //通过new调用
        this.name = name
        console.log('通过new调用');
    } else {
        throw new Error('没有使用new命令调用')
    }
}

let person = new Person('name')//'通过new调用'
let person2 = Person('name')//报错


//判断条件也可以写成
if(new.target === Person)
  • 在class类里面,则是在constructor函数里面可以使用new.target属性
 class Person {
     constructor(x, y) {
         if (new.target === Person) {
             console.log('true');
         } else {
             throw new Error('false')
         }
     }
 }

 let person1 = new Person();//'true'
 let person2 = Person();// 'Class constructor Person cannot be invoked without 'new''

子类继承父类的时候,new.target会返回子类

class Person {
    constructor(x, y) {
        if (new.target === Person) {
            console.log('true');
        } else {
            // throw new Error('false')
        }
    }
}
class Child extends Person {
    constructor(x, y) {
        super(x, y)
        console.log(new.target);
    }
}

let person1 = new Person()
let child1 = new Child()
//会打印Child类

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

当父类的new.target属性是其本身的时候,抛出错误

class Person {
    constructor(x, y) {
        if (new.target === Person) {
            throw new Error('本类不可以实例化')
        } else {
            //
        }
    }
}
class Child extends Person {
    constructor(x, y) {
        super()
        console.log(new.target);
    }
}

let person1 = new Person()//报错
let child1 = new Child()
console.log(child1);

7_继承简介和Object.getPrototypeOf()

继承简介

子类在继承父类的时候,如果没重新定义constructor函数,则默认是父类的constructor函数;

如果要重新定义constructor函数,则需要在函数内调用super方法,先通过父类加工出子类的this,然后使子类的this拥有和父类一样的实例属性与方法,然后加上子类自己的实例属性和方法。

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

es5和es6区别

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

Object.getPrototypeOf

该方法用来获取子类的父类

class Person{}

class Child extends Person;

Object.getPrototypeOf(Child);//Person

8_继承之super

super可以当作函数使用,也可以当作对象使用

super()函数

super关键字作为函数被调用的时候,表示的是父类的构造函数

当class类通过new关键字调用时,class内部会调用new.target属性,返回当前的class;而子类继承父类的时候,也会调用new.target属性,此时返回的是子类。

也能说,super()相当于A.prototype.constructor.call(this),(A指父类)

(作为函数的时候,super只能在构造函数里调用)

super对象

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

class A{
    p(){
        console.log('p')
    }
}

class B extends A {
	constructor(){
        super();
        super.p()//这里的super指的是父类的prototype
    }
}

需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A{
    constructor(){
        this.a = '1'//这里是给实例添加属性
    }
}

class B extends A {
    get m(){ //这里对m方法添加get,即获取到的时候会调用
        return super.a;  //这里尝试返回super.a,但由于a是实例上的属性,而不是原型prototype上的属性,所以没法访问
    }
}

let b = new B()
b.m//undefined
class A{
    constructor(){}
}
A.prototype.x = 2;//这里将属性定义在了原型上,这时候super就可以访问到

子类普通方法里调用super的方法的this指向

ES6 规定,在子类普通方法中通过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'
//实际上执行的是A.prototype.print.call(this)

所以通过super去给某个属性赋值的时候,super就是this,赋值的属性会变成子类实例的属性

class A{
    constructor(){
        this.x = 1;
    }
}
class B extends A{
    constructor(){
        super()
        this.x = 2;  //修改实例上的属性
        console.log(this.x);  //2
        super.x = 3;  //相当于this.x=3,修改了实例上的属性
        console.log(this.x);  //3
        console.log(super.x);  //undefined,因为super.x相当于读取的是A.prototype.x,所以返回的是undefined
    }
}
let b =new B()

*如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

super需要显示指定

//super需要显示指定为对象或者是函数,否则会报错
class A{}
class B extends A{
    constructor(){
        super();
        console.log(super)//会报错,因为没法看出是对象还是函数,所以一定要写成super()或者super.xx
    }
}
class A{}
class B extends A{
    constructor(){
        super();
        console.log(super.valueOf() instanceof B);//true
        //这里的super可以看出来是对象,其中 valueOf() 方法可返回 Boolean 对象的原始值;所以super.valueOf()返回的是一个B的实例
    }
}

由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。

let obj = {
    toString(){
        return super.toString(); 
    }
}
obj.toString();[object object]

9_类的prototype属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链

  • (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
  • (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A{}
class B extends A{}

B.__proto__ === A //true
B.prototype.__proto__  === A.prototype //true

上面代码中,子类B__proto__属性指向父类A,子类Bprototype属性的__proto__属性指向父类Aprototype属性。

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

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

\