养成良好的编程习惯
认识
代码首先重要的是应该是可读,其次是可执行。
糟糕的代码可能需要花费很大的成本来维护,所以一开始写的时候就要认真。
变量命名
变量命名非常重要,是代码可读的基础,一个变量名称,不仅是一个名称,是要表达出它在这个环境中的一些作用的,承担的角色的,是需要传递一些信息出来的。
1.命名对象(名词)
bananaList 不仅要表达是什么东西,可能还要表达出集合的概念, List ? Map 的形式, 可以通过加后缀的形式来凸显。
2.命名函数(动词)
fetchUserInfoAsync 这个函数是要 (fetch)干什么(返回值)
3.命名的上下文
每个上下文关注的点不同,表达同一个意思,但在不同的环境中,有可能会有不同的表达。
4.命名一致
不管什么时候,这个东西都是这样命名的 ~
错误处理
函数
一个函数应该只做一件事
函数中合理地处理异常
async function getUserDetail(id) {
const user = await fetchSingleUser(id);
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 上面这一行报错了:Can not read property 'books' of undefined.
// ...
}
试图通过下面的方式去修改
const favorites = await fetchUserFavorits(id);
user.favoriteBooks = favorites && favorites.books;
// 这下不会报错了
这种处理方法并不合理,原因如下:
- 函数 fetchUserFavorits 原本有它合理的返回,但是如果返回了不合理的东西 如,undefined 就已经失控了,程序就不应该再往下执行了
2.通过 这种方式修改 之后,favoriteBooks如果被赋值为 undefined ,如果后续有对 favoriteBooks 数组做一些操作,如 执行数组的某些方法,就会又出现问题,这是非常糟糕了。
如果 fetchUserFavorits 属于当前的项目,那这里 getUserDetail 并没有什么责任,应该 在 fetchUserFavorits 函数中去处理,因为 fetchUserFavorits 就不应该返回 undefined,我们应该去修复 fetchUserFavorits,任务失败时显式地告知出来,或者直接抛出异常。同时,getUserDetail 稍作修改:
// 情况1:显式告知,此时应认为获取不到收藏数据不算致命的错误
const result = await fetchUserFavorits(id);
if(result.success) {
user.favoriteBooks = result.data.books;
} else {
user.favoriteBooks = []
}
// 情况2:直接抛出异常
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 这时 `getUserDetail` 不需要改动,任由异常沿着调用栈向上冒泡
如果 fetchUserFavorits 不在当前项目中,而是依赖的外部模块呢?我认为,这时你就该为选择了这样一个不可靠的模块负责,在 getUserDetail 中增加一些「擦屁股」代码,来避免你的项目的其他部分受到侵害。
const favorites = await fetchUserFavorits(id);
if(favorites) {
user.favoriteBooks = favorites.books;
} else {
throw new Error('获取用户收藏失败');
}
控制函数的副作用
无副作用的函数 指不依赖上下文,也不改变上下文的函数
async function getUserDetail(id) {
const user = await fetchSingleUserInfo(id);
await addFavoritesToUser(user);
...
}
async function addFavoritesToUser(user) {
const result = await fetchUserFavorits(user.id);
user.favoriteBooks = result.books;
user.favoriteSongs = result.songs;
user.isMusicFan = result.songs.length > 100;
}
addFavoritesToUser 就是个有副作用的函数,因为它改变了 user ,无副作用的形式
async function getUserDetail(id) {
const user = await fetchSingleUserInfo(id);
const {books, songs, isMusicFan} = await getUserFavorites(id);
return Object.assign(user, {books, songs, isMusicFan})
}
async function getUserFavorites(id) {
const {books, songs} = await fetchUserFavorits(user.id);
return {
books, songs, isMusicFan: result.songs.length > 100
}
}
非入侵性地改造函数
函数是一段独立和内聚的逻辑。在产品迭代的过程中,我们有时候不得不去修改函数的逻辑,为其添加一些新特性。之前我们也说过,一个函数只应做一件事,如果我们需要添加的新特性,与原先函数中的逻辑没有什么联系,那么决定是否通过改造这个函数来添加新功能,应当格外谨慎。
const fetchUserInfo = (userId, callback) => {
const param = {
url: '/api/user',
method: 'post',
payload: {id: userId}
};
request(param, callback);
}
现在有了一个新需求:为 fetchUserInfo 函数增加一道本地缓存,如果第二次请求同一个 userId 的用户信息,就不再重新向服务器发起请求,而直接以第一次请求得到的数据返回。
const userInfoMap = {};
const fetchUserInfo = (userId, callback) => {
if (userInfoMap[userId]) { // 新增代码
callback(userInfoMap[userId]); // 新增代码
} else { // 新增代码
const param = {
// ... 参数
};
request(param, (result) => {
userInfoMap[userId] = result; // 新增代码
callback(result);
});
}
}
实际上,「缓存」和「获取用户数据」完全是独立的两件事。我提出的方案是,编写一个通用的缓存包装函数(类似装饰器)memorizeThunk,对 fetchUserInfo 进行包装,产出一个新的具有缓存功能的 fetchUserInfoCache,在不破坏原有函数可读性的基础上,提供缓存功能.
const memorizeThunk = (func, reducer) => {
const cache = {};
return (...args, callback) => {
const key = reducer(...args);
if (cache[key]) {
callback(...cache[key]);
} else {
func(...args, (...result) => {
cache[key] = result;
callback(...result);
});
}
}
}
const fetchUserInfo = (userInfo, callback) => {
// 原来的逻辑
}
const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);
类
避免滥用成员函数
JavaScript 中的类,是 ES6 才有的概念,此前是通过函数和原型链来模拟的。在编写类的时候,我们常常忍不住地写很多没必要的成员函数:当类的某个成员函数的内部逻辑有点复杂了,行数有点多了之后,我们往往会将其中一部分「独立」逻辑拆分出来,实现为类的另一个成员函数。比如,假设我们编写某个 React 组件来显示用户列表,用户列表的形式是每两个用户为一行。
class UserList extends React.Component{
// ...
chunk = (users) => {
// 将 ['张三', '李四', '王二', '麻子'] 转化为 [['张三', '李四'], ['王二', '麻子']]
}
render(){
const chunks = this.chunk(this.props.users);
// 每两个用户为一行
return (
<div>
{chunks.map(users=>
<row>
{users.map(user =>
<col><UserItem user={user}></col>
)}
</row>
)}
</div>
)
}
}
如上述代码所示,UserList
组件按照「两个一行」的方式来显示用户列表,所以需要先将用户列表进行组合。进行组合的工作这件事情看上去是比较独立的,所以我们往往会将 chunk
实现成 UserList
的一个成员函数,在 render 中调用它。
我认为这样做并不可取,因为 chunk 只会被 render 所调用,仅仅服务于 render。阅读这个类源码的时候,读者其实只需要在 render 中去了解 chunk 函数就够了。然而 chunk 以成员函数的形式出现,扩大了它的可用范围,提前把自己曝光给了读者,反而会造成干扰。读者阅读源码,首先就是将代码折叠起来,然后他看到的是这样的景象:
class UserList extends React.Component {
componentDidMount() {...}
componentWillUnmount() {...}
chunk() {...} // 读者的内心独白:这是什么鬼?
render() {...}
}
熟悉 React 的同学对组件中出现一个不熟悉的方法多半会感到困惑。不管怎么说,读者肯定会首先去浏览一遍这些成员函数,但是阅读 chunk
函数带给读者的信息基本是零,反而还会干扰读者的思路,因为读者现在还不知道用户列表需要以「每两个一行」的方式呈现。所以我认为,chunk
函数绝对应该定义在 render
中,如下所示:
render(){
const chunk = (users) => ...
const chunks = this.chunk(this.props.users);
return (
<div>
...
}
这样虽然函数的行数可能会比较多,但将代码折叠起来后,函数的逻辑则会非常清楚。而且,chunk
函数曝光在读者眼中的时机是非常正确的,那就是,在它即将被调用的地方。实际上,在「计算函数的代码行数」这个问题上,我会把内部定义的函数视为一行,因为函数对读者可以是黑盒,它的负担只有一行。
总结
伟大的文学作品都是建立在废纸堆上的,不断删改作品的过程有助于写作者培养良好的「语感」。当然,代码毕竟不是艺术品,程序员没有精力也不一定有必要像作家一样反复打磨自己的代码/作品。但是,如果我们能够在编写代码时稍稍多考虑一下实现的合理性,或者在添加新功能的时候稍稍回顾一下之前的实现,我们就能够培养出一些「代码语感」。这种「代码语感」会非常有助于我们写出高质量的可读的代码。