今天给大家介绍一个JavaScript代码中的新运算符:管道运算符|>。对一个值进行连续操作当我们在JavaScript中对一个值进行连续操作(比如函数调用)时,目前有两种基本方式:将值作为参数传递给特定的操作(或者如果有多个操作则嵌套操作)),例如:三(二(一(值)));将函数作为值的方法调用(如果有多个方法,则链接),例如:value.one().two().three()。在2020年JS现状调查中,“您认为JavaScript目前缺少什么?”问题,希望管道操作员的答案排在第4位。看来大家对JS这种连续操作的写法还是不太满意。首先,如果是嵌套的写法,简单的嵌套还好,但是当嵌套变深的时候,读起来就有点吃力了。嵌套的执行流程是从右向左移动的,而不是我们通常阅读代码的从左到右的方向。另外,我们也很难在很多括号之间找到一个地方来添加一些参数。例如,以下代码:console.log(chalk.dim(`$${Object.keys(envars).map(envar=>`${envar}=${envars[envar]}`).join('')}`,'节点',args.join('')));对于链式调用,它只能在我们将方法指定为值的实例方法时使用,这使得它的局限性很大。当然,如果你的库设计得很好(比如jQuery),它还是很容易使用的。流水线编程Unix操作系统有一种流水线机制,可以将上一个操作的值传递给下一个操作。这种机制非常有用,可以将简单的操作组合成复杂的操作。很多语言都有流水线的实现。这是一个简单的例子:functioncapitalize(str){returnstr[0].toUpperCase()+str.substring(1);}functionhello(str){returnstr+'你好!';}以上是两个简单的函数。如果要嵌套执行,传统写法和流水线写法如下:>大写|>你好//“Conardli你好!”两个相互竞争的提案关于pipelineoperator,目前ES中有两个相互竞争的提案:微软提出的F#:一种函数式编程语言,其核心是基于OCaml,这个operator可以很容易的写出curry风格的代码。HackbyMeta:大致是PHP的静态类型版本。此管道运算符专注于柯里化函数以外的语言特性。目前Meta提出的Hack应该会受到社区的欢迎,而微软提出的F#已经多次被TC39否决。不过不用担心,F#的优点以后说不定也会被引入到Hack中。下面分别看看这两个proposal的用法。Hackpipelineoperator下面是一个Hackpipelineoperator|>的简单例子:'ConardLi'|>console.log(%)//ConardLipipelineoperator|>的左边是一个表达式,它被求值,变成特殊的值变量%。我们可以在右侧使用该变量。返回右边的执行结果。前面的例子等同于:console.log('ConardLi')//ConardLi下面还有一些其他例子:value|>someFunction(1,%,3)//函数调用值|>%.someMethod()//方法调用值|>%+1//operatorvalue|>[%,'b','c']//数组字面值|>{someProp:%}//对象字面值|>await%//等待Promisevalue|>(yield%)//产生一个生成器值让我们看一个更复杂的例子,一个嵌套的函数调用:consty=h(g(f(x)));Hack管道运算符可以让我们更好的表达这段代码的意思:consty=x|>f(%)|>g(%)|>h(%);这段代码比较符合我们常规的编码思路,代码从左到右依次执行:f,g,h。F#管道运算符F#管道运算符与Hack管道运算符大致相似。但是,它没有特殊变量%。相反,运算符右侧的功能不会直接应用于其左侧。因此,下面两个表达式是等价的:'ConardLi'|>console.logconsole.log('ConardLi')因此,F#管道运算符更适合单参数函数,下面三个函数是等价的:consty=h(g(f(x)));//没有pipeconsty=x|>f(%)|>g(%)|>h(%);//Hackpipeconsty=x|>f|>g|>h;//F#pipe在这种情况下,Hackpipe比F#pipe更冗长。不过在多参数的情况下,F#pipe的写法稍微复杂一点:5|>add2(1,%)//Hackpipe5|>$=>add2(1,$)//F#pipe可以被查看发现F#管道需要额外写一个匿名函数,相比Hack管道显然缺乏一些灵活性。这可能也是大家更倾向于Hack管道的原因。管道运算符的一些实际用例(1)嵌套函数调用的扁平化JavaScript标准库创建的所有迭代器都有一个共同的原型。这个原型不能直接访问,但我们可以像这样检索它:constIteratorPrototype=Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));使用管道运算符,代码会更容易理解一些:constIteratorPrototype=[][Symbol.iterator]()|>Object.getPrototypeOf(%)|>Object.getPrototypeOf(%);(2)后处理看下面代码:functionmyFunc(){//···returnconardLi.someMethod();}如果现在我们想在函数返回前对返回值做一些其他的操作,应该怎么做我们的确是?以前,我们必须定义一个临时变量或者在函数外包装一个函数。使用管道运算符,我们可以这样做:functionmyFunc(){//···returntheResult|>(console.log(%),%);//(A)}在下面的代码中,我们的后处理值是一个函数——我们可以向它添加一个属性:consttestPlus=()=>{assert.equal(3+4,7);}|>Object.assign(%,{name:'测试加号运算符',});前面的代码等同于:consttestPlus=()=>{assert.equal(3+4,7);}Object.assign(testPlus,{name:'Testing+',});我们也可以像这样使用管道运算符:consttestPlus=()=>{assert.equal(3+4,7);}|>(%.name='Testtheplusoperator',%);链式函数调用我们可以使用Array的一些方法如.filter()和.map()来实现链式调用,但这只是一些内置的数组方法,我们不能通过库引入更多的Arrays该方法使用管道运算符,我们可以实现一些其他方法的链式调用,例如数组本身的方法:import{Iterable}from'@rauschma/iterable/sync';const{filter,map}=Iterable;constresultSet=inputSet|>filter(%,x=>x>=0)|>map(%,x=>x*2)|>newSet(%);最后回过头来看看题目的代码:constregexOperators=['*','+','[',']'].map(ch=>escapeForRegExp(ch)).join('')|>'['+%+']'|>newRegExp(%);其实相当于:let_ref;constregexOperators=((_ref=['*','+','[',']'].map(ch=>escapeForRegExp(ch)).join('')),newRegExp(`[${_ref}]`));与引入中间变量相比,流水线算子更易于阅读和简洁。
