Skip to content

静态属性和静态方法

我们可以把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"。这样的方法被称为 静态的(static)

在一个类中,它们以 static 关键字开头,如下所示:

js
class User {
    static staticMethod() {
        console.log(this === User);
    }
}

User.staticMethod(); // true

这实际上跟直接将其作为属性赋值的作用相同:

js
class User { }

User.staticMethod = function () {
    console.log(this === User);
};

User.staticMethod(); // true

User.staticMethod() 调用中的 this 的值是类构造器 User 自身(“点符号前面的对象”规则)。

通常,静态方法用于实现属于该类但不属于该类任何特定对象的函数。

例如,我们有对象 Article,并且需要一个方法来比较它们。一个自然的解决方案就是添加 Article.compare 方法,像下面这样:

js
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 的方法。

另一个例子是所谓的“工厂”方法。想象一下,我们需要通过几种方法来创建一个文章:

  1. 通过用给定的参数来创建(titledate 等)。
  2. 使用今天的日期来创建一个空的文章。
  3. ……其它方法。

第一种方法我们可以通过 constructor 来实现。对于第二种方式,我们可以创建类的一个静态方法来实现。

就像这里的 Article.createTodays()

js
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 的方法。

静态方法也被用于与数据库相关的公共类,可以用于搜索/保存/删除数据库中的条目, 就像这样:

js
// 假定 Article 是一个用来管理文章的特殊类
// 静态方法用于移除文章:
Article.remove({id: 12345});

静态属性

A recent addition

This is a recent addition to the language. Examples work in the recent Chrome.

静态的属性也是可能的,它们看起来就像常规的类属性,但前面加有 static

js
class Article {
    static publisher = "Cheny";
}

console.log(Article.publisher); // Cheny

这等同于直接给 Article 赋值:

js
Article.publisher = "Cheny";

继承静态属性和方法

静态属性和方法是可被继承的。

例如,下面这段代码中的 Animal.compareAnimal.planet 是可被继承的,可以通过 Rabbit.compareRabbit.planet 来访问:

js
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 将会被调用。

它是如何工作的?再次,使用原型。你可能已经猜到了,extendsRabbit[[Prototype]] 指向了 Animal

image-20220314093353966

所以,Rabbit extends Animal 创建了两个 [[Prototype]] 引用:

  1. Rabbit 函数原型继承自 Animal 函数。
  2. Rabbit.prototype 原型继承自 Animal.prototype

结果就是,继承对常规方法和静态方法都有效。

这里,让我们通过代码来检验一下:

js
class Animal { }
class Rabbit extends Animal { }

// 对于静态的
console.log(Rabbit.__proto__ === Animal); // true

// 对于常规方法
console.log(Rabbit.prototype.__proto__ === Animal.prototype); // true

总结

先来复习一下原型链的知识,因为这一节的静态属性和方法,使用extends也能继承到,所以先来回顾一下这块的内容。可以参考一下之前的文章。原型链

原型链

  1. 在js中,所有的对象都有一个隐藏的[[Prototype]]属性,它要么是一个对象,要么就是null(最顶层就是一个null)。

  2. 通过[[Prototype]]引用的对象,被称之为“原型”。

  3. __proto__属于[[Prototype]]的getter/setter。__proto__并不是语言本身的特性,是各大厂商具体实现时添加的私有属性,虽然目前很多浏览器的js引擎都提供了这个私有属性,但依旧不建议在生产环境中使用该属性(但是使用起来很方便,自己取舍吧)避免对环境产生依赖。生产环境下,我们可以使用Object.getPrototypeOf方法来获取实例对象的原型,然后再来为原型添加方法和属性。

  4. 如果我们想要读取obj的一个属性或者调用一个方法,并且它不存在,那么js就会尝试在原型中查找它。

    优先从自己身上查找。

    js
    let 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注释掉,就会去原型上找

    js
    let 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
    // 会优先从自己身上找,找不到才会去原型上找
  5. 再看一个new的情况,也是优先从自己身上找,找不到就去原型上找

    js
    function 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
  6. 原型的最顶部是null

    js
    console.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
  7. 构造方法的原型上都有个constructor属性,该属性指向它自己。

    new出来的对象也有个constructor属性,该属性指向它的构造方法。

    js
    console.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

类的属性和静态方法

回顾完原型链,再来总结一下本节学的内容。

  1. 使用class声明类的时候,可以直接使用static关键词声明静态属性和方法,这种声明的属性和方法,实际上是直接挂载到类上的,类其实就是一个函数,函数其实也就是一个对象,所以在对象上挂载点属性和方法,没啥毛病,调用的时候直接使用类名.xxx的形式调用,跟正常访问对象上的方法和属性一模一样。

    js
    class A {
        static name = 'cheny'
        static sayHi() {
            console.log(`hello i am ${this.name}`);
        }
    }
    
    console.log(A.name); // cheny
    A.sayHi() // hello i am cheny
  2. 静态属性和类字段的区别,类字段其实就是相当于在构造方法上声明了this.xxx = 'xxx',然后使用new声明一个对象的时候,这个类字段直接被挂载到了new出来的对象上。

    js
    class 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
  3. 静态属性和方法在使用extends关键字时,都会继承过来,因为extends把派生类的__proto__指向了父类,所以在访问父类的静态方法时,会顺着原型链往上找。

    js
    class 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
    // 报错,静态方法只能通过类名调用

参考

https://zh.javascript.info/static-properties-methods