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 class
和 interfaces
是相似的,他们不能够被实例化,他们可能包含一些实现或者未实现的方法声明,但是,在抽象类中,你能够声明不是static
、final
的属性,定义不是public
、protected
和private
的具体方法。而使用interfaces
,所有属性自动是public
、static
和final
,所有声明或者定义的方法自动被设置为public
。此外,你只能extend
一个class
,无论他是不是abstract
,但你可以 implements
任意数量的interfaces
。
abstract class 和 interfaces的使用场景:
-
考虑使用abstract class,如果你处于以下情况:
- 你想在几个密切相关的类中共享代码。
- 你希望extend的class具有很多的常见方法和字段,或者需要public以外的其他访问修饰符(例如protected、private)
- 你想要声明non-static或者non-final字段,这使你可以访问和修改其所属对象状态的方法。
-
考虑使用 interfaces,如果你处于以下情况:
- 你希望不相关的类来implement你的interface。
- 你想制定特定数据类型的行为,但不关心谁实现其行为。
- 你想利用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 来访问到这个「私有属性」。
(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
但通过Symbol实现的私有属性有一个bug,我们依然可以通过 Object.getOwnPropertySymbols 来实现对类的私有属性的访问。
(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);
我们会发现以上的实现会存在一些问题:
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
来修饰私有属性/方法,但这种约束只用于类型检查,编译期间有效,对于编译后的代码没有实际的约束效果。这里就不做详细介绍了。
总结
这是我近期的工作当中遇到的疑问的学习分享,在此总结存档,如果能够给同样对这些知识点有困惑的你一些启发或收获,那真是一件令人快乐的事儿~