01.静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo{
static classMethod() {
return 'hello';
}
}
Foo.classMethod()// 'hello'
varfoo=new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。
class Foo{
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar()// hello
上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
父类的静态方法,可以被子类继承。
class Foo{
static classMethod() {
return'hello';
}
}
class Barextends Foo{
}
Bar.classMethod()// 'hello'
上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。
class Foo{
static classMethod() {
return'hello';
}
}
class Bar extends Foo{
static classMethod() {
returnsuper.classMethod() + ', too';
}
}
Bar.classMethod()// "hello, too"
2.属性的新写法
实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。
class IncreasingCounter{
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
returnthis._count;
}
increment() {
this._count++;
}
}
上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。
class IncreasingCounter{
_count=0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
上面代码中,实例属性_count与取值函数value()和increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this。
这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。
class foo{
bar = 'hello';
baz = 'world';
constructor() {
// ...
}
}
上面的代码,一眼就能看出,foo类有两个实例属性,一目了然。另外,写起来也比较简洁。
3.静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
class Foo{
}
Foo.prop=1;
Foo.prop// 1
上面的写法为Foo类定义了一个静态属性prop。
目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。
class MyClass{
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp);// 42
}
}
这个新写法大大方便了静态属性的表达。
// 老写法
class Foo{
// ...
}
Foo.prop = 1;
// 新写法
class Foo{
staticprop = 1;
}
上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好。
4.私有方法和私有属性
现有的解决方案
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
一种做法是在命名上加以区别。
class Widget{
// 公有方法
foo(baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
returnthis.snaf=baz;
}
// ...
}
上面代码中,_bar()方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。
另一种方法就是索性将私有方法移出类,因为类内部的所有方法都是对外可见的。
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) {
returnthis[snaf] = baz;
}
// ...
};
上面代码中,bar和snaf都是Symbol值,一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。
const inst = newmyClass();
Reflect.ownKeys(myClass.prototype)
// [ 'constructor', 'foo', Symbol(bar) ]
上面代码中,Symbol 值的属性名依然可以从类的外部拿到。
私有属性的提案
目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。
class IncreasingCounter{
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
上面代码中,#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错
上面代码在类的外部,读取私有属性,就会报错。
下面是另一个例子。
class Point{
#x;
constructor(x=0) {
this.#x = +x;
}
get x() {
returnthis.#x;
}
set x(value) {
this.#x = +value;
}
}
上面代码中,#x就是私有属性,在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#x和x是两个不同的属性。
之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator。
这种写法不仅可以写私有属性,还可以用来写私有方法。
class Foo{
#a;
#b;
const ructor(a,b) {
this.#a = a;
this.#b = b;
}
#sum() {
return this.#a + this.#b;
}
printSum() {
console.log(this.#sum());
}
}
上面代码中,#sum()就是一个私有方法。
另外,私有属性也可以设置 getter 和 setter 方法。
class Counter{
#xValue = 0;
const ructor() {
super();
// ...
}
get #x() { return #xValue; }
set #x(value) {
this.#xValue = value;
}
}
上面代码中,#x是一个私有属性,它的读写都通过get #x()和set #x()来完成。
私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。
class Foo{
#privateValue = 42;
staticgetPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(newFoo());// 42
上面代码允许从实例foo上面引用私有属性。
私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。
class FakeMath{
staticPI = 22/7;
static #totallyRandomNumber = 4;
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}
staticrandom() {
console.log('I heard you like random numbers…')
returnFakeMath.#computeRandomNumber();
}
}
FakeMath.PI// 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
上面代码中,#totallyRandomNumber是私有属性,#computeRandomNumber()是私有方法,只能在FakeMath这个类的内部调用,外部调用就会报错。
in 运算符
try...catch结构可以用来判断是否存在某个私有属性。
class A{
use(obj) {
try{
obj.#foo;
}catch{
// 私有属性 #foo 不存在
}
}
}
consta=newA();
a.use(a);// 报错
上面示例中,类A并不存在私有属性#foo,所以try...catch报错了。
这样的写法很麻烦,可读性很差,V8 引擎改进了in运算符,使它也可以用来判断私有属性。
class A{
use(obj) {
if(#foo in obj) {
// 私有属性 #foo 存在
}else{
// 私有属性 #foo 不存在
}
}
}
上面示例中,in运算符判断当前类A的实例,是否有私有属性#foo,如果有返回true,否则返回false。
in也可以跟this一起配合使用。
class A{
#foo = 0;
m() {
console.log(#foo in this); // true
console.log(#bar in this); // false
}
}
注意,判断私有属性时,in只能用在定义该私有属性的类的内部。
class A{
#foo = 0;
statictest(obj) {
console.log(#foo in obj);
}
}
A.test(newA())// true
A.test({})// false
class B{
#foo = 0;
}
A.test(newB())// false
上面示例中,类A的私有属性#foo,只能在类A内部使用in运算符判断,而且只对A的实例返回true,对于其他对象都返回false。
子类从父类继承的私有属性,也可以使用in运算符来判断。
class A{
#foo = 0;
statictest(obj) {
console.log(#foo in obj);
}
}
classSubAextendA{};
A.test(newSubA())// true
上面示例中,SubA从父类继承了私有属性#foo,in运算符也有效。
注意,in运算符对于Object.create()、Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性。
class A{
#foo = 0;
statictest(obj) {
console.log(#foo in obj);
}
}
consta= new A();
consto1=Object.create(a);
A.test(o1)// false
A.test(o1.__proto__)// true
const o2={};
Object.setPrototypeOf(o2,A);
A.test(o2)// false
A.test(o2.__proto__)// true
上面示例中,对于修改原型链形成的继承,子类都取不到父类的私有属性,所以in运算符无效。
5.new.target 属性
new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if(new.target!==undefined) {
this.name=name;
}else{
thrownewError('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if(new.target===Person) {
this.name=name;
}else{
thrownewError('必须使用 new 命令生成实例');
}
}
varperson=newPerson('张三');// 正确
varnotAPerson=Person.call(person,'张三');// 报错
上面代码确保构造函数只能通过new命令调用。
Class 内部调用new.target,返回当前 Class。
class Rectangle{
const ructor(length,width) {
console.log(new.target===Rectangle);
this.length=length;
this.width=width;
}
}
varobj=newRectangle(3,4);// 输出 true
需要注意的是,子类继承父类时,new.target会返回子类。
class Rectangle{
const ructor(length,width) {
console.log(new.target===Rectangle);
// ...
}
}
class SquareextendsRectangle{
const ructor(length,width) {
super(length,width);
}
}
varobj=newSquare(3);// 输出 false
上面代码中,new.target会返回子类。
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape{
const ructor() {
if(new.target===Shape) {
thrownewError('本类不能实例化');
}
}
}
class RectangleextendsShape{
const ructor(length,width) {
super();
// ...
}
}
varx=newShape();// 报错
vary=newRectangle(3,4);// 正确
上面代码中,Shape类不能被实例化,只能用于继承。
注意,在函数外部,使用new.target会报错。