在ES6大行其道的今天,不应用点ES6特性似乎有些政治不正确。最近刚好有个Node的项目,最低要支持到nodejs 4.0,在node.green看了下ES6的支持度,我想使用的特性基本都有支持,遂决定在新项目中采用ES6来写。
当然第一件事情就是毫不留情地消灭var,项目中能用const的地方不用let,能用let的地方不用var。
第二件事情就是使用劳动人民喜闻乐见的箭头函数替代function。当我心满意足地看到满屏的=>
时,现实给了我一记响亮的耳光——改过之后的程序错误百出!
所以,当我们使用箭头函数时,一定要搞清楚箭头函数是什么回事,适用于什么场景。本文就针对以上问题来讨论下箭头函数。
箭头函数是什么?
箭头函数的语法我就不讲了,相信大家都见识过。跟我一样,大家喜欢箭头函数90%的原因是它好看。除了好看,它是不是与function等价呢?肯定不等价,因为TC39不可能仅因为好看而引入一个语法糖(class除外)。
箭头函数的渊源可以追溯到上古时期一个叫lambda演算的东西。lambda演算是数学家提出来的,有些数学家跟我们程序员一样也很懒,数学定理那么多,今天要证三角定律,明天要证勾股定律,累不累!那能不能将所有的证明问题用一个统一的体系进行形式化描述,然后由机器来完成自动推导呢?lambda演算就是干这个的,图灵也搞了一套体系叫图灵机,两者是等价的。
关于lambda演算说了这么多,好像跟今天要讲的箭头函数没什么关系?其实是有关系的,lambda演算深刻影响了箭头函数的设计。数学家们喜欢用纯函数式编程语言,纯函数的特点是没有副作用,给予特定的输入,总是产生确定的输出,甚至有些情况下通过输出能够反推输入。要实现纯函数,必须使函数的执行过程不依赖于任何外部状态,整个函数就像一个数学公式,给定一套输入参数,不管是在地球上还是火星上执行都是同一个结果。
箭头函数要实现类似纯函数的效果,必须剔除外部状态。所以当你定义一个箭头函数,在普通函数里常见的this
、arguments
、caller
是统统没有的。
箭头函数没有this
箭头函数没有this
,那下面的代码明显可以取到this
啊:
function foo() {
this.a = 1
let b = () => console.log(this.a)
b()
}
foo() // 1
以上箭头函数中的this
其实是父级作用域中的this
,即函数foo
的this
。箭头函数引用了父级的变量,构成了一个闭包。以上代码等价于:
function foo() {
this.a = 1
let self = this
let b = () => console.log(self.a)
b()
}
foo() // 1
箭头函数不仅没有this
,常用的arguments
也没有。如果你能获取到arguments
,那它一定是来自父作用域的。
function foo() {
return () => console.log(arguments[0])
}
foo(1, 2)(3, 4) // 1
上例中如果箭头函数有arguments
,就应该输出的是3而不是1。
一个经常犯的错误是使用箭头函数定义对象的方法,如:
let a = {
foo: 1,
bar: () => console.log(this.foo)
}
a.bar() //undefined
以上代码中,箭头函数中的this
并不是指向a
这个对象。对象a
并不能构成一个作用域,所以再往上到达全局作用域,this
就指向全局作用域。如果我们使用普通函数的定义方法,输出结果就符合预期,这是因为a.bar()
函数执行时作用域绑定到了a
对象。
let a = {
foo: 1,
bar: function() { console.log(this.foo) }
}
a.bar() // 1
另一个错误是在原型上使用箭头函数,如:
function A() {
this.foo = 1
}
A.prototype.bar = () => console.log(this.foo)
let a = new A()
a.bar() //undefined
同样,箭头函数中的this
不是指向A
,而是根据变量查找规则回溯到了全局作用域。同样,使用普通函数就不存在问题。
通过以上说明,我们可以看出,箭头函数除了传入的参数之外,真的是什么都没有!如果你在箭头函数引用了this
、arguments
或者参数之外的变量,那它们一定不是箭头函数本身包含的,而是从父级作用域继承的。
什么情况下该使用箭头函数
到这里,我们可以发现箭头函数并不是万金油,稍不留神就会踩坑。
至于什么情况该使用箭头函数,《You Don’t Know JS》给出了一个决策图:
以上决策图看起来有点复杂,我认为有三点比较重要:
- 箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如用在
map
、reduce
、filter
的回调函数定义中; - 不要在最外层定义箭头函数,因为在函数内部操作
this
会很容易污染全局作用域。最起码在箭头函数外部包一层普通函数,将this
控制在可见的范围内; - 如开头所述,箭头函数最吸引人的地方是简洁。在有多层函数嵌套的情况下,箭头函数的简洁性并没有很大的提升,反而影响了函数的作用范围的识别度,这种情况不建议使用箭头函数。
写的不错,可以转到公众号么?
在保证代码可读性的前提下,是可以考虑适当使用的
@i5ting 拿去,不谢,哈哈
@i5ting 公众号名是啥呀
@ldyfy Node全栈,搜nodeonly即可
这里应该有错误, 输出的应该是 undefined
function foo() {
let a = 1
let b = () => console.log(this.a)
b()
}
foo() // 1
当代码执行 foo()
的时候, 这个执行 context 的 this
指向的是 全局对象, 而函数内的 let a=1
声明的 a
存在于这个 执行context 中, 并没有成为全局对象的属性
@William17
顶
我就记住普通函数的this是绑定到执行时的对象中的
这个foo()
是在全局的,所以是没有a
这个对象的
说白了就是箭头函数没有自己的this,注意这一点就好了
基本只在纯函数中使用,箭头函数嵌套很容易看晕,还不如改成function嵌套…
为啥我感觉例子就是有问题的。。。
function foo() {
let a = 1
let b = () => console.log(this.a)
b()
}
foo() // 1
这段代码因为使用了let,因此只能在strict模式下跑,而strict模式下foo函数的this是undefined,怎么能打印出this.a来呢? 而且就算把let换成var,让这段函数在非strict模式下跑,此时this指向的是global,var a是foo函数作用于定义的,global.a明显是undefined啊
ES6之所以从函数分离出一个箭头函数,就是因为之前函数承载了太多的东西,构造器,this,arguments,等等,新的箭头函数把这些和“函数”无关的东西统统去掉,就是安安静静的做一个纯函数应有的功能。所以,用与不用,少用还是多用,其实没那么复杂,如果只是纯函数,为什么不用,如果涉及到词法作用域,这个时候就小心了,注意箭头函数的this绑定问题即可
@William17 @hyj1991 @hezhongfeng 你们说得对,示例已经做了相应的改动。看来我还需要把《You Don’t Know JS》好好看下
就是this和argument和平时不一样,话说箭头函数的坑基本都踩过,是需要慎用
踩过几次坑就知道啥时候能用,啥时候不能用啦 哈哈
现阶段还没遇到过头疼的坑,我写箭头函数主要是为了告诉别人让别人看到我会es6
谢谢,提醒!!!!!!!!!!!!!!!!!!
-。 - 我一直都是反对派,就是没人鸟我,终于看到有人跟我持差不多意见的了,包括各大厂商各大开源库都滥用了。
以前一直理解=>
是 this 绑定了函数定义时作用域,现在感觉没有 this 的这种说法真是更好理解啊!
😁 曾经滥用过一段,被 @xadillax 大神纠正过来了
就像async 和 await 出来之后也会被滥用一样… 本该异步的也不异步了…
@artisan 我默默的点个头 我也是这个原因😄
给个“滥用”箭头函数的实例吧
来自酷炫的 CNodeMD
。app出了问题。。。。。我想表达的是,我有一个“滥用”箭头的实例,带引号的那种 复制代码爆炸了(如下),直接上我知乎回答链接好了 http://www.zhihu.com/question/50571253/answer/121608813
'use strict'; const Y = proc => (x => proc(y => (x(x))(y)))(x => proc(y => (x(x))(y))); const $typeis = target => n => typeof n === target; const $undefined = n => $typeis('undefined')(n); const $_ = _ => () => _; const $if = cond => func => cond ? func() : undefined; const $ifelse = cond => func => nega => cond ? func() : nega(); const $notnull = it => func => $if(!$undefined(it) && it !== null)(() => func(it)); const $switch = (...condmap) => (...params) => $notnull(condmap.find(({cond}) => cond(...params)))(({func}) => func(...params)); const $same = a => b => a === b; const $log = obj => (console.log('log', obj), obj); const tokenize = code => ((tokenRegExp, results) => (Y(getToken => () => [tokenRegExp.exec(code)].filter(r => r !== null).map(m => results.push(m[1])).forEach(getToken))(), results))(/\s*([A-Za-z][A-Za-z0-9]*|\d+(\.\d+)?|\.\d+|\S)\s*/g, []); const isNumber = token => !$undefined(token) && token.match(/^\d+(\.\d+)?|\.\d+$/) !== null; const isName = token => !$undefined(token) && token.match(/^[A-Za-z][A-Za-z0-9]*$/) !== null; const makeStream = tokens => (pos => ({ get: () => tokens[pos], next: () => (ret => (() => ret)(pos++))(tokens[pos]), cancel: () => pos--, preview: () => tokens[pos + 1], skip: func => (ret => (() => ret)(pos++))(func(tokens[pos])), done: () => pos >= tokens.length }))(0); const genParser = (...conds) => next => stream => Y(rec => last => (left => (token => $ifelse(conds.some($same(token)))(() => (ret => $ifelse(stream.done())($_(ret))(() => rec(ret)))({type: token, left, right: next(stream)}))(() => (() => left)(stream.cancel())))(stream.next()))(last ? last : next(stream)))(); const paramParse = parser => stream => Y(pp => arr => (newarr => $ifelse(stream.get() === ',')(() => (() => pp(newarr))(stream.next()))($_(newarr)))([...arr, parser(stream)]))([]); const fixSignNumber = parseOne => stream => token => $ifelse(['+', '-'].some($same(token)))(() => ({type: token, left: {type: 'number', value: 0}, right: parseOne(stream)}))($_({type: 'error'})); const parseAST = Y(parser => genParser('+', '-')(genParser('*', '/')(Y(parserOne => stream => $switch( {cond: isNumber, func: token => ({type: 'number', value: parseFloat(token)})}, {cond: isName, func: token => $ifelse(stream.get() === '(')(() => (() => stream.skip(() => ({type: 'func', name: token, params: $log(paramParse(parser)(stream))})))(stream.next()))(() => ({type: 'name', name: token}))}, {cond: $same('('), func: () => stream.skip(() => parser(stream))}, {cond: $_(true), func: fixSignNumber(parserOne)(stream)} )(stream.next()))))); const astEval = Y(astEval => ast => ($switch( {cond: $same('+'), func: () => astEval(ast.left) + astEval(ast.right)}, {cond: $same('-'), func: () => astEval(ast.left) - astEval(ast.right)}, {cond: $same('*'), func: () => astEval(ast.left) * astEval(ast.right)}, {cond: $same('/'), func: () => astEval(ast.left) / astEval(ast.right)}, {cond: $same('number'), func: () => ast.value}, {cond: $same('name'), func: () => Math[ast.name.toUpperCase()]}, {cond: $same('func'), func: () => Math[ast.name.toLowerCase()].apply(null, ast.params.map(astEval))} )(ast.type))); let cacheAST = null; // FOR DEBUG const mathEval = code => astEval(cacheAST = parseAST(makeStream(tokenize(code))));
来自酷炫的 CNodeMD
最近刚好有用到,总感觉哪里不对劲
=> 这东西绑定this主要是用在class里面,不用再写.bind(this),RN里面用这个就很爽,然而现在浏览器和node7.2都没支持class,不过当然还有babel可以用
真服了 讲的那么复杂 一句话就能讲明白 箭头函数没有this, 它里面的this按照变量查找的形式查找
function foo() {
that = this
this.arrow = () => {
console.log(this.name)
}
this.simulationArrow = () => {
console.log(that.name)
}
this.common = function() {
console.log(this.name)
}
}
var f = new foo()
arrow = f.arrow
simulationArrow = f.simulationArrow
common = f.common
f.name = "foo"
name = "global"
arrow() // 输出foo
simulationArrow() // 输出foo
common() // 输出global
上面的结果就很清楚了 new foo() 闭包,封装了一个this对象 所以箭头取到得就是赋值给tha的t这个对象,查找规则不一样而已
@yongningfu 您的理解是正确的,所以这篇文章对您没有价值
半年前我开开心心的用着4.x 6.x,天天折腾ES6,如今跳到新公司苦逼的用着0.12
@SinalVee 为什么不升级?
@jingsam 项目太多了,整套环境搭起来光node的项目就有十几个
@SinalVee 估计只能用docker隔离这些项目,然后再逐步升级
学习
这篇文章写的特别好,想发出这种呼声很久了
@libook 恩,技术都要搞清楚本质,不能不分场景地瞎用。
@jingsam 其实对于箭头函数,我通常会过于保守,因为业务复杂,使用箭头函数通常会考虑很多风险,考虑这些问题又要耗费很多时间,算下来可能要比写function花的时间还要长。
没有这些东西,或许就是未了简单,和性能吧
优秀的文章,理解深入而 说透本质;至于瑕疵和bug,不显眼了
@libook 确实没错,一般不是特别需要,我是不会用箭头函数的,不然以后会碰到一些说不清的麻烦,特别是 this 这块。只有在一些特定需要他的特殊作用域才会考虑用箭头函数,我是非常不喜欢为了简写去用箭头函数的。
讲this
的例子是错的。
@hellozhangran 请指教
cool arrow function 我在node支持的时候就发现不对了,当时也踩了坑,this绑定到调用者的context了。 还查了mozilla的说明。自己明白后就没有像楼主这么详细的介绍给大家。赞
mark一下 ——来自吴小粥的cnode-react手机版
@jingsam
“不要在最外层定义箭头函数,因为在函数内部操作this会很容易污染全局作用域。”
用"use strict";
就不会污染全局作用域了。
@zhanzhenzhen 确实是个办法。我还是建议在箭头函数使用this或者arguments的时候,一定要打起12分小心!