代码风格规范

32 阅读15分钟

前言

为前端开发提供良好的基础编码风格修行指南。

基于 Airbnb 编码规范。所有规范分为三个等级:必须推荐可选

必须(Mandatory) 级别要求必须严格按照规范的编码格式编写,否则会在代码扫描和自动化构建中报错。对应 RFC 2119 中的 MUST, MUST NOTREQUIRED 级别。

推荐(Preferable) 级别希望员工尽量按照规范编写,但如有特殊情况,可以不采用。对应 RFC 2119 中的 SHOULD, SHOULD NOT 级别。

可选(Optional) 级别并不对编码提出要求,一般是JavaScript常识以及ES6、ES7的新语法,但仍希望员工按参考规范编写。委员会将定期review编码规范,不断提高规范等级要求。对应 RFC 2119 中的 MAY 级别。

本文档中的代码为示例代码,出于演示目的,不一定完全符合本文档所规定的所有规则要求。

本文档中的示例代码中会有 Good 或 Best 的提示,表示这是遵守代码规范的一种写法。 需要说明的是,虽然 Good 写法符合这条规范,但不代表这是满足规范的唯一方式。

0. 相关工具

npm eslint 规则包

安装:

javascript

npm install eslint @types/eslint eslint-plugin-prettier --save-dev

react

npm install eslint-plugin-react

vue

npm install eslint-plugin-vue vue-eslint-parser

.eslintrc.js:

module.exports = {
  extends: [require.resolve('@umijs/fabric/dist/eslint')],
  globals: {
    page: true,
    REACT_APP_ENV: true,
  },
  rules: {
    '@typescript-eslint/no-unused-expressions': [
      'error',
      {
        allowShortCircuit: true,
      },
    ],
    'no-dupe-keys': 2,
    'no-shadow': 'off',
    '@typescript-eslint/no-shadow': [1],
  },
};

typescript 还需要安装依赖

npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev

.eslintrc.js:

module.exports = {
  extends: [require.resolve('@umijs/fabric/dist/eslint')],
}

使用 prettier 的用户需要安装对应的依赖

npm install prettier eslint-plugin-prettier --save-dev

.prettierrc.js:

const fabric = require('@umijs/fabric');

module.exports = {
  ...fabric.prettier,
};

注意:使用 prettier 规则之后,会自动禁用掉与之相冲突的格式相关的规则。由于prettier与ESLint存在冲突,开源扫描仍以ESLint扫描结果为准。

1. 类型

  • 1.1 【可选】 基本类型: 当你访问一个基本类型时,直接操作它的值。

    • string
    • number
    • boolean
    • null
    • undefined
    • symbol
    const foo = 1;
    let bar = foo;
    
    bar = 9;
    
    console.log(foo, bar); // => 1, 9
    
    • 符号(Symbols)不能完全的被 polyfill,因此在不能原生支持symbol类型的浏览器或环境中,不应该使用symbol类型。
  • 1.2 【可选】 复杂类型: 当你访问一个复杂类型时,直接操作其值的引用。

    • object
    • array
    • function
    const foo = [1, 2];
    const bar = foo;
    
    bar[0] = 9;
    
    console.log(foo[0], bar[0]); // => 9, 9
    

2. 引用

  • 2.1 【必须】 使用 const 定义你的所有引用;避免使用 var。 eslint: [prefer-const](<https://eslint.org/docs/rules/prefer-const.html>), [no-const-assign](<https://eslint.org/docs/rules/no-const-assign.html>)

    原因? 这样能够确保你不能重新赋值你的引用,否则可能导致错误或者产生难以理解的代码。

    // bad
    var a = 1;
    var b = 2;
    
    // good
    const a = 1;
    const b = 2;
    
  • 2.2 【必须】 如果你必须重新赋值你的引用, 使用 let 代替 var。 eslint: [no-var](<https://eslint.org/docs/rules/no-var.html>)

    原因? let 是块级作用域,而不像 var 是函数作用域。

    // bad
    var count = 1;
    if (true) {
      count += 1;
    }
    
    // good, use the let.
    let count = 1;
    if (true) {
      count += 1;
    }
    
  • 2.3 【可选】 注意,let 和 const 都是块级作用域。

    // const 和 let 只存在于他们定义的块级作用域中。
    {
      let a = 1;
      const b = 1;
    }
    console.log(a); // ReferenceError
    console.log(b); // ReferenceError
    

3. 对象

  • 3.1 【必须】 使用字面量语法创建对象。 eslint: [no-new-object](<https://eslint.org/docs/rules/no-new-object.html>)

    // bad
    const item = new Object();
    
    // good
    const item = {};
    
  • 3.2 【推荐】 在创建具有动态属性名称的对象时使用计算属性名。

    原因? 它允许你在一个地方定义对象的所有属性。

    function getKey(k) {
      return `a key named ${k}`;
    }
    
    // bad
    const obj = {
      id: 5,
      name: 'San Francisco',
    };
    obj[getKey('enabled')] = true;
    
    // good
    const obj = {
      id: 5,
      name: 'San Francisco',
      [getKey('enabled')]: true,
    };
    
  • 3.3 【推荐】 用对象方法简写。 eslint: [object-shorthand](<https://eslint.org/docs/rules/object-shorthand.html>)

    // bad
    const value = 1;
    const atom = {
      value: value,
      addValue: function (newValue) {
        return atom.value + newValue;
      },
    };
    
    // good
    const value = 1;
    const atom = {
      value,
      addValue(newValue) {
        return atom.value + newValue;
      },
    };
    
  • 3.4 【推荐】 用属性值简写。 eslint: [object-shorthand](<https://eslint.org/docs/rules/object-shorthand.html>)

    原因? 它更加简洁并更具描述性。

    const lukeSkywalker = 'Luke Skywalker';
    
    // bad
    const obj = {
      lukeSkywalker: lukeSkywalker,
    };
    
    // good
    const obj = {
      lukeSkywalker,
    };
    
  • 3.5 【推荐】 声明对象时,将简写的属性放在前面。

    原因? 这样更容易的判断哪些属性使用的简写。

    const anakinSkywalker = 'Anakin Skywalker';
    const lukeSkywalker = 'Luke Skywalker';
    
    // bad
    const obj = {
      episodeOne: 1,
      twoJediWalkIntoACantina: 2,
      lukeSkywalker,
      episodeThree: 3,
      mayTheFourth: 4,
      anakinSkywalker,
    };
    
    // good
    const obj = {
      lukeSkywalker,
      anakinSkywalker,
      episodeOne: 1,
      twoJediWalkIntoACantina: 2,
      episodeThree: 3,
      mayTheFourth: 4,
    };
    
  • 3.6 【必须】 只使用引号标注无效标识符的属性。 eslint: [quote-props](<https://eslint.org/docs/rules/quote-props.html>)

    原因? 一般来说,我们认为这样更容易阅读。 它能更好地适配语法高亮显示功能,并且更容易通过许多 JS 引擎进行优化。

    // bad
    const bad = {
      'foo': 3,
      'bar': 4,
      'data-blah': 5,
    };
    
    // good
    const good = {
      foo: 3,
      bar: 4,
      'data-blah': 5,
    };
    
  • 3.7 【推荐】 不能直接调用 Object.prototype 的方法,如: hasOwnPropertypropertyIsEnumerableisPrototypeOf。 eslint: [no-prototype-builtins](<https://eslint.org/docs/rules/no-prototype-builtins.html>)

    原因? 这些方法可能被有问题的对象上的属性覆盖 - 如 { hasOwnProperty: false } - 或者,对象是一个空对象 (Object.create(null))。

    // bad
    console.log(object.hasOwnProperty(key));
    
    // good
    console.log(Object.prototype.hasOwnProperty.call(object, key));
    
    // best
    const has = Object.prototype.hasOwnProperty; // 在模块范围内的缓存中查找一次
    console.log(has.call(object, key));
    
    /* or */
    import has from 'has'; // <https://www.npmjs.com/package/has>
    console.log(has(object, key));
    
  • 3.8 【推荐】 使用对象扩展操作符(spread operator)浅拷贝对象,而不是用 [Object.assign](<https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign>) 方法。 使用对象的剩余操作符(rest operator)来获得一个新对象,该对象省略了某些属性。 eslint: [prefer-object-spread](<https://eslint.org/docs/rules/prefer-object-spread>)

    // very bad
    const original = { a: 1, b: 2 };
    const copy = Object.assign(original, { c: 3 }); // 变异的 `original` ಠ_ಠ
    delete copy.a; // 这....
    
    // bad
    const original = { a: 1, b: 2 };
    const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }
    
    // good
    const original = { a: 1, b: 2 };
    const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }
    
    const { a, ...noA } = copy; // noA => { b: 2, c: 3 }
    

4. 数组

  • 4.1 【必须】 使用字面量语法创建数组。 eslint: [no-array-constructor](<https://eslint.org/docs/rules/no-array-constructor.html>)

    // bad
    const items = new Array();
    
    // good
    const items = [];
    
  • 4.2 【必须】 使用 Array#push 代替直接赋值来给数组添加项。

    const someStack = [];
    
    // bad
    someStack[someStack.length] = 'abracadabra';
    
    // good
    someStack.push('abracadabra');
    
  • 4.3 【必须】 使用数组展开符 ... 来拷贝数组。

    // bad
    const len = items.length;
    const itemsCopy = [];
    let i;
    
    for (i = 0; i < len; i += 1) {
      itemsCopy[i] = items[i];
    }
    
    // good
    const itemsCopy = [...items];
    
  • 4.4 【推荐】 使用展开符 ... 代替 [Array.from](<https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/from>),将一个可迭代对象转换成一个数组。

    const foo = document.querySelectorAll('.foo');
    
    // good
    const nodes = Array.from(foo);
    
    // best
    const nodes = [...foo];
    
  • 4.5 【必须】 使用 Array.from 将一个类数组(array-like)对象转换成一个数组。

    const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 };
    
    // bad
    const arr = Array.prototype.slice.call(arrLike);
    
    // good
    const arr = Array.from(arrLike);
    
  • 4.6 【必须】 使用 Array.from 代替展开符 ... 映射迭代器,因为它避免了创建一个中间数组。

    // bad
    const baz = [...foo].map(bar);
    
    // good
    const baz = Array.from(foo, bar);
    
  • 4.7 【推荐】 在数组回调函数中使用 return 语句。 如果函数体由单个语句的返回表达式组成,并且无副作用,那么可以省略返回值, 具体查看 8.2。 eslint: [array-callback-return](<https://eslint.org/docs/rules/array-callback-return>)

    // good
    [1, 2, 3].map((x) => {
      const y = x + 1;
      return x * y;
    });
    
    // good
    [1, 2, 3].map(x => x + 1);
    
    // bad - 没有返回值,意味着在第一次迭代后 `acc` 没有被定义
    [[0, 1], [2, 3], [4, 5]].reduce((acc, item, index) => {
      const flatten = acc.concat(item);
      acc[index] = flatten;
    });
    
    // good
    [[0, 1], [2, 3], [4, 5]].reduce((acc, item, index) => {
      const flatten = acc.concat(item);
      acc[index] = flatten;
      return flatten;
    }, []);
    
    // bad
    inbox.filter((msg) => {
      const { subject, author } = msg;
      if (subject === 'Mockingbird') {
        return author === 'Harper Lee';
      } else {
        return false;
      }
    });
    
    // good
    inbox.filter((msg) => {
      const { subject, author } = msg;
      if (subject === 'Mockingbird') {
        return author === 'Harper Lee';
      }
    
      return false;
    });
    
  • 4.8 【推荐】 如果数组有多行,则在数组开始括号 [ 的时候换行,然后在数组结束括号 ] 的时候换行。 eslint: [array-bracket-newline](<https://eslint.org/docs/rules/array-bracket-newline>)

    // bad
    const arr = [
      [0, 1], [2, 3], [4, 5],
    ];
    
    const objectInArray = [{
      id: 1,
    }, {
      id: 2,
    }];
    
    const numberInArray = [
      1, 2,
    ];
    
    // good
    const arr = [[0, 1], [2, 3], [4, 5]];
    
    const objectInArray = [
      {
        id: 1,
      },
      {
        id: 2,
      },
    ];
    
    const numberInArray = [
      1,
      2,
    ];
    

5. 解构

  • 5.1 【推荐】 在访问和使用对象的多个属性时使用对象解构。 eslint: [prefer-destructuring](<https://eslint.org/docs/rules/prefer-destructuring>)

    原因? 解构可以避免为这些属性创建临时引用。

    // bad
    function getFullName(user) {
      const firstName = user.firstName;
      const lastName = user.lastName;
    
      return `${firstName} ${lastName}`;
    }
    
    // good
    function getFullName(user) {
      const { firstName, lastName } = user;
      return `${firstName} ${lastName}`;
    }
    
    // best
    function getFullName({ firstName, lastName }) {
      return `${firstName} ${lastName}`;
    }
    
  • 5.2 【推荐】 使用数组解构。 eslint: [prefer-destructuring](<https://eslint.org/docs/rules/prefer-destructuring>)

    const arr = [1, 2, 3, 4];
    
    // bad
    const first = arr[0];
    const second = arr[1];
    
    // good
    const [first, second] = arr;
    
  • 5.3 【必须】 在有多个返回值时, 使用对象解构,而不是数组解构。

    原因? 你可以随时添加新的属性或者改变属性的顺序,而不用修改调用方。

    // bad
    function processInput(input) {
      // 处理代码...
      return [left, right, top, bottom];
    }
    
    // 调用者需要考虑返回数据的顺序。
    const [left, __, top] = processInput(input);
    
    // good
    function processInput(input) {
      // 处理代码...
      return { left, right, top, bottom };
    }
    
    // 调用者只选择他们需要的数据。
    const { left, top } = processInput(input);
    

6. 字符

  • 6.1 【推荐】 使用单引号 '' 定义字符串。 eslint: [quotes](<https://eslint.org/docs/rules/quotes.html>)

    // bad
    const name = "Capt. Janeway";
    
    // bad - 模板文字应该包含插值或换行。
    const name = `Capt. Janeway`;
    
    // good
    const name = 'Capt. Janeway';
    
  • 6.2 【必须】 不应该用字符串跨行连接符的格式来跨行编写,这样会使当前行长度超过100个字符。

    原因? 断开的字符串维护起来很痛苦,并且会提高索引难度。

    // bad
    const errorMessage = 'This is a super long error that was thrown because \\
    of Batman. When you stop to think about how Batman had anything to do \\
    with this, you would get nowhere \\
    fast.';
    
    // bad
    const errorMessage = 'This is a super long error that was thrown because ' +
      'of Batman. When you stop to think about how Batman had anything to do ' +
      'with this, you would get nowhere fast.';
    
    // good
    const errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';
    
  • 6.3 【必须】 构建字符串时,使用字符串模板代替字符串拼接。 eslint: [prefer-template](<https://eslint.org/docs/rules/prefer-template.html>) [template-curly-spacing](<https://eslint.org/docs/rules/template-curly-spacing>)

    原因? 字符串模板为您提供了一种可读的、简洁的语法,具有正确的换行和字符串插值特性。

    // bad
    function sayHi(name) {
      return 'How are you, ' + name + '?';
    }
    
    // bad
    function sayHi(name) {
      return ['How are you, ', name, '?'].join();
    }
    
    // bad
    function sayHi(name) {
      return `How are you, ${ name }?`;
    }
    
    // good
    function sayHi(name) {
      return `How are you, ${name}?`;
    }
    
  • 6.4 【必须】 永远不要使用 eval() 执行放在字符串中的代码,它导致了太多的漏洞。 eslint: [no-eval](<https://eslint.org/docs/rules/no-eval>)

  • 6.5 【必须】 不要在字符串中转义不必要的字符。 eslint: [no-useless-escape](<https://eslint.org/docs/rules/no-useless-escape>)

    原因? 反斜杠损害了可读性,因此只有在必要的时候才可以出现。

    // bad
    const foo = '\\'this\\' \\i\\s \\"quoted\\"';
    
    // good
    const foo = '\\'this\\' is "quoted"';
    const foo = `my name is '${name}'`;
    

7. 函数

  • 7.1 【可选】 使用命名的函数表达式代替函数声明。 eslint: [func-style](<https://eslint.org/docs/rules/func-style>)

    原因? 函数声明时作用域被提前了,这意味着在一个文件里函数很容易(太容易了)在其定义之前被引用。这样伤害了代码可读性和可维护性。如果你发现一个函数又大又复杂,并且它干扰了对这个文件其他部分的理解,那么是时候把这个函数单独抽成一个模块了!别忘了给表达式显式的命名,不用管这个名字是不是由一个确定的变量推断出来的(这在现代浏览器和类似babel编译器中很常见)。这消除了由匿名函数在错误调用栈产生的所有假设。 * (Discussion)

    // bad
    function foo() {
      // ...
    }
    
    // also good *
    const foo = function () {
      // ...
    };
    
    // good
    const short = function longUniqueMoreDescriptiveLexicalFoo() {
      // ...
    };
    
  • 7.2 【必须】 把立即执行函数包裹在圆括号里。 eslint: [wrap-iife](<https://eslint.org/docs/rules/wrap-iife.html>)

    原因? 立即调用的函数表达式是个独立的单元 - 将它和它的调用括号还有入参包装在一起可以非常清晰的表明这一点。请注意,在一个到处都是模块的世界中,您几乎用不到 IIFE。

    // immediately-invoked function expression (IIFE) 立即调用的函数表达式
    (function () {
      console.log('Welcome to the Internet. Please follow me.');
    }());
    
  • 7.3 【必须】 切记不要在非功能块中声明函数 (if, while, 等)。 请将函数赋值给变量。 浏览器允许你这样做, 但是不同浏览器会有不同的行为, 这并不是什么好事。 eslint: [no-loop-func](<https://eslint.org/docs/rules/no-loop-func.html>)

  • 7.4 【必须】 ECMA-262 将 block 定义为语句列表。 而函数声明并不是语句。

    // bad
    if (currentUser) {
      function test() {
        console.log('Nope.');
      }
    }
    
    // good
    let test;
    if (currentUser) {
      test = () => {
        console.log('Yup.');
      };
    }
    
  • 7.5 【必须】 永远不要给一个参数命名为 arguments。 这将会覆盖函数默认的 arguments 对象。 eslint: [no-shadow-restricted-names](<https://eslint.org/docs/rules/no-shadow-restricted-names>)

    // bad
    function foo(name, options, arguments) {
      // ...
    }
    
    // good
    function foo(name, options, args) {
      // ...
    }
    
  • 7.6 【推荐】 使用 rest 语法 ... 代替 arguments。 eslint: [prefer-rest-params](<https://eslint.org/docs/rules/prefer-rest-params>)

    原因? ... 明确了你想要拉取什么参数。 而且, rest 参数是一个真正的数组,而不仅仅是类数组的 arguments 。

    // bad
    function concatenateAll() {
      const args = Array.prototype.slice.call(arguments);
      return args.join('');
    }
    
    // good
    function concatenateAll(...args) {
      return args.join('');
    }
    
  • 7.7 【推荐】 使用默认的参数语法,而不是改变函数参数。

    // really bad
    function handleThings(opts) {
      // 不!我们不应该修改参数。
      // 更加错误的是: 如果 opts 是一个 "非正值"(falsy)它将被设置成一个对象
      // 这或许正是你想要的,但它可能会导致一些难以察觉的错误。
      opts = opts || {};
      // ...
    }
    
    // still bad
    function handleThings(opts) {
      if (opts === void 0) {
        opts = {};
      }
      // ...
    }
    
    // good
    function handleThings(opts = {}) {
      // ...
    }
    
  • 7.8 【必须】 使用默认参数时避免副作用。

    原因? 他们很容易混淆。

    var b = 1;
    // bad
    function count(a = b++) {
      console.log(a);
    }
    count();  // 1
    count();  // 2
    count(3); // 3
    count();  // 3
    
  • 7.9 【推荐】 总是把默认参数放在最后。 eslint: [default-param-last](<https://eslint.org/docs/rules/default-param-last>)

    // bad
    function handleThings(opts = {}, name) {
      // ...
    }
    
    // good
    function handleThings(name, opts = {}) {
      // ...
    }
    
  • 7.10 【推荐】 永远不要使用函数构造器来创建一个新函数。 eslint: [no-new-func](<https://eslint.org/docs/rules/no-new-func>)

    原因? 以这种方式创建一个函数跟 eval() 差不多,将会导致漏洞。

    // bad
    var add = new Function('a', 'b', 'return a + b');
    
    // still bad
    var subtract = Function('a', 'b', 'return a - b');
    
  • 7.11 【必须】 函数声明语句中需要空格。 eslint: [space-before-function-paren](<https://eslint.org/docs/rules/space-before-function-paren>) [space-before-blocks](<https://eslint.org/docs/rules/space-before-blocks>)

    原因? 一致性很好,在删除或添加名称时不需要添加或删除空格。

    // bad
    const f = function(){};
    const g = function (){};
    const h = function() {};
    
    // good
    const x = function () {};
    const y = function a() {};
    
  • 7.12 【推荐】 不要改变入参。 eslint: [no-param-reassign](<https://eslint.org/docs/rules/no-param-reassign.html>)

    原因? 操作入参对象会导致原始调用位置出现意想不到的副作用。

    // bad
    function f1(obj) {
      obj.key = 1;
    }
    
    // good
    function f2(obj) {
      const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1;
    }
    
  • 7.13 【推荐】 不要对入参重新赋值,也不要给入参的属性赋值。部分要求修改入参的常用库(如 Koa、Vuex)可以豁免。 eslint: [no-param-reassign](<https://eslint.org/docs/rules/no-param-reassign.html>)

    原因? 重新赋值参数会导致意外的行为,尤其是在访问 arguments 对象的时候。 它还可能导致性能优化问题,尤其是在 V8 中。

    // bad
    function f1(a) {
      a = 1;
      // ...
    }
    
    function f2(a) {
      if (!a) { a = 1; }
      // ...
    }
    
    // good
    function f3(a) {
      const b = a || 1;
      // ...
    }
    
    function f4(a = 1) {
      // ...
    }
    
  • 7.14 【推荐】 优先使用扩展运算符 ... 来调用可变参数函数。 eslint: [prefer-spread](<https://eslint.org/docs/rules/prefer-spread>)

    原因? 它更加清晰,你不需要提供上下文,并且能比用 apply 来执行可变参数的 new 操作更容易些。

    // bad
    const x = [1, 2, 3, 4, 5];
    console.log.apply(console, x);
    
    // good
    const x = [1, 2, 3, 4, 5];
    console.log(...x);
    
    // bad
    new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]));
    
    // good
    new Date(...[2016, 8, 5]);
    
  • 7.15 【推荐】 调用或者书写一个包含多个参数的函数应该像这个指南里的其他多行代码写法一样: 每行值包含一个参数,并且最后一行也要以逗号结尾。eslint: [function-paren-newline](<https://eslint.org/docs/rules/function-paren-newline>)

    // bad
    function foo(bar,
                  baz,
                  quux) {
      // ...
    }
    
    // good
    function foo(
      bar,
      baz,
      quux,
    ) {
      // ...
    }
    
    // bad
    console.log(foo,
      bar,
      baz);
    
    // good
    console.log(
      foo,
      bar,
      baz,
    );
    

8. 箭头函数

  • 8.1 【推荐】 当你必须使用匿名函数时 (当传递内联函数时), 使用箭头函数。 eslint: [prefer-arrow-callback](<https://eslint.org/docs/rules/prefer-arrow-callback.html>), [arrow-spacing](<https://eslint.org/docs/rules/arrow-spacing.html>)

    原因? 它创建了一个在 this 上下文中执行的函数版本,它通常是你想要的,并且是一个更简洁的语法。

    什么时候不适用? 如果你有一个相当复杂的函数,你可能会把这些逻辑转移到它自己的命名函数表达式里。

    // bad
    [1, 2, 3].map(function (x) {
      const y = x + 1;
      return x * y;
    });
    
    // good
    [1, 2, 3].map((x) => {
      const y = x + 1;
      return x * y;
    });
    
  • 8.2 【推荐】 如果函数体由一个没有副作用的 表达式 语句组成,删除大括号和 return。否则,保留括号并继续使用 return 语句。 eslint: [arrow-parens](<https://eslint.org/docs/rules/arrow-parens.html>), [arrow-body-style](<https://eslint.org/docs/rules/arrow-body-style.html>)

    原因? 语法糖。 多个函数被链接在一起时,提高可读性。

    // bad
    [1, 2, 3].map(number => {
      const nextNumber = number + 1;
      `A string containing the ${nextNumber}.`;
    });
    
    // good
    [1, 2, 3].map(number => `A string containing the ${number + 1}.`);
    
    // good
    [1, 2, 3].map((number) => {
      const nextNumber = number + 1;
      return `A string containing the ${nextNumber}.`;
    });
    
    // good
    [1, 2, 3].map((number, index) => ({
      [index]: number,
    }));
    
    // 没有副作用的隐式返回
    function foo(callback) {
      const val = callback();
      if (val === true) {
        // 如果回调返回 true 执行
      }
    }
    
    let bool = false;
    
    // bad
    foo(() => bool = true);
    
    // good
    foo(() => {
      bool = true;
    });
    
  • 8.3 【推荐】 如果表达式跨越多个行,用括号将其括起来,以获得更好的可读性。

    原因? 它清楚地表明了函数的起点和终点。

  // bad
  ['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call(
      httpMagicObjectWithAVeryLongName,
      httpMethod,
    )
  );

  // good
  ['get', 'post', 'put'].map(httpMethod => (
    Object.prototype.hasOwnProperty.call(
      httpMagicObjectWithAVeryLongName,
      httpMethod,
    )
  ));
  • 8.4 【推荐】 如果你的函数只有一个参数并且函数体没有大括号,就删除圆括号。 否则,为了保证清晰和一致性,请给参数加上括号。 注意:总是使用括号是可以接受的,在这种情况下,我们使用 “always” option 来配置 eslint. eslint: [arrow-parens](<https://eslint.org/docs/rules/arrow-parens.html>)

    原因? 让代码看上去不那么乱。

    // bad
    [1, 2, 3].map((x) => x * x);
    
    // good
    [1, 2, 3].map(x => x * x);
    
    // good
    [1, 2, 3].map(number => (
      `A long string with the ${number}. It’s so long that we don’t want it to take up space on the .map line!`
    ));
    
    // bad
    [1, 2, 3].map(x => {
      const y = x + 1;
      return x * y;
    });
    
    // good
    [1, 2, 3].map((x) => {
      const y = x + 1;
      return x * y;
    });
    
  • 8.5 【推荐】 避免搞混箭头函数符号 (=>) 和比较运算符 (<=, >=)。 eslint: [no-confusing-arrow](<https://eslint.org/docs/rules/no-confusing-arrow>)

    // bad
    const itemHeight = item => item.height > 256 ? item.largeSize : item.smallSize;
    
    // bad
    const itemHeight = (item) => item.height > 256 ? item.largeSize : item.smallSize;
    
    // good
    const itemHeight = item => (item.height > 256 ? item.largeSize : item.smallSize);
    
    // good
    const itemHeight = (item) => {
      const { height, largeSize, smallSize } = item;
      return height > 256 ? largeSize : smallSize;
    };
    
  • 8.6 【推荐】 在箭头函数用隐式 return 时强制将函数体的位置约束在箭头后。 eslint: [implicit-arrow-linebreak](<https://eslint.org/docs/rules/implicit-arrow-linebreak>)

    // bad
    (foo) =>
      bar;
    
    (foo) =>
      (bar);
    
    // good
    (foo) => bar;
    (foo) => (bar);
    (foo) => (
        bar
    );
    

9. 类和构造器

react

Airbnb React/JSX Style Guide

github.com/lin-123/jav…

其他补充

1.注释

1.1 文件顶部的注释,包括描述、作者、日期

/**
 * @description xxxxxx
 * @author XXX
 * @since 19/05/21
 */

1.2 模块的注释

/**
 * 拷贝数据
 * @param  {*}  data   要拷贝的源数据
 * @param  {boolean} [isDeep=false] 是否深拷贝,默认浅拷贝
 * @return {*}         返回拷贝后的数据
 */

1.3 业务代码注释

/*业务代码注释*/

1.4 变量注释

interface IState {
  // 名字
  name: string;
  // 电话
  phone: number;
  // 地址
  address: string;
}
  1. 引用组件顺序(必须)

先引用外部组件库,,再引用当前组件块级组件, 然后是 common 里的公共函数库最后是 css 样式

import * as React from 'react';
import { Dropdown, Menu, Icon } from 'antd';
import Header from './Header';
import toast from 'common/toast';
import './index.less';
  1. 括号(推荐)

下列关键字后必须有大括号(即使代码块的内容只有一行):if, else, for, while, do, switch, try, catch, finally, with。

// not good
if (condition) doSomething();

// good
if (condition) {
  doSomething();
}
  1. 类型断言
// bad
function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

// bad 
function getLength(something: string | number): number {
    if ((<string>something).length) {
        return (<string>something).length;
    } else {
        return something.toString().length;
    }
}

// good
function getLength(something: string | number): number {
  if (typeof something === 'string') {
    return something.length;
  } else {
    return something.toString().length;
  }
}
  1. interface声明顺序

日常用到比较多的是四种,只读参数放第一位,必选参数第二位,可选参数次之,不确定参数放最后

interface iProps {
  readonly x: number;
  readonly y: number;
  name: string;
  age: number;
  height?: number;
  [propName: string]: any;
}
  1. ts好用的相关工具泛型
  • Record<string,any> 用这个来声明对象结构的类型
用于定义一个javascript的对象,key是字符串,value是任意类型
const people:Record<string,any> = {
    name: 'chengfeng',
    age: 10
}
  • Partial 作用是将传入的属性变为可选项.
interface iPeople {
    title: string;
    name: string;
}

const people: Partial<iPeople> = {
    title: 'Delete inactive users',
};
定义的结构可以是接口iPeople的任意key
  • Readonly 作用是将传入的属性变为变成只读
interface iPeople {
    title: string;
    name: string;
}

const people: Readonly<Todo> = {
    title: 'todo list',
    name: chenfeng;
};
title name属性就是只读的了
  • Required 的作用是将传入的属性变为必选项
interface iPeople {
    title?: string;
    name?: string;
}

const people1: Props = { title: 'ts' }; // OK

const people22: Required<iPeople> = { title: 'ts' }; // Error: property 'name' missing
  1. 渲染默认值
  • 添加非空判断可以提高代码的稳健性,例如后端返回的一些值,可能会出现不存在的情况,应该要给默认值.
// bad
render(){
  {name}
}

// good
render(){
  {!!name || '--'}
}
  • 还有一种情况,就是本来后端应该返回一个数组给你,但是数据库取不到数据,可能后端给你返回了null,然后前端null.length。
// bad
const { list, totalCount } = await getPeopleList(keyword, page, pageSize);
list 可能是null或者undefined
list.length将直接导致前端报错

this.setState({
  status: STATUS.READY,
  apps: list,
  total: totalCount,
  page: page,
});

// good 
const { list, totalCount } = await getPeopleList(keyword, page, pageSize);
this.setState({
  status: STATUS.READY,
  apps: list || [],
  total: totalCount || 0,
  page: page,
});
  1. 业务代码里面的异步请求需要 try catch
getStudentList = async () => {
  try {
    this.setState({
      loading: true,
      isEmpty: false
    });
    await getStudentList({});
  } catch (e) {
    // TODO
    console.log(e)
  } finally {
    //  失败之后的一些兜底操作
    this.setState({
      loading: false,
      isEmpty: true
    });
  }
};
  1. setState可能是同步的
  • setState 在react里的合成事件和钩子函数中是“异步”的。
  • setState 在原生事件和 setTimeout 中是同步的。
  1. 第三方库函数的使用

用 try catch 包裹,防止第三方库的出现错误,导致整个程序崩溃

/*
 * Echart 用于代绘制图表,但当其自身发生错误时,可能影响到业务代码的执行
 */
// bad
const iniDom = document.getElementById('init-container');
const echartObj = echarts.init(iniDom);
this.setState(
  {
    echartObj
  },
  () => {
    const { echartObj } = this.state;
    // 更新图表
    echartObj.setOption(CHART_CONFIG, true);
  }
);

// good
try {
  const iniDom = document.getElementById('init-container');
  const echartObj = echarts.init(iniDom);
  this.setState(
    {
      echartObj
    },
    () => {
      const { echartObj } = this.state;
      // 更新图表
      echartObj.setOption(CHART_CONFIG, true);
    }
  );
} catch (error) {
  // TODO
}
  1. Event 事件对象类型

很多小伙伴用了很久的ts,都不知道常用 Event 事件对象类型:

ClipboardEvent<T = Element> 剪贴板事件对象

DragEvent<T = Element> 拖拽事件对象

ChangeEvent<T = Element> Change 事件对象

KeyboardEvent<T = Element> 键盘事件对象

MouseEvent<T = Element> 鼠标事件对象

TouchEvent<T = Element> 触摸事件对象

WheelEvent<T = Element> 滚轮事件对象

AnimationEvent<T = Element> 动画事件对象

TransitionEvent<T = Element> 过渡事件对象

import { MouseEvent } from 'react';

interface IProps {
  onClick(event: MouseEvent<HTMLDivElement>): void;
}
  1. 使用私有属性取代state状态(hooks版 用useRef)

对于一些不需要控制ui的状态属性,我们可以直接绑到this上, 即私有属性,没有必要弄到this.state上,不然会触发渲染机制,造成性能浪费 例如请求翻页数据的时候,我们都会有个变量。

// bad
state: IState = {
  pageNo:1,
  pageSize:10
};

// good 
queryParams:Record<string,any> = {
  pageNo:1,
  pageSize:10
}
  1. if else 等判断太多了,后期难以维护。

职责链模式

我们可以通过存储【判定规则】的数组,来实现行为。如果规则匹配,那么就执行这条规则对应的分支。我们把这样的数组称为【职责链】

const rules = [
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  },
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  },
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  }
  // ...
]

rules 中的每一项都具有 match 与 action 属性。这时我们可以将原有函数的 else if 改写对职责链数组的遍历:

function demo (a, b, c) {
  for (let i = 0; i < rules.length; i++) {
    if (rules[i].match(a, b, c)) {
      return rules[i].action(a, b, c)
    }
  }
}

ramda 里面的cond 用起来很舒服

import {  cond, T, compose,equals,always } from 'ramda';
const fn = cond([
  [equals(0),   always('water freezes at 0°C')],
  [equals(100), always('water boils at 100°C')],
  [T,           temp => 'nothing special happens at ' + temp + '°C']
]);
fn(0); //=> 'water freezes at 0°C'
fn(50); //=> 'nothing special happens at 50°C'
fn(100); //=> 'water boils at 100°C
  1. 提前实施useReducer Hook

    React中最常使用的Hook之一是**useState.**在过去的时间里,我创建和看到的组件都有很多不同的 状态。所以很自然地,它们会被大量的useState Hook所淹没。

const CustomersMap = () => {
  const [isDataLoading, setIsDataLoading] = useState(false)
  const [customersData, setCustomersData] = useState([])
  const [hasError, setHasError] = useState(false)
  const [isHovered, setIsHovered] = useState(false)
  const [hasMapLoaded, setHasMapLoaded] = useState(false)
  const [mapData, setMapData] = useState({})
  const [formData, setFormData] = useState({})
  const [isBtnDisabled, setIsBtnDisabled] = useState(false)
  
  ...
  
  return ( ... )
}

如果你能创建一些较小的子组件,在那里你可以转换一些state和JSX,那么这是一个很好的方法。这样你就可以一步到位地清理你的useState Hook和你的JSX。

在我们上面的例子中,我们可以把最后两个状态(states)放到一个单独的组件中,这个组件处理所有与表单有关的状态(state)和JSX。

但在有些情况下,这样做是没有意义的,你必须把这些不同的状态(states)放在一个组件里。为了提高你的组件的可读性,有一个useReducer钩。

useReducer 当你有复杂的状态逻辑(state logic),涉及到多个子值,或者下一个状态(state)取决于上一个状态(state)时,通常比useState更可取。useReducer还可以让你优化触发深度更新的组件的性能,因为你可以把调度(dispatch)传递下去而不是回调(callbacks)。

考虑到这一点,该组件在使用useReducer时就会变成这样:

// INITIAL STATE
const initialState = {
  isDataLoading: false,
  customerData: [],
  hasError: false,
  isHovered: false,
  hasMapLoaded: false,
  mapData: {},
  formdata: {},
  isBtnDisabled: false
}

// REDUCER
const reducer = (state, action) => {
  switch (action.type) {
    case 'POPULATE_CUSTOMER_DATA':
      return {
        ...state,
        customerData: action.payload
      }
    case 'LOAD_MAP':
      return {
        ...state,
        hasMapLoaded: true
      }
    ...
    ...
    ...
    default: {
      return state
    }	
  }
}

// COMPONENT
const CustomersMap = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  
  ...
  
  return ( ... )
}

该组件本身看起来更干净,并伴随着一些巨大的好处,你可以在文档中看到。如果你已经习惯了Redux,reducer的概念和它的构建方式对你来说并不陌生。

我个人的规则是,如果我的组件超过了四个useState Hook,或者状态(state)本身比布尔值更复杂,就使用useReducer Hook。

  1. 对齐

对 JSX 语法使用这些对齐风格。 eslint: [react/jsx-closing-bracket-location](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-closing-bracket-location.md>) [react/jsx-closing-tag-location](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-closing-tag-location.md>)

// bad
<Foo superLongParam="bar"
     anotherSuperLongParam="baz" />

// good
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
/>

// 如果能放在一行,也可以用单行表示
<Foo bar="bar" />

// Foo 里面的标签正常缩进
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
>
  <Quux />
</Foo>

// bad
{showButton &&
  <Button />
}

// bad
{
  showButton &&
    <Button />
}

// good
{showButton && (
  <Button />
)}

// good
{showButton && <Button />}
  1. 引用

    1. 在 JSX 属性中用双引号("),但是在js里用单引号(')。eslint: [jsx-quotes](<https://eslint.org/docs/rules/jsx-quotes>)
    2. Why? 正常的 HTML 属性也通常使用双引号而不是单引号,所以 JSX 属性也使用这个约定。

    2. // bad
<Foo variant="stuff"></Foo>

// good
<Foo variant="stuff" />
  1. 在自闭和标签内空一格。 eslint: [no-multi-spaces](<https://eslint.org/docs/rules/no-multi-spaces>), [react/jsx-tag-spacing](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-tag-spacing.md>)
// bad
<Foo/>

// very bad
<Foo                 />

// bad
<Foo
 />

// good
<Foo />
  1. JSX 里的大括号不要空格。 eslint: [react/jsx-curly-spacing](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md>)
// bad
    <Foo bar={ baz } />
// good
    <Foo bar={baz} />
  1. 当没有子元素时,最好用自闭合标签。 eslint: [react/self-closing-comp](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md>)
// bad
<Foo variant="stuff"></Foo>

// good
<Foo variant="stuff" />
  1. 如果你的组件有多行属性,用他的闭合标签单独作为结束行。 eslint: [react/jsx-closing-bracket-location](<https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-closing-bracket-location.md>)
// bad
<Foo
  bar="bar"
  baz="baz" />

// good
<Foo
  bar="bar"
  baz="baz"
/>

参考资料

1.中文版翻译

2.imweb代码规范

3.github.com/Khan/style-…

4.juejin.cn/post/684490…

  1. clean-code-javascript
  2. CUPID****好代码的五个特质 CUPID—for joyful coding**
  3. React 最佳实践--如何写出更好的 React 代码
  4. 精读《React Hooks 最佳实践》
  5. A Complete Guide to useEffect
  6. 前端React最佳实践