功能程序员对JavaScript的介绍(组成软件)

注意:这是从头开始学习JavaScriptES6 +中的功能编程和组合软件技术的“组合软件”系列(现在是书!)的一部分 。 敬请关注。 还有更多的东西要来!
买书 | 索引 | <上一页 | 下一个>

对于不熟悉JavaScript或ES6 +的用户,这里只是简要介绍。 无论您是初学者还是经验丰富的JavaScript开发人员,您都可以学到新知识。 以下内容只是划伤表面并让您兴奋。 如果您想了解更多,则只需进行更深入的探索。 还有很多。

学习编码的最好方法是编码。 我建议您继续使用交互式JavaScript编程环境,例如CodePen或Babel REPL。

另外,您也可以使用Node或浏览器控制台REPL。

表达式和值

表达式是一段代码,可以求值。

以下是JavaScript中所有有效的表达式:

  7; 
  7 +1;  // 8 
  7 * 2;  // 14 
  '你好';  // 你好 

可以为表达式的值命名。 这样做时,将首先对表达式求值,并将结果值分配给名称。 为此,我们将使用const关键字。 这不是唯一的方法,但它是您最常使用的方法,因此我们现在将继续使用const

  const hello ='Hello'; 
你好; // 你好

var,let和const

JavaScript支持另外两个变量声明关键字: varlet 。 我喜欢按照选择的顺序来考虑它们。 默认情况下,我选择最严格的声明: const 。 用const关键字声明的变量无法重新分配。 最终值必须在声明时分配。 这听起来可能很严格,但是限制是一件好事。 这是一个信号,告诉您“分配给该名称的值不会改变”。 它可以帮助您立即完全理解名称的含义,而无需阅读整个功能或块作用域。

有时重新分配变量很有用。 例如,如果您使用手动命令式迭代而不是功能更强大的方法,则可以迭代分配给let的计数器。

由于var对变量的了解最少,因此它是最弱的信号。 自从我开始使用ES6以来,我从未在真正的软件项目中故意声明过var

请注意,一旦使用letconst声明了变量,再次尝试对其进行声明都会导致错误。 如果您希望在REPL(读取,评估,打印循环)环境中具有更大的实验灵活性,则可以使用var而不是const来声明变量。 允许重新声明var

本文将使用const来使您习惯在实际程序中默认使用const ,但是可以出于交互实验的目的随意替换var

种类

到目前为止,我们已经看到了两种类型:数字和字符串。 JavaScript还具有布尔值( truefalse ),数组,对象等。 稍后我们将介绍其他类型。

数组是值的有序列表。 可以将其视为可以容纳许多物品的盒子。 这是数组文字符号:

  [1、2、3]; 

当然,这是一个可以赋予名称的表达式:

  const arr = [1,2,3]; 

JavaScript中的对象是键:值对的集合。 它还有一个文字符号:

  { 
核心价值'
}

当然,您可以为对象分配名称:

  const foo = { 
bar:“ bar”
}

如果要将现有变量分配给同名的对象属性键,则有一个快捷方式。 您只需键入变量名,而不是同时提供键和值:

  const a ='a'; 
const oldA = {a:a}; //长而冗长的方式
const oA = {a}; //短小甜蜜!

只是为了好玩,让我们再做一次:

  const b ='b'; 
const oB = {b};

对象可以轻松地组合成新的对象:

  const c = {... oA,... oB};  // {a:'a',b:'b'} 

这些点是对象散布算子。 它遍历oA的属性并将它们分配给新对象,然后对oB进行相同操作,从而覆盖新对象上已经存在的所有键。 在撰写本文时,对象传播是一项新的实验性功能,可能无法在所有流行的浏览器中使用,但是如果不适用于您,则可以使用替代方法: Object.assign()

  const d = Object.assign({},oA,oB);  // {a:'a',b:'b'} 

Object.assign()示例中只需要多输入一点,如果您要编写很多对象,它甚至可以节省一些输入。 请注意,在使用Object.assign() ,必须将目标对象作为第一个参数传递。 属性将被复制到的对象。 如果您忘记并忽略了目标对象,则在第一个参数中传递的对象将发生突变。

以我的经验,更改一个现有对象而不是创建一个新对象通常是一个错误。 至少,它容易出错。 注意Object.assign()

解构

对象和数组都支持解构,这意味着您可以从它们中提取值并将它们分配给命名变量:

  const [t,u] = ['a','b']; 
; // '一种'
你 //'b'
  const blep = { 
blob:“ blob”
};

//以下等效于:
// const blop = blep.blop;
const {blop} = blep;
腮红 //'斑点'

与上面的数组示例一样,您可以一次分解为多个分配。 您将在许多Redux项目中看到以下一行:

  const {类型,有效载荷} =操作; 

这是在reducer上下文中使用的方式(有关该主题的更多信息将在以后发布):

  const myReducer =(状态= {},操作= {})=> { 
const {类型,有效载荷} =操作;
开关(类型){
情况“ FOO”:返回Object.assign({},状态,有效载荷);
默认值:返回状态;
}
};

如果您不想为新绑定使用其他名称,则可以分配一个新名称:

 const { blop: bloop } = blep; 
bloop; // 'blop'

阅读:将blep.blop分配为bloop

比较和三元

您可以使用严格相等运算符(有时称为“三等分”)比较值:

  3 +1 === 4;  //正确 

还有一个草率的等于运算符。 它的正式名称为“等于”运算符。 非正式地,“双重等于”。 Double equals具有一个有效的用例或两个用例,但默认情况下,最好使用===运算符代替。

其他比较运算符包括:

  • >大于
  • <小于
  • >=大于或等于
  • <=小于或等于
  • !=不等于
  • !==不严格等于
  • &&逻辑和
  • || 逻辑或

三元表达式是一种表达式,它使您可以使用比较器提出问题,并根据该表达式是否为真来求值以得出不同的答案:

  14-7 === 7吗?  “是的!”  : '不。';  //是的! 

职能

JavaScript具有函数表达式,可以将其分配给名称:

  const double = x => x * 2; 

这意味着与数学函数f(x) = 2x 。 大声说出来,该函数读取x f等于2x 。 仅当将其应用于x的特定值时,此函数才有意义。 要在其他方程式中使用该函数,请编写f(2) ,其含义与4相同。

换句话说, f(2) = 4 。 您可以将数学函数视为从输入到输出的映射。 在这种情况下, f(x)x的输入值到等于输入值与2乘积的对应输出值的映射。

在JavaScript中,函数表达式的值是函数本身:

 双;  // [Function:double] 

您可以使用.toString()方法查看函数定义:

  double.toString();  //'x => x * 2' 

如果要将函数应用于某些参数,则必须通过函数调用来调用它。 函数调用将函数应用于其参数,并计算为返回值。

您可以使用(argument1, argument2, ...rest)调用函数。 例如,要调用我们的double函数,只需添加括号并将一个值传递给double即可:

  double(2);  // 4 

与某些功能语言不同,这些括号是有意义的。 没有它们,该函数将不会被调用:

 双4;  // SyntaxError:意外数字 

签名

函数具有签名,其中包括:

  1. 可选的函数名称。
  2. 括号中的参数类型列表。 可以选择命名参数。
  3. 返回值的类型。

类型签名不需要在JavaScript中指定。 JavaScript引擎将在运行时确定类型。 如果您提供了足够的线索,也可以使用数据流分析通过开发人员工具(例如IDE(集成开发环境)和Tern.js)来推断签名。

JavaScript缺少其自身的功能签名表示法,因此存在一些相互竞争的标准:JSDoc在历史上非常流行,但是冗长而笨拙,没有人愿意将doc注释与代码保持最新,因此许多JS开发人员停止使用它。

TypeScript和Flow目前是最大的竞争者。 我不确定如何用这两种方式表达我所需的一切,因此我仅出于文档目的使用Rtype。 有些人回想起Haskell仅用咖喱粉制的Hindley-Milner类型。 我很乐意看到一个标准化的,适用于JavaScript的好的注释系统,即使仅出于文档目的,但我认为目前没有任何当前解决方案可以胜任。 现在,斜视并尽最大努力跟上奇怪的类型签名,这些签名可能看起来与您所使用的类型略有不同。

  functionName(param1:类型,param2:类型)=>类型 

double的签名是:

  double(x:n)=>数字 

尽管JavaScript不需要对签名进行注释,但有效地交流有关如何使用函数以及如何构成函数的信息,了解什么是签名以及它们的含义仍然很重要。 大多数可重用的函数组合实用程序都要求您传递共享相同类型签名的函数。

默认参数值

JavaScript支持默认参数值。 以下函数的工作方式类似于恒等函数(一个返回与您传入的值相同的函数),除非您使用undefined调用它,或者根本不传递任何参数,然后返回零,而不是:

  const orZero =(n = 0)=> n; 

要设置默认值,只需在函数签名中使用=运算符将其分配给参数即可,如上面的n = 0所示。 当您以这种方式分配默认值时,即使没有显式声明类型注释,诸如Tern.js,Flow或TypeScript之类的类型推断工具也可以自动推断函数的类型签名。

结果是,只要在编辑器或IDE中安装了正确的插件,您就可以在键入函数调用时看到内嵌显示的函数签名。 您还将能够基于调用签名一目了然地了解如何使用该函数。 在合理的地方使用默认分配可以帮助您编写更多的自说明代码。

注意:带有默认值的参数不计入函数的.length属性,这将使依赖于.length值的实用程序(如autocurry)失效。 某些curry实用程序(例如lodash/curry )允许您传递自定义变量,以解决此限制。

命名参数

JavaScript函数可以将对象文字作为参数,并在参数签名中使用解构赋值,以实现与命名参数等效的功能。 注意,您还可以使用默认参数功能为参数分配默认值:

  const createUser =({ 
名称=“匿名”,
avatarThumbnail ='/avatars/anonymous.png'
})=>({
名称,
头像缩略图
});
  const george = createUser({ 
名称:“乔治”
头像:“ avatars / shades-emoji.png”
});
 乔治; 
/ *
{
名称:“乔治”
头像:“ avatars / shades-emoji.png”
}
* /

休息和传播

JavaScript函数的一个共同特征是能够使用rest运算符将函数签名中的其余参数集合在一起: ...

例如,以下函数只是丢弃第一个参数,并将其余参数作为数组返回:

  const aTail =(head,... tail)=>尾巴; 
aTail(1、2、3); // [2,3]

其余部分将单个元素聚集在一起形成数组。 Spread的作用与此相反:它将元素从数组扩展到单个元素。 考虑一下:

  const shiftToLast =(head,... tail)=> [... tail,head]; 
shiftToLast(1、2、3); // [2,3,1]

JavaScript中的数组具有一个迭代器,该迭代器在使用spread运算符时被调用。 对于数组中的每个项目,迭代器都会提供一个值。 在表达式[...tail, head] ,迭代器将每个元素从tail数组按顺序复制到由周围文字表示法创建的新数组中。 由于head已经是一个单独的元素,因此我们只需将其放到数组的末尾即可。

咖喱

curried函数是一次同时使用多个参数的函数:它采用一个参数,然后返回采用下一个参数的函数,依此类推,直到提供了所有参数为止,此时,应用程序已完成,并且返回最终值。

可以通过返回另一个函数来启用Curry和部分应用程序:

  const highpass =截止=> n => n> =截止; 
const gt4 = highpass(4); // highpass()返回一个新函数

您不必使用箭头功能。 JavaScript也有一个function关键字。 我们使用箭头功能,是因为关键字function输入要多得多。 这等效于上面的highPass()定义:

  const highpass =函数highpass(cutoff){ 
返回函数(n){
返回n> =截止;
};
};

JavaScript中的箭头大致表示“功能”。 函数行为上有一些重要的区别,具体取决于您使用的是哪种函数( =>缺少它自己的this ,并且不能用作构造函数),但是到那里时,我们将了解这些区别。 现在,当您看到x => x ,请考虑“一个带x并返回x ”。 因此,您可以读取const highpass = cutoff => n => n >= cutoff; 如:

“highpass是接受cutoff并返回接受n并返回n >= cutoff结果的n >= cutoff ”。

由于highpass()返回一个函数,因此您可以使用它来创建一个更专门的函数:

  const gt4 = highpass(4); 
  gt4(6);  //正确 
gt4(3); //错误

Autocurry使您可以自动咖喱函数,以实现最大的灵活性。 假设您有一个函数add3()

 const add3 = curry((a, b, c) => a + b + c); 

借助autocurry,您可以通过几种不同的方式使用它,并且它将根据传入的参数多少返回正确的内容:

 add3(1, 2, 3); // 6 
add3(1, 2)(3); // 6
add3(1)(2, 3); // 6
add3(1)(2)(3); // 6

抱歉,Haskell爱好者,JavaScript缺少内置的自动运行机制,但是您可以从Lodash导入一个:

 $ npm install --save lodash 

然后,在您的模块中:

 从“ lodash / curry”进口咖喱; 

或者,您可以使用以下魔术:

  //微小的递归autocurry 
const curry =(
f,arr = []
)=>(... args)=>(
a => a.length === f.length?
F A) :
咖喱(f,a)
)([... arr,... args]);

功能组成

当然,您可以编写函数。 函数组合是将一个函数的返回值作为参数传递给另一个函数的过程。 用数学符号表示:

 f . g 

在JavaScript中将其翻译为:

  f(g(x)) 

从内到外进行评估:

  1. x被评估
  2. g()应用于x
  3. f()应用于g(x)的返回值

例如:

  const inc = n => n + 1; 
inc(double(2)); // 5

将值2传递到double() ,它产生44被传入inc() ,其结果为5

您可以将任何表达式作为参数传递给函数。 在应用函数之前,将对表达式求值:

  inc(double(2)* double(2));  // 17 

由于double(2)计算结果为4 ,因此您可以将其读取为inc(4 * 4) ,其计算结果为inc(16) ,然后计算结果为17

函数组合对于函数式编程至关重要。 稍后我们将有更多内容。

数组

数组具有一些内置方法。 方法是与对象关联的函数:通常是关联对象的属性:

  const arr = [1,2,3]; 
arr.map(double); // [2,4,6]

在这种情况下, arr是对象, .map()是对象的属性,具有值的功能。 调用它时,该函数将应用于参数以及称为this的特殊参数,该特殊参数在调用方法时会自动设置。 this值是.map()如何访问数组内容的方式。

请注意,我们将double函数作为值传递给map而不是调用它。 这是因为map接受一个函数作为参数,并将其应用于数组中的每个项目。 它返回一个包含double()返回值的新数组。

请注意,原始的arr值保持不变:

  arr;  // [1、2、3] 

方法链接

您还可以链接方法调用。 方法链接是直接在函数的返回值上调用方法的过程,而无需按名称引用返回值:

  const arr = [1,2,3]; 
arr.map(double).map(double); // [4,8,12]

谓词是一个返回布尔值( truefalse )的函数。 .filter()方法采用一个谓词并返回一个新列表,仅选择通过该谓词(返回true )的项包括在新列表中:

  [2,4,6] .filter(gt4);  // [4,6] 

通常,您需要从列表中选择项目,然后将这些项目映射到新列表:

  [2,4,6] .filter(gt4).map(double);  [8,12] 

注意:在本文的后面,您将看到一种更有效的方法,即使用称为转换器的东西同时进行选择和映射,但首先需要探索其他事情。

结论

如果您的头现在正在旋转,请不要担心。 我们勉强摸索了许多值得更多探索和考虑的事物。 我们很快会回头并更深入地探讨其中一些主题。

买书 | 索引 | <上一页 | 下一个>

在EricElliottJS.com上了解更多信息

EricElliottJS.com的成员可以观看带有交互式代码挑战的视频课程。 如果您还不是会员,请立即注册。