静态属性和静态方法
我们可以把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"
。这样的方法被称为 静态的(static)。
在一个类中,它们以 static
关键字开头,如下所示:
class User {
static staticMethod() {
console.log(this === User);
}
}
User.staticMethod(); // true
这实际上跟直接将其作为属性赋值的作用相同:
class User { }
User.staticMethod = function () {
console.log(this === User);
};
User.staticMethod(); // true
在 User.staticMethod()
调用中的 this
的值是类构造器 User
自身(“点符号前面的对象”规则)。
通常,静态方法用于实现属于该类但不属于该类任何特定对象的函数。
例如,我们有对象 Article
,并且需要一个方法来比较它们。一个自然的解决方案就是添加 Article.compare
方法,像下面这样:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static compare(articleA, articleB) {
return articleA.date - articleB.date;
}
}
// 用法
let articles = [
new Article("HTML", new Date(2019, 1, 1)),
new Article("CSS", new Date(2019, 0, 1)),
new Article("JavaScript", new Date(2019, 11, 1))
];
articles.sort(Article.compare);
console.log(articles[0].title); // CSS
这里 Article.compare
代表“上面的”文章,意思是比较它们。它不是文章的方法,而是整个 class 的方法。
另一个例子是所谓的“工厂”方法。想象一下,我们需要通过几种方法来创建一个文章:
- 通过用给定的参数来创建(
title
,date
等)。 - 使用今天的日期来创建一个空的文章。
- ……其它方法。
第一种方法我们可以通过 constructor 来实现。对于第二种方式,我们可以创建类的一个静态方法来实现。
就像这里的 Article.createTodays()
:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// 记住 this = Article
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
console.log(article.title); // Today's digest
现在,每当我们需要创建一个今天的文章时,我们就可以调用 Article.createTodays()
。再说明一次,它不是一个文章的方法,而是整个 class 的方法。
静态方法也被用于与数据库相关的公共类,可以用于搜索/保存/删除数据库中的条目, 就像这样:
// 假定 Article 是一个用来管理文章的特殊类
// 静态方法用于移除文章:
Article.remove({id: 12345});
静态属性
A recent addition
This is a recent addition to the language. Examples work in the recent Chrome.
静态的属性也是可能的,它们看起来就像常规的类属性,但前面加有 static
:
class Article {
static publisher = "Cheny";
}
console.log(Article.publisher); // Cheny
这等同于直接给 Article
赋值:
Article.publisher = "Cheny";
继承静态属性和方法
静态属性和方法是可被继承的。
例如,下面这段代码中的 Animal.compare
和 Animal.planet
是可被继承的,可以通过 Rabbit.compare
和 Rabbit.planet
来访问:
class Animal {
static planet = "Earth";
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
console.log(`${this.name} runs with speed ${this.speed}.`);
}
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}
}
// 继承于 Animal
class Rabbit extends Animal {
hide() {
console.log(`${this.name} hides!`);
}
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
rabbits[0].run(); // Black Rabbit runs with speed 5.
console.log(Rabbit.planet); // Earth
现在我们调用 Rabbit.compare
时,继承的 Animal.compare
将会被调用。
它是如何工作的?再次,使用原型。你可能已经猜到了,extends
让 Rabbit
的 [[Prototype]]
指向了 Animal
。
所以,Rabbit extends Animal
创建了两个 [[Prototype]]
引用:
Rabbit
函数原型继承自Animal
函数。Rabbit.prototype
原型继承自Animal.prototype
。
结果就是,继承对常规方法和静态方法都有效。
这里,让我们通过代码来检验一下:
class Animal { }
class Rabbit extends Animal { }
// 对于静态的
console.log(Rabbit.__proto__ === Animal); // true
// 对于常规方法
console.log(Rabbit.prototype.__proto__ === Animal.prototype); // true
总结
先来复习一下原型链的知识,因为这一节的静态属性和方法,使用extends也能继承到,所以先来回顾一下这块的内容。可以参考一下之前的文章。原型链
原型链
在js中,所有的对象都有一个隐藏的
[[Prototype]]
属性,它要么是一个对象,要么就是null(最顶层就是一个null)。通过
[[Prototype]]
引用的对象,被称之为“原型”。__proto__
属于[[Prototype]]
的getter/setter。__proto__
并不是语言本身的特性,是各大厂商具体实现时添加的私有属性,虽然目前很多浏览器的js引擎都提供了这个私有属性,但依旧不建议在生产环境中使用该属性(但是使用起来很方便,自己取舍吧)避免对环境产生依赖。生产环境下,我们可以使用Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法和属性。如果我们想要读取obj的一个属性或者调用一个方法,并且它不存在,那么js就会尝试在原型中查找它。
优先从自己身上查找。
jslet o1 = { name: 'aaa', sayHi: function () { console.log(`hello i am ${this.name}`); } } let o2 = { name: 'bbb', __proto__: o1 } o1.sayHi() // hello i am aaa o2.sayHi() // hello i am bbb // 会优先从自己身上找,找不到才会去原型上找
假如把o2的name注释掉,就会去原型上找
jslet o1 = { name: 'aaa', sayHi: function () { console.log(`hello i am ${this.name}`); } } let o2 = { // name: 'bbb', __proto__: o1 } o1.sayHi() // hello i am aaa o2.sayHi() // hello i am aaa // 会优先从自己身上找,找不到才会去原型上找
再看一个new的情况,也是优先从自己身上找,找不到就去原型上找
jsfunction A(name) { this.name = name } A.prototype.sayHi = function () { console.log(`hello i am ${this.name} and i am ${this.age} years old`); } let o1 = new A('o1') o1.age = 20 // 优先找自己的 A.prototype.age = 18 // 找不到才会从原型上找 o1.sayHi() // hello i am o1 and i am 20 years old
原型的最顶部是null
jsconsole.log(Object.prototype.__proto__ === null); // true // 构造方法都是通过Function new出来的 console.log(Object.__proto__ === Function.prototype); // true // 所有对象的prototype其实就是一个对象,它的原型指向Object.prototype console.log(Function.prototype.__proto__ === Object.prototype); // true // 构造方法都是通过Function new出来的 console.log(Function.__proto__ === Function.prototype); // true
构造方法的原型上都有个constructor属性,该属性指向它自己。
new出来的对象也有个constructor属性,该属性指向它的构造方法。
jsconsole.log(Object.prototype.constructor === Object); // true // 构造方法的原型上都有个constructor属性,该属性指向它自己。 console.log(Function.prototype.constructor === Function); // true // 构造方法的原型上都有个constructor属性,该属性指向它自己。 function A() { } let o1 = new A() console.log(o1.constructor === A); // true // new出来的对象有一个constructor属性,指向构造方法 console.log(o1.__proto__ === A.prototype); // true // new出来的对象的原型执行构造方法的prototype console.log(A.__proto__ === Function.prototype); // true // 所有通过function关键词声明的方法,它的原型都指向 Function.prototype
类的属性和静态方法
回顾完原型链,再来总结一下本节学的内容。
使用class声明类的时候,可以直接使用static关键词声明静态属性和方法,这种声明的属性和方法,实际上是直接挂载到类上的,类其实就是一个函数,函数其实也就是一个对象,所以在对象上挂载点属性和方法,没啥毛病,调用的时候直接使用
类名.xxx
的形式调用,跟正常访问对象上的方法和属性一模一样。jsclass A { static name = 'cheny' static sayHi() { console.log(`hello i am ${this.name}`); } } console.log(A.name); // cheny A.sayHi() // hello i am cheny
静态属性和类字段的区别,类字段其实就是相当于在构造方法上声明了
this.xxx = 'xxx'
,然后使用new声明一个对象的时候,这个类字段直接被挂载到了new出来的对象上。jsclass A { static name = 'cheny' static sayHi() { console.log(`hello i am ${this.name}`); } // 类字段 name2 = 'xzz' // 类字段其实就是类似直接在构造方法上声明了下面的代码 // this.name2 = 'xzz' } console.log(A.name); // cheny console.log(A.name2); // undefined A.sayHi() // hello i am cheny let o1 = new A() // name2是类字段 console.log(o1.name2); // xzz
静态属性和方法在使用extends关键字时,都会继承过来,因为extends把派生类的
__proto__
指向了父类,所以在访问父类的静态方法时,会顺着原型链往上找。jsclass A { static name111 = 'cheny' static sayHi() { console.log(`hello i am ${this.name}`); } } class B extends A { } console.log(B.name111); // cheny console.log(B.sayHi === A.sayHi); // true console.log(B.__proto__ === A); // true // extends把派生类的 原型指向了父类,所以静态属性和方法都继承过来了 let o1 = new B() o1.sayHi() // TypeError: o1.sayHi is not a function // 报错,静态方法只能通过类名调用