养成良好的编程习惯

认识

代码首先重要的是应该是可读,其次是可执行。
糟糕的代码可能需要花费很大的成本来维护,所以一开始写的时候就要认真。

变量命名

变量命名非常重要,是代码可读的基础,一个变量名称,不仅是一个名称,是要表达出它在这个环境中的一些作用的,承担的角色的,是需要传递一些信息出来的。

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;
// 这下不会报错了

这种处理方法并不合理,原因如下:

  1. 函数 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 函数曝光在读者眼中的时机是非常正确的,那就是,在它即将被调用的地方。实际上,在「计算函数的代码行数」这个问题上,我会把内部定义的函数视为一行,因为函数对读者可以是黑盒,它的负担只有一行。

总结

伟大的文学作品都是建立在废纸堆上的,不断删改作品的过程有助于写作者培养良好的「语感」。当然,代码毕竟不是艺术品,程序员没有精力也不一定有必要像作家一样反复打磨自己的代码/作品。但是,如果我们能够在编写代码时稍稍多考虑一下实现的合理性,或者在添加新功能的时候稍稍回顾一下之前的实现,我们就能够培养出一些「代码语感」。这种「代码语感」会非常有助于我们写出高质量的可读的代码。

参考文章:
https://fed.taobao.org/blog/taofed/do71ct/writing-readable-code/?spm=taofed.blogs.blog-list.6.40315ac8VXtPHE

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353