关于Class,我的一些新收获

458 阅读6分钟

1. extends 和 implements

extends 用于扩展一个类,implements 用于实现接口。

一个接口和一个普通类的的区别在于,接口中只是做一些接口方法声明,你不需要实现声明的任何方法,implements这个接口的class必须实现这些方法。简单的理解接口相当于定义一个类的类型约束,接口通常用来定义实现类的外观,也就是实现类的行为定义。代码示例:

implements interface:

interface ExampleInterface {
  doAction: () => void;
  doThis: (v: number) => string;
}

class Sub implements ExampleInterface {
  // 必须实现
  doAction() {

  }
  // 必须实现
  doThis(val: number) {
    return '';
  }

  // 可以实现接口类定义方法的基础上增加别的实现
  doThat() {
    return '';
  }
}

extends class:

class SuperClass {
  saying() {
    console.log('saying----');
  }
  speaking() {
    console.log('speaking----');
  }
}

class ChildClass extends SuperClass {
  speaking() {
    console.log('child speaking----');
  }
  dancing() {
    console.log('child dancing----');
  }
}

const s: SuperClass = new SuperClass();
s.saying(); // saying----
s.speaking(); // speaking----

const child: ChildClass = new ChildClass();
child.saying(); // saying----
child.speaking(); // child speaking----

// 在继承的基础上充血
class ChildClass1 extends SuperClass {
  speaking() {
    super.speaking();
    console.log('ChildClass1 speaking-----')
  }
  dancing() {
    console.log('child dancing----');
  }
}
const child2 = new ChildClass1();
child2.speaking(); 
// speaking---- 
// ChildClass1 speaking-----

2. class 和 abstract class

抽象类是被声明为 abstract的类,它可能包含也可能不包含抽象方法,抽象类不能被实例化,但它们可以被子类继承。

抽象方法是在没有实现的情况下声明的方法,示例如下:

abstract moveTo: (deltaX: number, deltaY: number) => void;

如果一个类包含抽象方法,这个类本身必须被声明为抽象类,例如:


abstract class MuziClass {
  abstract study: () =>void;
}

当抽象类被子类继承时,子类通常会提供父类的所有抽象方法的实现,如果不是,则子类也必须声明为 abstract class

需要注意的是,interfaces中未声明为default或者 static的方法是隐式抽象的,因此抽象修饰符不与interface定义的方法一起使用。

3. abstract class 和 interfaces 的区别

abstract classinterfaces是相似的,他们不能够被实例化,他们可能包含一些实现或者未实现的方法声明,但是,在抽象类中,你能够声明不是staticfinal的属性,定义不是publicprotectedprivate的具体方法。而使用interfaces,所有属性自动是publicstaticfinal,所有声明或者定义的方法自动被设置为public。此外,你只能extend一个class,无论他是不是abstract,但你可以 implements 任意数量的interfaces

abstract class 和 interfaces的使用场景:

  • 考虑使用abstract class,如果你处于以下情况:

    1. 你想在几个密切相关的类中共享代码。
    2. 你希望extend的class具有很多的常见方法和字段,或者需要public以外的其他访问修饰符(例如protected、private)
    3. 你想要声明non-static或者non-final字段,这使你可以访问和修改其所属对象状态的方法。
  • 考虑使用 interfaces,如果你处于以下情况:

    1. 你希望不相关的类来implement你的interface。
    2. 你想制定特定数据类型的行为,但不关心谁实现其行为。
    3. 你想利用interface实现多重继承。

4. 私有属性的实现

日常开发中,处于代码的稳定性和可控性的考虑,我们会需要定义一些仅内部可访问的私有属性,大家通常会用哪种方式实现私有属性呢?这里梳理一些定义私有属性的常见方式分享给大家。

(1)_xxx

最常见的私有属性的定义方式,是以"_"开头定义私有属性。

class Muzishuiji {
    constructor() {
        this._creator = "muzi"
    }
    getCreator() {
        return this._creator;
    }
}
const person = new Muzishuiji();
console.log(person.getCreator()); // muzi

但是这种定义方式,只是一种语义上的规范,我们依然可以通过访问 person._creator 来访问到这个「私有属性」。

image.png

(2) Proxy or Object.efineProperty

我们可以使用Proxy or Object.efineProperty来对"_"开头定义的私有属性做一层访问隔离,从而实现“私有”。

// Proxy

class Muzishuiji {
  constructor() {
    this._creator = "muzi"
  }
  getCreator() {
    return this._creator;
  }
}
const person = new Muzishuiji();
const handler = {
  get(target, prop) {
    if(prop.startsWith('_')) {
      return undefined;
    }
    return target[prop];
  },
  set(target, prop, value) {
    if (prop.startsWith('_')) {
      return;
    }
    target[prop] = value;
  },
  // 过滤Object.keys的返回值
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
}
const personProxy = new Proxy(person, handler);
console.log(personProxy._creator); // undefined

// Object.defineProperty

Object.defineProperty(person, '_creator', {
  get() {
    return undefined;
  },
  set(newValue) {
    return;
  }
});
console.log(person._creator); // undefined
person._creator = 'sfsdf'; 
console.log(person._creator); // undefined

此方式这样实例级别的私有属性,需要对每个实例化的对象都做一层代理,这样过于繁琐,可以通过Symbol来实现class级别的私有属性。

(3)Symbol

使用Symbol实现的私有属性是借助Symbol可以创建唯一值的特性,外部拿不到对应的属性名,就没办法访问对应的属性值,外部通过暴露出来的get方法访问。

const creatorName = Symbol('creatorName');
const creatorAge = Symbol('creatorAge');

class Creator {
  constructor() {
    this[creatorName] = 'muzishuiji';
    this[creatorAge] = 18
  }
  getName() {
    return this[creatorName];
  }
}
const person = new Creator();
console.log(person.getName());  // muzishuji

image.png

但通过Symbol实现的私有属性有一个bug,我们依然可以通过 Object.getOwnPropertySymbols 来实现对类的私有属性的访问。

image.png

(4)引入第三个对象,借助闭包来实现私有属性

  • 借助普通对象实现
// Creator.ts
const closureObj = {};

class Creator {
  constructor() {
    closureObj.name = "muzishuiji";
    closureObj.age = 18
  }
  getName() {
    return closureObj.name;
  }
  setName() {
    closureObj.name = 'efdssfdfsd'
  }
}
export default Creator;

// instance.ts
import Creator from "./Creator.ts";

const person = new Creator();
person.getName(); 
  • 借助Map对象实现
// Creator.ts
const mapObj = new Map();
class Creator {
  constructor() {
    mapObj.set("name", "muzishuiji");
    mapObj.set("age", 18);
  }
  getName() {
    return mapObj.get("name");
  }

}

module.exports = Creator;

// instance.ts
const Creator = require("./Creator.ts");

const person = new Creator();
console.log(person.getName());
console.log(person.name);
console.log(person.age);

image.png

我们会发现以上的实现会存在一些问题:

a. 多个实例共享同一个对象,如果暴露set方法,实例之间的修改会互相影响;

b. 对象销毁了这个第三个对象依然存在;

我们可以通过weakMap的只能用对象作为key值,对象销毁,这个键值对就销毁的特性来解决这个问题。

a. 因为使用对象作为key的,不同的实例对象放在不同的键值对上,互相没影响。

b. 对象销毁的时候,对应的键值对就销毁,不需要手动触发回收机制。

  • 借助WeakMap对象实现
// Creator.ts
const weakMapName = new WeakMap();
const weakMapAge = new WeakMap();
class Creator {
  constructor() {
    weakMapName.set(this, 'muzishuiji');
    weakMapAge.set(this, '18');
  }
  getName() {
    return weakMapName.get(this);
  }
  setAge(age) {
    weakMapAge.set(this, age)
  }
  getAge() {
    return weakMapAge.get(this)
  }
}
module.exports = Creator;

// instance.ts
const Creator = require("./test.ts");
const person = new Creator();
console.log(person.getName());  // muzishuiji
console.log(person.getAge());   // 18
person.setAge(28)
console.log(person.getAge());   // 28

(5) #xxx

私有属性的es草案,可以通过#的方式来标识私有属性和方法。

// 源代码
class Creator {
    #name;
    #age;
    constructor() {
      this.#name = 'muzishuiji';
      this.#age = 18;
    }
    getName() {
    	return this.#name;
    }
}

const person = new Creator();
console.log(person.getName());
console.log(person.name);
console.log(person.age);

// 通过babel编译后的结果
"use strict";

function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }

function _checkPrivateRedeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } }

function _classPrivateFieldGet(receiver, privateMap) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); return _classApplyDescriptorGet(receiver, descriptor); }

function _classApplyDescriptorGet(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }

function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); _classApplyDescriptorSet(receiver, descriptor, value); return value; }

function _classExtractFieldDescriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); }

function _classApplyDescriptorSet(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } }

var _name = /*#__PURE__*/new WeakMap();

var _age = /*#__PURE__*/new WeakMap();

class Creator {
  constructor() {
    _classPrivateFieldInitSpec(this, _name, {
      writable: true,
      value: void 0
    });

    _classPrivateFieldInitSpec(this, _age, {
      writable: true,
      value: void 0
    });

    _classPrivateFieldSet(this, _name, 'muzishuiji');

    _classPrivateFieldSet(this, _age, 18);
  }

  getName() {
    return _classPrivateFieldGet(this, _name);
  }

}

const person = new Creator(); 
console.log(person.getName());  // muzishuiji
console.log(person.name);       // undefined
console.log(person.age);        // undefined

可以看到,该编译结果对“#”的处理核心也是通过WeakMap对象来实现。只是给WeakMap对象的get和set方法增加了一层包裹,做一些校验。

除了以上方法之外,ts里也可以使用private来修饰私有属性/方法,但这种约束只用于类型检查,编译期间有效,对于编译后的代码没有实际的约束效果。这里就不做详细介绍了。

总结

这是我近期的工作当中遇到的疑问的学习分享,在此总结存档,如果能够给同样对这些知识点有困惑的你一些启发或收获,那真是一件令人快乐的事儿~