CoffeeScript 的静态方法和静态变量(类方法和类变量)

CoffeeScript 的静态方法和静态变量其实就是 ES6 里面的静态方法和静态变量,但是本身 ES6 应该只实现了静态方法,静态变量的定义方法并不直观,同时加上 CoffeeScript 的语法就更不好得知如何实现,coffeescript.org 似乎也没有对这方面进行过多介绍。网上搜索到的办法大部分只介绍了如何创建静态变量和静态方法,但也就到此为止了,对于其应用的写法,比如如何在实例方法里操作静态变量和如何在静态方法里操作静态变量的区别并没有详细说明。这次打算把自己写 coffee-telegram-bot 时尝试出的办法记录下来。

静态变量

很遗憾这里没有 static 这么简单的关键字,即使是在 EMCAScript 里 static 这个关键字也只是用来修饰静态方法,用 static 修饰变量仍然是个提案。首先还是来说如何在常规的 JavaScript 里使用静态变量(只考虑 ES6 之后的情况,因为多亏了 CoffeeScript,我也没有写过 ES5),它是如此的简单而又奇怪,简单是因为即使是我这样又笨又懒的人看过一遍也能在 Node 里完整地默写出来不报错,但是这是有原因的——因为它实在是太古怪了以至于你看一眼就忘不了它:

1
2
3
4
5
6
7
8
9
class Test {
  constructor() {
    ++Test.counter;
  }
}
Test.counter = 0;
let t = new Test();
console.log(Test.counter);
// 输出 1

没错这段代码是如此的直观,以至于我都不需要写注释来解释每一句干了什么,我敢肯定你有任何一点面向对象的基础都能明白,如果你对任何一个部分表示不理解,我建议你仔细去学习一个面向对象的程序语言—— Java 是个好选择因为它很成熟,同时是个严格的面向对象的程序语言。

当然,JavaScript 并不是那么纯粹的语言或者说它几乎什么都能做一点并且这个趋势越来越大,所以你并不需要深入的面向对象知识也能看懂上面那一段代码。在 ES6 中实现一个类变量或者叫静态变量就是这个简单地两步:第一在类定义的方法内直接使用类的名字来访问静态成员——你没看错,这里没有 this ——毕竟这里的 this 在上下文中大多数指的还是实例而不是类本身。第二在类定义的外部给你想要的变量进行一个初始化(赋初值)。

我敢肯定对于 JavaScript 这样一门号称“一切能够用 JavaScript 实现的最终都会用 JavaScript 实现”的语言而言肯定不只这么一种实现类变量的方法,但是最起码对我而言这是最直观的一种,我相信对于一切有一点 JavaScript 基础的人而言这也都是最直观的一种,如果你想问为什么没有更直观的在类定义内部实现的像 Java 或者什么一样的写法,很抱歉,暂时没有,或许你可以等到 ES7 的时代再来学习 JavaScript,当然到那个时候你又会被这门语言其他的问题纠结到 ES8,这很现实,这是一个不完美但同时不断高速进化的语言,它越来越广泛的应用逼着它去填由于愚蠢的商业手段导致的停滞不前带来的坑,当然最起码——JavaScript 虽然出生仓促,但基础并不很糟,或者说它有着各种奇特的实现不断让人燃烧自己的脑细胞。

好了是时候回到今天的主题了,我觉得我从来就不是一个合格的 JavaScript 程序员因为我喜欢 CoffeeScript——这大概是原罪,特别是在一群 TypeScript 的好战分子眼里——我没有说 TypeScript 不好,但是我只是很喜欢 CoffeeScript 的语法糖以及动态类型带来的天马行空或者乱七八糟的写法——你何必把一只泥沼里的乌龟拉去杀死放在庙里供奉呢?

CoffeeScript 的官网虽然没有明说,但是它确实有简化这部份的语法糖:

你只需要像写一个方法一样写一个变量,把它放在和方法一个层次上,然后要在开头加上 @,无论你使用 : 还是 = 来赋值都没有关系。在实例方法中使用时的写法有点变化,你不应该用 @ 因为它不属于当前实例这个 this,它属于 thisconstructor 也就是说 @constructor,CoffeeScript 编译器会正确的把这个替换成类的名字——没办法,JavaScript 并没有代词指代这种情况。

1
2
3
4
5
6
7
class Test
  @counter: 0
  constructor: () ->
    ++@constructor.counter
t = new Test()
console.log(Test.counter)
# 输出 1

编译出来的结果是这样的,虽然有着浓浓的咖啡味道但是也不难懂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Generated by CoffeeScript 2.1.1
(function() {
  var Test, t;
  Test = (function() {
    class Test {
      constructor() {
        ++this.constructor.counter;
      }
    };
    Test.counter = 0;
    return Test;
  }).call(this);
  t = new Test();
  console.log(Test.counter);
  // 输出 1
}).call(this);

实际上如果你只是想 创造 一个属于类的变量还有一种写法,但是严格来讲它并不属于类变量而是访问原型上的一个变量(不要忘了 JavaScript 的原型链),这个变量仍然会实例化,之所以在这里提起是因为网上很多搜索结果把这个也称作类变量,我认为是不正确的。

1
2
3
4
5
6
7
class Test
  counter: 0
  constructor: () ->
    ++@counter
t = new Test()
console.log(Test::counter)
# 输出 0

不推荐这个写法并不是因为它用了一个 C++ 常见的符号 ::,而是因为它只能让你访问到一个原型上的变量,最后仍然会被实例化,也没有直观的语法糖让你在方法里修改它(大概直接写 Test:: 可行?)。

静态方法

有了前面的基础静态方法无论是在原生 JavaScript 里面还是 CoffeeScript 都变得相当直观——大概只需要一个例子:

在 CoffeeScript 中和处理静态变量很类似,都是简单地在前面加上 @,当然如果你要在静态方法里操作静态变量不需要写 @constructor 而是直接写 @,因为这个时候上下文的 this 就是这个类本身——严格来说这也是不对的,能够绑定到这个上下文的原因是 CoffeeScript 编译的时候增加的函数作用域——这是个好东西,咖啡味。换句话说 ES6 这里根本就没有和 this 扯上关系,它只能傻乎乎的用类本名,CoffeeScript 在这里自己多搞了一点,用这个来变通,翻译成 JavaScript 的时候再替换掉文本。

1
2
3
4
5
6
7
8
9
class Test
  @counter: 0
  @printCounter: () =>
    console.log(@counter)
  constructor: () ->
    ++@constructor.counter
t = new Test()
Test.printCounter()
# 输出 1

编译成 JavaScript 之后是这样的,static 关键字终于出来刷存在感了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Generated by CoffeeScript 2.1.1
(function() {
  var Test, t;
  Test = (function() {
    class Test {
      static printCounter() {
        return console.log(Test.counter);
      }
      constructor() {
        ++this.constructor.counter;
      }
    };
    Test.counter = 0;
    return Test;
  }).call(this);
  t = new Test();
  Test.printCounter();
  // 输出 1
}).call(this);

总结

事实上到上面已经靠几个例子说明了正确的用法,但是由于还有交叉处理的部分,简单总结一下。

1.把变量写在类定义里面属性的层次并且在变量名前面加上 @ 会在 CoffeeScript 里产生一个静态变量。

2.在方法名前面加 @ 会在 CoffeeScript 里产生一个静态方法。

3.在静态方法中操作静态变量只需要像在实例方法中操作实例变量一样使用 this,这个作用域就是类定义的上下文,CoffeeScript 自动把 @ 替换成类的名字以便符合 ES6 语法。

4.在实例方法中操作静态变量需要使用 @constructor,原因是这个时候的 this 是实例本身而不是类,类在 CoffeeScript 里绑定到了 this.constructor,编译时自动替换成类的名字(因为 ES6 只能这样访问类变量而不是用一个引用)。

5.在静态方法中操作实例变量……根本没有这样的用法好吧!!!

碎碎念

明明是凑数的文章却偏偏写了这么多,反正大概是没有人看的,毕竟 CoffeeScript 是要凉了,就像那些激进分子希望的一样,光是喜欢又有什么用呢?他们是不会管你喜欢不喜欢的,他们不喜欢就得了。短期内倒是希望 Atom 能搞点什么大新闻,可以不要让 VSCode 激进分子在我这边黑 Atom,我就是喜欢用 Atom 不喜欢用 VSCode,就算你觉得 Atom 有很多缺点可是我觉得它足够合适乃至我可以接受它的缺点,在我这边吵除了生气以外还能有什么作用不成?所以我讨厌辩论,光是说,却没什么实际作用。或者这大概是软粉和微软受害者的冲突而已?最近没得东西写出来,还是去推一推 Fate/Stay Night 或者打一打 CSGO 好了。

AlynxZhou

A Coder & Dreamer

既然看了喵写的文章,不打算投喂一下再走吗?哼!