class 要使用私有变量,居然要包一层闭包,再用一个 WeakMap 去存?
以前用构造函数的时候好歹 var 就行了。。 问题是连实例变量都没有,这个 class 究竟有什么用? 要类变量的话也是妥妥要包一层了
用一个闭包返回一个 class。。 我还不如直接用构造函数呢。。。
大家平时用这个 class 的多吗,涉及到类变量和私有变量是如何去处理的呢?
https://github.com/jeffmo/es-class-fields-and-static-properties 看来 babel experimental 已经搞出来了
ES 里面有类变量的规范,Babel 等 transpiler 也已经实现了。私有变量是没有的,不过以现在 OO 的影响力,加进去估计只是时间问题了。
但是换个思路想,干嘛一定要用私有变量呢?真担心有代码会改写私有变量么?或者担心队友滥用私有变量?这种事情靠开发者自我约束就能解决。Ruby 和 JavaScript 都属于这类的语言。少了语言约束,但得到了灵活性。
我觉得 ES 给出 class 的写法,不是为了让 JavaScript 更 OO,而是为了在不改变 prototype 的设计思路的情况下,为批量创建 object 提供一个更 简便 和 统一 的标准(虽然也丧失了一些灵活性)。你看看各种框架里千奇百怪的创建“类”的语法就会觉得 ES class 还是有必要的。但这不代表你就一定要用 class 去构造整个应用,class 给了一个选择,但不是唯一的选择。如果你最后真需要了解 class,可以看看我之前写的 JavaScript 原型系统的变迁,以及 ES6 class 。
废话了这么多,说说我自己的应用场景,一般情况下我可能直接创建 object,如果很多 object 需要共同的模板我就会用 class。如果需要标注私有变量,我就用 _xxx
。团队里有明确的约束和 code review 的情况下,一般不需要担心什么。
class 要使用私有变量,居然要包一层闭包,再用一个 WeakMap 去存
因为 ECMAScript 从来就没有私有成员这种概念,而闭包是确保变量不会泄漏的可靠方法,所以用闭包模拟私有成员是 JavaScript 的必修课。至于 WeakMap 来存并非是唯一的方案(不过的确是比较好的),比方说 Symbols 也可以(参见:http://exploringjs.com/es6/ch_classes.html#sec_private-data-for-classes)。
问题是连实例变量都没有,这个 class 究竟有什么用?
Are you sure?
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
要类变量的话也是妥妥要包一层了
Again, Are you sure?
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static get ZERO() {
return new Point(0, 0);
}
}
或者直接 Point.ZERO = new Point(0, 0)
也是可以的。
还不如直接用构造函数呢
你想多了,其实 class
就是构造器+原型继承的语法糖而已,JavaScript 从来都不是基于类型系统的 OO 编程语言,这一点一直都没有变过。
我不喜欢用 Class
,不管是过去的构造器模式还是现在的新语法。不是因为对任何编程范式有偏见,本来 JavaScript 就不是 Class Based OO 语言,硬生生的去模仿就是会觉得别扭罢了。和 Java、C# 等语言不同,class
不是必需品,这也就意味着你完全可以不用。然而奇怪的是使用 JavaScript 的人很多却是不用 class
不行,这是不是对这门语言存在很大的误解呢?
更重要的是在实践中我们会发现使用其他的模式——比如工厂函数(Factory Functions)要远比 class
简洁、灵活,用它来替代 class
几乎总能得到更好的结果。
// person.js
export default _ => {
return {
greet() {
console.log('Hello world!')
}
}
}
用起来和一个 Class 几乎一模一样——除了不需要用 new
,这不算坏处吧?
import Person from './person'
const nightire = Person()
nightire.greet() // "Hello world!"
console.log
限制了 greet
方法的行为,为了不局限问候的方式,可以使用依赖注入——这是解耦的一种简便易行的方法。即使在现实中很多时候看不出需要依赖注入的迹象,我们也应该有意识的这么做。在定义一种“类型”的时候对外界知道的越少越好(于是就更容易复用、扩展、组合……)。
// person.js
export default ioStream => {
return {
greet() {
ioStream.send('Hello world!')
}
}
}
比如说我们可以把 console
封装一下,让系统内所有的 ioStream
都具有统一的接口,然后就可以直接使用:
import Person from './person'
import ConsoleStream from 'console-stream'
const nightire = Person(ConsoleStream())
nightire.greet() // "Hello world!"
这个是顺带一提的事情,因为我注意到不懂得处理依赖注入(或者说更高层次上的解耦概念)的人通常都会把单元测试写得无比蛋疼……实际上,对象字面量在很多时候胜过一切构造模式:
import test from 'ava'
import Person from './person'
test(`a person can send message to io stream`, t => {
const ioStream = {
send(anything) {
t.same(anything, 'Hello world!');
}
}
const anyone = Person(ioStream)
anyone.greet()
})
其实私有成员可以变得很自然很自然,闭包一样在用,只是不那么扎眼了:
// person.js
export default ioStream => {
let _message = 'Hello world!'
return {
greet() {
ioStream.send('Hello world!')
},
get message() {
return _message
},
set message(message) {
_message = message
}
}
}
用法就不写了,和之前没什么区别。getter/setter 也不是必须的,看接口设计需求了。
这才是对 OO 来说最重要的(相较于怎么定义/创建对象来说),总的来说组合总是要优于继承,工厂模式搞起来尤其轻松。
比方说我们已经有了一个动作“类”:
// action.js
export default ioStream => {
return {
wave() {
ioStream.send('(Waving Hands...)')
}
}
}
那么与 Person
的组合可以这样:
import Person from './person'
import Action from './action'
import ConsoleStream from 'console-stream'
const _console = ConsoleStream()
const nightire = Object.assign({}, Person(_console), Action(_console))
nightire.message = 'Farewell, my friend!'
nightire.wave() // "(Waving Hands...)"
nightire.greet() // "Farewell, my friend!"
这是我觉得最好的一个优点。由于 this
在 JavaScript 中是在运行时动态绑定的,如果使用你代码的人不理解这一点,那么他们就会犯错误(而且会指责是你写的不对……)。有些人是因为不理解 this
而不敢用,有些人则是为了迁就前者而干脆不去用,架构师会比较容易体会这类情况。
这是典型的容易犯错的例子:
// stepper.js
export default class Stepper {
constructor(offset) {
this.offset = offset
}
add(amount) {
return amount + this.offset
}
}
// main.js
import Stepper from './stepper'
const stepper = new Stepper(1)
[1, 2, 3].map(stepper.add.bind(stepper))
容易犯错的地方就是最后一行,如果不加 .bind(stepper)
的话最终 this
的指向就是错误的。但往往使用者并不理解这一点,反正看到你的文档就知道这个能加上初始化传入的 offset
就是了,除非你不厌其烦的在文档里强调:“注意上下文的变化,如有必要请用 bind()
明确 this
的指向“……啊,说不定你还得培训一下让大家都知道如有“必要”的确切范围。
然而你也可以这样来重写一下:
// stepper.js
export default offset => {
return {
add(amount) {
return amount + offset
}
}
}
// main.js
import Stepper from './stepper'
const stepper = Stepper(1)
[1, 2, 3].map(stepper.add) // [2, 3, 4]
于是无论是具体实现还是接口定义都能保持简洁一致。
这是一些使用工厂函数代替类型定义的常用场景,我不是说百分之百不要用 class
(或是构造器之类的),他们也有用武之地,只是人们在抱怨他们不如熟悉的 Java 等语言好用的时候也应该问问自己:我真的了解它吗?这是唯一的选择吗?
#11 楼 @mizuhashi 其实我早就看到这帖子了,但是之前并没有想要说点啥……是因为看了后来的一些评论才写的。
把对象的方法抽出来传这件事情并非习惯问题,而是 map()
函数本来就是接受一个一个函数的啊,又不能只把 stepper
对象传给它。在现实中我们经常看到:
[1, 2, 3].map(function(item) {
stepper.add(item)
})
这样的写法可能会比较贴合大多数人的习惯,然而说句不好听的,这纯属“脱裤子放屁多此一举”……可转念一想也怪不得人家,谁让 JavaScript 的 this
如此不接地气呢?像我上文的那种写法才是本来应该的样子,如果在定义接口的时候能预先考虑 this
绑定的使用场景,那么函数式的“范儿”就会足足的。
#15 楼 @darkbaby123 过奖过奖。
顺便广告一下,Babel 的用户手册和插件开发手册简体中文版已经基本完成,这份文档是我献给 JavaScript 社区的新年礼物,当然要先感谢 Babel Core Team 编写的英文版。
晚些时候我打算写一篇介绍 Flow Syntax 的文章,Babel 的最新版本刚刚支持了它,为 JavaScript 提供了注入类型注解/推导,静态类型检查等特性,从此以后不再是 TypeScript 的专利啦(或者说这是 TypeScript 为 ECMAScript 做出的贡献)。
手册的地址:https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/README.md