ecomfe / spec

This repository contains the specifications.
4.63k stars 1.61k forks source link

关于箭头函数只有一个参数的括号问题 #28

Closed otakustay closed 8 years ago

otakustay commented 8 years ago

我们现在的规则是箭头函数只有一个参数,则此参数必须省略

真实的情况是,只有一个参数的箭头函数会有不同场景不同状态,虽然没有明确的结论,但我想我们应该对各种情况再重新审视一下:

普通的多行函数定义

let hash = str => {
    let compute = require('md5');
    return compute(str);
};

普通的单行函数定义

let hash = str => require('md5').compute(str);

函数工厂

let hook = controller => route => console.log(route.url);

let hook = controller => route => action => console.log(action.context);

let hook = controller => route => action => {
    let context = action.context;
    console.log(context.url, context.args);
};

用作参数的单语句函数

let list = names.map(name => name.split(' '));

用作参数的多语句函数

let list = names.map(name => {
    let [firstName, lastName] = name.split(' ');
    return {firstName, lastName};
});

异步函数

let fetch = async id => request(`users/${id}`)

let fetchName = async id => {
    let user = await request(`users/${id}`);
    return user.fullName;
};

顺便箭头+generator是早晚的事,不知道到时候会长啥样……讨论在这边:https://esdiscuss.org/topic/generator-arrow-functions

数组解构

let getFullName = [first, last] => first + ' ' + last

对象解构

let getFullName = ({first, last}) => first + ' ' + last

解构我们现在是要求加括号的,主要原因是对象解构不加括号跑不了……

另外解构也会和多行的组合,效果又有点不一样,可以写着感受下

参数展开

let warn = ...args => console.error(...args);

let filterAndJoin = ...args => {
    return args.filter(s => !!s)
        .map(s => s.replace(/ /g, ''))
        .join(' ')
};

---

这些情况还有可能是组合的,比如在参数中用异步函数等等……

我个人在实践中的感受是,只有“用作参数的单语句函数”这一情况不加括号是另人比较舒服的,理由如下:

1. 作为参数时,前后都已经有括号,使得这个表达式有明确的边界,容易识别
2. 作为参数时是一个callback,其参数个数由当前正在调用的函数(如`.filter`或`.map`)决定的,出于接口的稳定性,这种callback的参数个数基本不会发生变化,所以不会有一会儿多了个参数要加括号了,一会儿参数又减掉了要把括号去掉了这种情况

其它情况下,因为一些原因,我更倾向于加括号:

1. 缺少边界的情况下,参数和函数体的识别变得困难,特别是在多级函数工厂再配上一个单语句函数体的情况下
2. 由于解构和参数展开的存在,使得参数并不是一个简单的identifier,前面后面经常会有杂七杂八的东西,少个括号包起来会造成阅读上一定程度的麻烦(要脑内look ahead之类的)
3. 因为`async`和未来的generator的存在,参数前面还可能出现不同的关键字等,又会造成这部分和参数之间也缺少足够明确的分隔

所以我们是不是集众人之力,对不同情况做一下判断,再整合出一个标准来?
errorrik commented 8 years ago

个人感觉,普通函数和函数工厂的case,用普通function是不是感觉好过arrow function?

写些代码做了下比较。个人感觉,当this无关时:

  1. 多行函数在声明时,使用箭头函数,并没有多大的收益
  2. 单行函数在声明时,也没有多大的收益
  3. 多行函数做为callback时,收益不大
  4. 单行函数做为callback时,可读性提升明显
  5. 工厂函数下,使用箭头函数反而不好读
  6. 单参数有解构场景,收益还是不错的

so,我提议:

  1. 是否具名函数?是跳到2,否跳到3
  2. 是否需要绑定this?是跳到3,否跳到4
  3. 允许使用箭头函数
  4. 不允许使用箭头函数

普通的多行函数定义

let hash = str => {
    let compute = require('md5');
    return compute(str);
};

fucntion hash(str) {
    let compute = require('md5');
    return compute(str);
}

普通的单行函数定义

let hash = str => require('md5').compute(str);

fucntion hash(str) {
    return require('md5').compute(str);
}

函数工厂

let hook = controller => route => console.log(route.url);
function hook(controller) {
    return function (route) {
        return console.log(route.url);
    };
}

用作参数的单语句函数

let list = names.map(name => name.split(' '));

let list = name.map(function (name) {
    return name.split(' ');
});

用作参数的多语句函数

let list = names.map(name => {
    let [firstName, lastName] = name.split(' ');
    return {firstName, lastName};
});

let list = name.map(function (name) {
    let [firstName, lastName] = name.split(' ');
    return {firstName, lastName};
});

数组解构

let getFullName = [first, last] => first + ' ' + last

function getFullName(str) {
    let [first, last] = str;
    return first + ' ' + last;
}

对象解构

let getFullName = ({first, last}) => first + ' ' + last

function getFullName(str) {
    let {first, last} = str;
    return first + ' ' + last;
}

参数展开

let warn = ...args => console.error(...args);
function warn(...args) {
    return console.error(...args);
}

let filterAndJoin = ...args => {
    return args.filter(s => !!s)
        .map(s => s.replace(/ /g, ''))
        .join(' ')
};

function filterAndJoin(...args) {
    return args.filter(s => !!s)
        .map(s => s.replace(/ /g, ''))
        .join(' ')
}
hax commented 8 years ago

数组解构也是需要括号的。

var f = [a] => a

在Firefox和Babel中都报语法错误。 必须写成:

var f = ([a]) => a
errorrik commented 8 years ago

👍 @hax

@otakustay 现在的条目是 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略。,对于解构的场景是ok的

hax commented 8 years ago

我现在倾向于,除了callback的场景,其他还是用普通 functions 而不是 arrow functions。

@otakustay 的例子都可以按这个原则处理,除了“函数工厂”(实际是柯里化?)的例子 —— 但我自己其实很少用柯里化,需要足够的 use cases 证明其用途(参见反对使用柯里化的意见http://zhuanlan.zhihu.com/dummydigit/20165394 ),且我们始终可以用显式的柯里化,如下:

function hook(controller, route, action) {
    let context = action.context;
    console.log(context.url, context.args);
}
const curriedHook = hook::curry()
errorrik commented 8 years ago

我现在倾向于,除了callback的场景,其他还是用普通 functions 而不是 arrow functions。

我同意这个。不过可以再加个前提条件,除非需要this

otakustay commented 8 years ago

在使用OO主要场景的系统中,会有大量的函数是需要绑定this的,如果我们的规则是“尽量不使用箭头函数”的话,会出现代码中混杂着箭头函数和普通function,这种不一致感是否会产生一些不适感?

函数工厂比较典型的是redux的middleware,贴个官方的示例:

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

@hax 你不是连顶层作用域也倾向用箭头吗……

hax commented 8 years ago

@otakustay 按我现在的规则(只在callback时优先使用arrow functions),是无所谓是否在顶层作用域的。至于this绑定,大多数callback的场景是没有使用动态this的需求的(也就是要么没有this,要么绑定的是lexcial this)。一些可以用this的callback场景,比如事件处理,其this也多可以用其他方式替代(如 event.currentTarget)。

redux 没用过,不过看其文档,也就是在需要 currying 时采用了 arrow functions。那么可以将 currying 作为一个额外的 arrow functions 的适用场景。不过如我前面所说,currying 的合理 use cases 并不多(包括 redux 这里我也不确定是否真的合理),且可以转换为其他形式。

otakustay commented 8 years ago

@hax 如果一个函数单独定义,但后续被用来作为callback

let inc = x => x + 1;

list.map(inc);

这种情况如何处理,认为是“在callback时”的情况吗?如果是的话,同样会遇到本主题讨论的各种问题

errorrik commented 8 years ago

终于看明白。我的观点和 @hax 还是有些小区别。

@hax 观点是,callback时 优先 使用arrow function。我的观点是,callback时 允许 使用arrow function。

errorrik commented 8 years ago

这种情况如何处理,认为是“在callback时”的情况吗?如果是的话,同样会遇到本主题讨论的各种问题

这种我觉得不算吧

otakustay commented 8 years ago

这种我觉得不算吧

会产生一些问题,比如我有2个事件需要同样的一个处理函数(这很常见,比如给不同元素绑相同功能,或者鼠标/键盘绑同样的功能),这个处理函数通常是会独立写的,而这个函数也很多时候是需要lexical this的

errorrik commented 8 years ago

会产生一些问题,比如我有2个事件需要同样的一个处理函数(这很常见,比如给不同元素绑相同功能,或者鼠标/键盘绑同样的功能),这个处理函数通常是会独立写的,而这个函数也很多时候是需要lexical this的

嗯,所以我的观点是:

  1. 是否callback?是跳到3,否跳到2
  2. 是否需要绑定this?是跳到3,否跳到4
  3. 允许使用箭头函数
  4. 不允许使用箭头函数

总结一下,[强制] 在非callback的场景,并且不需要lexical this时,不允许使用arrow function

otakustay commented 8 years ago

首先对这个结论我本身会需要一些时间去接受,不过问题不大

但这并没有解决我们这个Issue主要讨论的问题,依旧有使用arrow function的场景,依旧会有多行、解构、异步等情况及这些情况的各种组件存在,对只有一个参数时如何处理……

otakustay commented 8 years ago

我说下这几天我自己得出来的一个结论,当然和你们2位的有很大区别:

  1. 尽量使用箭头函数
  2. 仅当满足以下全部条件时,省略参数的括号。其它情况参数必须添加括号
    • 箭头函数表达式作为参数使用
    • 函数仅有一个参数
    • 函数体只有一个语句
  3. 仅当满足以下全部条件时,省略函数体的大括号,其它情况函数体必须添加左右大括号
    • 函数只有一个语句
    • 函数体不存在换行
    • 该语句不是一个对象字面量
    • 不存在多层的箭头函数

第2条和async与否无关,事实上基于Promise的异步流程控制下几乎不存在一个async函数当参数去用的场景

这样规定的目的是在很多容易影响阅读的场景下强制让箭头函数是多行且有函数体的大括号作为分界线的,这样相比普通的函数少写了function关键字省力但又不影响整个代码本身的换行和缩进的结构

好的IDE/Editor会将箭头函数的=>高亮,事实上效果要比高亮function更好,不仅仅能标识一个函数,还能分隔参数和函数体

errorrik commented 8 years ago

首先对这个结论我本身会需要一些时间去接受,不过问题不大

这只是我的个人提议,可以有其它结论诶,扔给投票就是了。

但这并没有解决我们这个Issue主要讨论的问题,依旧有使用arrow function的场景,依旧会有多行、解构、异步等情况及这些情况的各种组件存在,对只有一个参数时如何处理……

我们现在的条目是:[强制] 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略。

我觉得唯一可能有可读性障碍的是类似这样用箭头函数定义具名函数的代码:

let foo = bar => u.camelize(bar);

这种情况下,假设[强制] 在非callback的场景,并且不需要lexical this时,不允许使用arrow function,基本可以屏蔽掉单行箭头函数定义具名函数的场景。多行箭头函数定义具名函数的可读性又会好得多。所以我觉得可忽略。

otakustay commented 8 years ago

这条实践中恐怕并不能屏蔽掉单行箭头具名函数,仅一行的需要lexical this的函数是很多的,它们不被用在callback或被多处callback共享也是常见的,典型如:

let redirect = e => this.navigateTo(e.target.href);

$('#navigator a').on('click', redirect);
$('#foot-navigator a').on('click', redirect);

当然可以把selector写一起,但并不见得是好事

我就建议这种场景参数的括号不要省,或者不允许只写一行必须加{}

errorrik commented 8 years ago

@otakustay 提到的场景确实存在。不过我不太明白的是这种场景下为啥参数的括号不要省

仅当满足以下全部条件时,省略参数的括号 函数体只有一个语句

总觉得根据函数体决定要不要写参数括号,这个逻辑有点怪

errorrik commented 8 years ago

关于参数的括号

我觉得[强制] 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略。其实挺好的。因为:

// 加了括号也不见得提高了可读性

let hash = str => {
    let compute = require('md5');
    return compute(str);
};

let hash = (str) => {
    let compute = require('md5');
    return compute(str);
};

关于箭头函数体

现在有两条

[强制] 箭头函数的函数体只有一个非 Object Literal 的单行表达式语句,且作为返回值时,必须省略 {} 和 return。 [强制] 箭头函数的函数体只有一个 Object Literal,且作为返回值时,不得省略 {} 和 return。

其实涵盖了 @otakustay 的提议中的前3条:

仅当满足以下全部条件时,省略函数体的大括号,其它情况函数体必须添加左右大括号

至于多层的箭头函数,我觉得你要真能写成一行,我也能接受。

const logger = store => next => action => {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
};
// 这样的代码看着也不难受,一眼就知道那个{}body对应的是什么

关于尽量使用箭头函数

其实我有些不太确定,是不是我老了不能接受新事物了。我只提一个场景的问题:如果你有一堆this无关的util函数,在一个文件里,你愿意写箭头函数,还是写普通函数?

otakustay commented 8 years ago

最后一点我们就留着投票算了……

const logger = store => next => action => {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
};

这个代码的核心问题不是在看不到body,而是在于已经理不出logger到底返回了啥了……实际上logger返回一个函数,这个函数会返回另一个函数,另一个函数会执行body,我第一次见的时候理出这个逻辑花了不少时间

const logger = store => {
    return next => {
        return action => {
            console.log('dispatching', action)
            let result = next(action)
            console.log('next state', store.getState())
            return result
        }
    }
};

这样不知道会不会能通过缩进更好地表达出“这是3层函数依次套”这个结构

hax commented 8 years ago

没错,我现在采用的实际上就是 eslint 的 prefer-arrow-callback 规则。这个规则其实不是说其他地方就不能用 arrow fucntions,而是工具可以确定适合 arrow functions 的地方会要求你用 arrow functions。

“2个事件需要同样的一个处理函数”我认为是应该用 arrow functions 的,因为它实际是 prefer-arrow-callback 的延伸,但本身 prefer-arrow-callback 目前并不能检测出是否违反了这条规则。

所以这是一种单向允许的策略,而不是直接非此即彼。你可以扩展边界,但是工具只检查它可以确定的case。所以这是一种保守,但是可执行性非常高的 policy。

实际上,我之前尝试过更激进的“尽量使用箭头函数”,配合使用 method shorthand 后的效果是,除了用于 bind operator 的还可以写函数外,其他地方就完全没有function关键字的存在了。但存在以下问题:

  1. Generators 还是使用了 function 关键字形式【将来也许语法上会支持】
  2. async 还是使用了 function 关键字形式【当时语法还不支持,现在其实应该可以用 async arrow functions 了】
  3. 用于 bind operator 的 function 工具并不能完全直接识别出来【也许可以通过类型声明或注释来表明】

我尝试后的结论是,长远看,“尽量使用箭头函数”是有可能的,但是目前实施起来一致性不够(有比较多的例外),工具支持也有困难。

errorrik commented 8 years ago

这个代码的核心问题不是在看不到body,而是在于已经理不出logger到底返回了啥了……实际上logger返回一个函数,这个函数会返回另一个函数,另一个函数会执行body,我第一次见的时候理出这个逻辑花了不少时间

说实话,我第一次看的时候也理了一会,再想也挺简单的,n个=>就是n层function,只有最后一个参数是这个函数执行的动态参数。

当然,这仅限于=>不间断连接的场景。这种场景阅读障碍我个人觉得还好。

长远看,“尽量使用箭头函数”是有可能的,但是目前实施有困难

ok,好吧,问题是:在未来可以达成此目标时,如果我们规定了非callback场景不建议使用箭头函数,再重新修订成尽量使用箭头函数,是一个成本较大的、涉及原则性的变更。所以,我觉得 @otakustay 可以考虑下,我们是投票决定,还是暂时放弃对箭头函数practice的指导。

hax commented 8 years ago

prefer-arrow-callback 的规则并没有“非callback场景不建议使用箭头函数”,实际上是对非callback的写法没做规定,留给开发人员自己探索。我个人认为coding style留下一些暂不确定的弹性空间是可以的。没有必要在没有充分实践的时候先作出禁止性的规定。这样当充分实践后得到新规则后,也不会变成“成本较大的、涉及原则性的变更”。

因为我们必须承认我们自己的认识不可能是完全的。而且语言本身也不是没有缺点的。既然如此,coding style也不可能是完美的,应该避免僵化,允许改进的空间。

errorrik commented 8 years ago

原先想着规定非callback场景,且无需lexical this时,不允许使用箭头函数的原因是,如果你不规定,一定会出现一个文件里全是util函数,有的用箭头函数,有的不用,这种混乱的场景。

不过,我们现今确实无法得出到箭头函数更好还是普通函数更好的实践结论,所以,我同意 @hax 的意见,如上2楼的提议:暂时放弃对箭头函数practice的指导。 @otakustay 你觉得呢?

另外, @otakustay 你觉得我们是不是需要规定prefer-arrow-callback?

otakustay commented 8 years ago

从讨论来看,我觉得我们这一期可以先放弃对箭头函数的各种规范,包括prefer-arrow-callback,一段时间后看大家的实际代码是怎么样的吧

errorrik commented 8 years ago

从讨论来看,我觉得我们这一期可以先放弃对箭头函数的各种规范,包括prefer-arrow-callback,一段时间后看大家的实际代码是怎么样的吧

ok,关于箭头函数使用的指导原则,就hold了哈。另外,关于参数括号和函数体,保持现在的条目or修订?

otakustay commented 8 years ago

[强制] 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略。

这一条额外加一下async的示例,同样省略括号

[强制] 箭头函数的函数体只有一个非 Object Literal 的单行表达式语句,且作为返回值时,必须省略 {} 和 return。

我希望改成建议,理由如上面的多层函数工厂的示例

otakustay commented 8 years ago

我又想了一下,强制prefer-arrow-callback恐怕有问题,最典型的就是jQuery这货的mapeach等的callback里是有this的,官方示例都如此:

$( ":checkbox" )
  .map(function() {
    return this.id;
  })
  .get()
  .join();

我知道其实不用this也行,但大量初级的工程师只会参考官方示例

hax commented 8 years ago

@otakustay 所以prefer-arrow-callback对于有this且后面不跟bind的并不会报error。

otakustay commented 8 years ago

两回事,其实我说的并不是eslint这条规则本身,而是我们不应该强制callback使用arrow这个概念上的事情 :)

hax commented 8 years ago

@otakustay 一回事情,prefer-arrow-callback实际上保留了这个例外。

当然我认为实际上这种保留是有代价的,就是你其实无法检测出那个是真需要this还是忘记写bind了。我个人倾向于对这种情况报warning。

上面的例子更好的代码应该是:

$( ":checkbox" )
  .map(function(i, elem) {
    return elem.id;
  })
  .get()
  .join();

这样就会报错要求你用arrow function。 当然框架api不支持(jQuery倒是支持的)也没办法,毕竟不能指望一些老框架设计本身是ok的。

otakustay commented 8 years ago

可能我用了prefer-arrow-callback这个规则名称引起了一些误解,我只想表达我们的规范不能这么些:

[强制] 作为callback的函数必须使用箭头函数

当然这大概是显而易见的所以我多虑了……

在 2015年12月10日,下午5:39,HE Shi-Jun notifications@github.com 写道:

@otakustay 一回事情,prefer-arrow-callback实际上保留了这个例外。

当然我认为实际上这种保留是有代价的,就是你其实无法检测出那个是真需要this还是忘记写bind了。我个人倾向于对这种情况报warning。

上面的例子更好的代码应该是:

$( ":checkbox" ) .map(function(elem) { return elem.id; }) .get() .join(); 这样就会报错要求你用arrow function。 当然框架api不支持也没办法,毕竟不能指望一些老框架设计本身是ok的。

— Reply to this email directly or view it on GitHub.

otakustay commented 8 years ago

权当记录,以备后续需要时能查看

箭头函数还有一个问题在于没有Hoisting效果,所以无法将一些辅助的函数集中定义在后面

当然是否应该利用Hoisting将函数往后放这一点肯定会存在分歧,我个人持中立态度

hax commented 8 years ago

@otakustay 没错。我认为辅助函数定义在后面是好的。这也算 arrow functions 的一个小问题。不过对于非即时执行的脚本来说并不会有什么问题。对于需要即时执行的,我倾向于将复杂的实现部分抽成单独模块,其中即时执行通常是初始化,包成init函数导出,然后client代码调用init()。这样就不会有问题了。

otakustay commented 8 years ago

很遗憾的是我们的代码检测工具对于非立即执行的代码也会判断“先定义后调用”这一点,比如

function foo() {
    console.log(bar);
}

let bar = 3;

这个代码我们的工具会报错,bar是个函数也差不多, @firede 能确认下这个事吗

hax commented 8 years ago

@otakustay 既然是自己开发lint工具,那就可以改实现嘛。 :laughing:

errorrik commented 8 years ago

为什么是 @firede ,难道不是 @chriswong

chriswong commented 8 years ago

现在是除了函数声明可以延后定义,其它的必须出现在调用的代码之前,因为你无法保证真正调用时变量已经 init。这样是允许的:

function foo() {
    console.log(bar);
}

function bar() {
}
errorrik commented 8 years ago

根据上面的讨论,总结下:

  1. 尽量使用箭头函数 在当前条件下不现实,所以,暂时不对箭头函数的使用进行任何限制。
  2. 保持[强制] 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略不变
  3. 由于多个=>串起来的代码可读性问题,[强制] 箭头函数的函数体只有一个非 Object Literal 的单行表达式语句,且作为返回值时,必须省略 {} 和 return改成建议
hax commented 8 years ago

@chriswong 如果变量定义是在top level的,那么可以保证真正调用时变量已经 init。除非初始化代码里调用了其他内容(例如模块循环引用),但这种情况比较罕见。