这三个新特性可能改变JavaScript未来

2022/1/31 1:04:34

本文主要是介绍这三个新特性可能改变JavaScript未来,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

  你想不想知道下一波令人兴奋无比的 JavaScript 特性?你甚至都不知道自己需要这些特性。现在,我要向你展示三个可能会改变你编写 JavaScript 代码方式的提案。

  在开始之前,我要先做一个小小的免责声明:

  所有这些特性都在开发和讨论当中。我的目的是为这些特性做一些宣传,并让人们知道 TC39 正在努力寻找共识、修复所有语法和语义,并让它们能够在下一版 ECMAScript 中发布。如果你有任何疑问、意见或想要提供支持,请访问 TC39 提案存储库 ,为你支持的特性添加星标,提出你的疑问,并参与其中。

  在开始介绍第一个提案之前,我想问一个简单的问题:

  ECMAScript 中的 this 与很多其他编程语言中的 this 具有不同的语义,在其他编程语言中,this 通常指的是词法作用域。让我们通过一些小例子来说明这个问题:

  全局作用域中的“this”

  在这个例子中,this 的值是什么?

  console.info(this);

  在全局作用域内,this 指的是全局对象,如浏览器中的 window、Web Worker 的 self 和 NodeJS 中的 module.exports 对象。

  函数作用域中的“this”

  在函数作用域中,this 的行为取决于函数的调用方式,因此预测它的值会很蹊跷。通过以下示例,我们可以更好地理解它:

  我创建了一个 MeowctComponent,它只有一个指向 button 元素的 paw 属性和一个名为 meow 的方法,它将在控制台打印 paw 实例的属性。

  只有在单击按钮时才会执行 meow 方法,因此,this 将按钮作为上下文,并且由于按钮没有 paw 属性,它将在控制台中打印 undefined。

  要修复这个行为,我们可以利用 Function.prototype.bind() 方法将 this 显式绑定到 cat 实例,方法将绑定函数返回给第一个参数,也就是上下文。现在,因为我们将 cat.meow 方法绑定到 cat 实例,所以 meow 方法中的 this.paw 正确指向了 button 元素。

  除了 Function.prototype.bind() 方法,我们还可以使用箭头函数来获得相同的效果。它保留了上下文中 this 的值,而无需显式绑定上下文,对于大多数情况,箭头函数解决了需要显式绑定 this 的问题,但仍然有两种情况需要使用显式绑定。

  使用 this 调用已知函数以提供上下文

  let hasOwnProp=Object.prototype.hasOwnProperty;

  let obj=Object.create(null);

  obj.hasOwnProperty('x') // Type Error...

  hasOwnProp.call(obj, "x"); //false

  obj.x=100;

  hasOwnProp.call(obj, "x"); // true

  obj 对象没有扩展 Object.prototype,但我们需要使用 Object.prototype 的 hasOwnProperty 方法检查 obj 是否含有 x 属性。为了实现这一点,我们必须调用 call 方法,并将 obj 作为第一个参数显式地传给它,但通常我们似乎不会这么做。

  提取方法

  当我们需要从对象(如上面的 MeowctComponent)中提取方法时,就会遇到第二种情况。

  这些场景都是展示第一个 TC39 提案的完美示例。

  Bind 操作符包含了一个新的操作符::(双冒号),为前面两种场景提供了语法糖。它有两种格式:二元和一元。

  在使用二元形式时,绑定操作符将创建一个函数,将左侧部分与右侧的 this 绑定在一起,如下所示:

  let hasOwnProp=Object.prototype.hasOwnProperty;

  let obj=Object.create(null);

  obj.hasOwnProperty('x') // Type Error...

  obj::hasOwnProp("x"); //false

  obj.x=100;

  obj::hasOwnProp("x"); // true

  在使用一元形式时,操作符将创建一个绑定到给定引用基础的函数作为 this 变量的值,如下所示:

  ...

  cat.paw.addEventListener('click', ::cat.meow);

  // which desugars to

  cat.paw.addEventListener('click', cat.meow.bind(cat));

  ...

  绑定操作符为创建虚拟方法带来了新的机会,就像这个示例一样:

  import { map, takeWhile, forEach } from "iterlib";

  getPlayers()

  ::map(x=> x.character())

  ::takeWhile(x=> x.strength > 100)

  ::forEach(x=> console.log(x));

  它非常有用,因为开发人员不需要为了解决一个小问题去下载整个库,从而降低了对生产应用程序包大小的影响。此外,它让库变得更容易扩展。

  我们刚刚探讨了使用虚拟方法的好处。但是,在使用绑定操作符时,我们必须依赖将 this 变量绑定到特定函数。在某些情况下,我们不能这样做。在介绍下一个提案之前,有必要再深入研究一下这个用例。

  我们假设有一个请求,要求创建一个用于清理用户文本的函数,然后将所有数字符号转换为文本表示。通常我们是怎么做的?

  const userInput=document.getElementById('user-input').value;

  const sanitizedInput=sanitize(userInput);

  const textualizedNumberInput=textualizeNumbers(sanitizedInput);

  console.log(textualizedNumberInput);

  上面的代码是一种可能的解决方案。它非常标准,但为了可读性,我们创建了很多中间变量。你可能希望移除这些中间变量,让我们看看这会是什么样子:

  console.log(

  textualizeNumbers(

  sanitize(

  document.getElementById('user-input').value

  )

  )

  );

  上面的代码看起来有点混乱。为了理解数据流,开发人员需要从中间向上看,这样感觉不太自然。是否有更好的方法来组合这些函数,同时不需要使用深层嵌套或很多中间变量?

  管道操作符最小提案

  管道操作符为上面介绍的用例提供了语法糖,它通过更加可读和函数式的方式简化了一系列函数的调用。它向后兼容,并为扩展内置原型提供了另一种选择。为了说明它将如何简化代码,让我们看看前面的例子如果使用管道操作符会怎样。

  document.getElementById('user-input').value

  |> sanitize

  |> textualizeNumbers

  |> console.log

  让我们来分解这些代码,看看它有多直观。

  第一个管道步骤可以被称为 subject。管道操作符接受这个 subject,并将它作为参数插入到下一步,它的结果将成为下一个管道步骤的 subject,如下所示:

  document.getElementById('user-input').value

  // ^ this is the subject of the next step...

  |> sanitize

  /* ^ ... that will be added as a parameter of this step which desugars to

  sanitize(document.getElementById('user-input').value) which will be the

  subject of the next step ...

  */

  |> textualizeNumbers

  /* ^ ... that will be added as a parameter of this step which desugars to

  textualizeNumbers(sanitize(document.getElementById('user-input').value))

  which will be the subject of the next step ...

  */

  |> console.log

  /* ^ ... that will be added as a parameter of this step which

  finally desugars to

  console.log(

  textualizeNumbers(

  sanitize(

  document.getElementById('user-input').value

  )

  )

  );

  */

  管道化多个参数

  我们假设 textualizeNumbers 函数有第二个参数,它是一个不应该被文本化的数字白名单,我们只需要将 0 和 1 进行文本化。那么应该如何将其添加到管道中?

  document.getElementById('user-input').value

  |> sanitize

  |> (text=> textualizeNumbers(text, ['0', '1']))

  |> console.log

  你可以使用箭头函数来接收多个参数。请注意,在最小提议中,箭头函数必须用括号括起来,否则会发生语法错误。这个括号似乎是一个语法开销,我们将在稍后讨论如何解决这个问题。

  管道作为异步函数

  假设清理函数异步处理服务器上的输入,并返回一个 promise。

  document.getElementById('user-input').value

  |> await sanitize

  // ^ this is ambiguous

  // await sanitize(x) or (await sanitize())(x)?

  |> (text=> textualizeNumbers(text, ['0', '1']))

  |> console.log

  我们可以使用 await 关键字来等待结果,但这段代码是有问题的。它有点含糊不清,不知道是该使用 await sanitize(x) 还是 (await sanitize())(x)。

  最小提案中的语法和语义仍然存在一些问题。目前还有其他两个竞争提案试图解决这个问题:智能管道提案和 F# 管道提案。

  智能管道提案

  在智能管道提案中,之前的示例可以这样写:

  document.getElementById('user-input').value

  |> await sanitize(#)

  // ^ whenever () or [] is needed we use Topic style

  |> textualizeNumbers(#, ['0', '1']))

  /* ^ This is the placeholder for the

  * subject of the previous pipeline step

  */

  |> console.log

  // ^ Bare style

  智能管道提案定义了两种样式和占位符标记:裸样式(Bare Style)和主题样式(Topic Style),以及 # 占位符。每当需要使用括号或方括号时,必须使用主题样式。其他情况可以使用裸样式。# 占位符表示需要将上一步的 subject 放在这个位置。# 占位符在提案中还未最终确定,标记仍然可以更改。

  如果你有一个柯里化的函数,必须使用主题风格,否则会发生语法错误,如下所示:

  x |> myCurried(10) // Syntax Error

  x |> myCurried(10)(#) // Fine

  F# 语法提案

  F# 管道提案尝试以较少的语法开销解决相同的问题。它创建了一个 await 步骤,这有助于解决我们在最小提案中发现的歧义。

  document.getElementById('user-input').value

  |> sanitize

  |> await

  // ^ await step

  |> (text=> textualizeNumbers(text, ['0', '1']))

  |> console.log

  await 的意思是上一步骤需要等待将 subject 传递给下一个步骤。

  console.log(

  textualizeNumbers(

  await (

  sanitize(document.getElementById('user-input').value

  )

  )

  );

  请注意,它并没有解决需要使用括号将箭头函数括起来的“语法开销”,我们稍后将讨论一种临时解决方法。

  部分应用(Partial Application)

  在讨论管道操作符时,我们看到了一个管道步骤示例,其中有一些绑定值和一个通配符。

  ...

  textualizeNumbers(#, ['0', '1']))

  // ^ wildcard

  ...

  这段代码可以被视为部分应用,但它究竟是什么意思呢?

  部分应用是指给定具有 N 个参数的函数,将其中一些参数绑定到固定值,然后返回具有较少参数的另一函数。

  为了说明部分应用的概念,我们可以使用以下代码片段:

  const sayHi=(greetings, name, location)=>

  console.log(`${greetings}, ${name}, from ${location}!`);

  sayHi('Yo', 'James', 'Colombia');

  // 'Yo, James from Colombia!onst sayHi=(greetings, name, location)=> console.log(`${greetings}, ${name}, from ${loc

  这段代码提供了一个简单的函数,它接收三个参数并将它们作为问候消息打印到控制台。我们假设只有前两个参数有值,第三个值需要从一个 promise 中获得。我们应该怎么做?

  const greetings='Hello';

  const name='Maria';

  // fetches the location from the server ˉ\_(ツ)_/ˉ

  getLocation()

  .then(

  location=> sayHi(greetings, name, location)

  );

  这种方法可能被认为是次优的,因为我们为了保持前两个参数的值创建两个额外的变量。或许还有更好的办法。

  在 ECMAScript 中,可以使用 Function.prototype.bind 实现部分应用。通常,我们忘记了.bind() 方法不仅可以绑定上下文,还可以绑定参数。现在让我们来改进之前的例子。

  const sayHiByLocation=sayHi.bind(null, 'Hello', 'Maria');

  getLocation()

  .then(

  location=> sayHiByLocation(location)

  );

  由于我们不需要绑定上下文,因此我们将 null 作为第一个参数传递进去,.bind() 方法将返回一个新函数,其中“Hello”和“Maria”分别绑定到第一个和第二个参数。

  不过,如果我们只有 greetings 和 location 参数,那该怎么办?我们该如何绑定第一个和最后一个参数?这个时候,使用 Function.prototype.bind() 是做不到的,因为它只能按顺序(如 a、b 和 c)绑定参数。它不能跳过 b 直接绑定 a 和 c。

  我们可以尝试使用柯里化。虽然我们可以使用柯里化获得与部分应用相同的效果,但柯里化不等同于部分应用。

  柯里化是指给定具有 N 个参宿的函数,然后返回一个具有 N-1 个参数的函数。有了这个定义,我们就可以针对之前的问题给出一个解决方案:

  const curriedSayHi=greetings=>

  location=>

  name=> sayHi(greetings, name, location);

  const sayHiTo=curriedSayHi('Hello')( 'Portugal');

  getName()

  .then(name=> sayHiTo(name));

  但问题是,如果我们需要另一个参数顺序,就需要编写额外的柯里化函数。而且我们很难预测下一个调用是绑定下一个参数还是调用目标函数。有没有其他方法可以在不使用这些模板代码的情况下解决这个问题?

  这个时候,我们可以使用箭头函数:

  const sayHiTo=name=> sayHi('Hello', name, 'Portugal');

  getName()

  .then(name=> sayHiTo(name));

  正如你所看到的,在 ECMAScript 中有很多方法可以实现部分应用,但它们都没有被标准化。这些用例正是部分应用提案试图解决的问题。

  部分应用提案

  部分应用提案创建了两个新的标记放在函数调用中,我们可以将参数部分应用到特定函数上。?(问号)用于绑定单个参数,…(省略号)用于绑定多个参数。有了这些定义,我们可以重写之前的示例:

  const sayHiTo=curriedSayHi('Hello', ?, 'Portugal');

  getName()

  .then(name=> sayHiTo(name));

  正如你所看到的,现在可以很清楚地绑定任意参数,而无需添加额外的代码。另外,只需要看一下代码,就可以知道绑定了多少参数,以及需要提供哪些参数。

  省略号标记

  这个提案还提到了另一个标记,即省略号标记(…),它可以将参数分散到特定位置。听起来很复杂是吗?让我们来看看实际的代码。

  假设我们需要取一个系列中最大的数字,但如果所有数字都小于零,则返回零。

  const maxGreaterThanZero=(...numbers)=>

  Math.max(0, Math.max(...numbers));

  maxGreaterThanZero(1, 3, 5, 7); // 7

  maxGreaterThanZero(-1, -3); // 0

  上面的代码是之前问题的另一种可能的解决方案。我们嵌套了两个 Math.max 方法,里面那个返回 numbers 参数中最大的数字。如果里面的那个方法返回小于零的数字,那么外面的那个方法将确保返回零。我们可以使用省略号标记获得相同的效果。

  const maxGreaterThanZero=Math.max(0, ...);

  maxGreaterThanZero(1, 3, 5, 7); // 7

  maxGreaterThanZero(-1, -3); // 0

  超级简单,不是吗?现在让我们来看看如何将这个特性与管道操作符用在一起。

  还记得之前谈到带有 await 的 F# 语法吗?我们指出箭头函数的语法开销并没有得到解决。现在,通过部分应用,我们可以像下面这样重写代码:

  document.getElementById('user-input')

  |> sanitize

  |> await

  |> textualizeNumbers(?, ['0', '1'])

  /* ^ this will return a function which expects

  * ? token to be the subject from the previous step

  */

  |> console.log

  如你所见,这些提案中有很多方面仍未最终确定。采用其中一个提案可能会导致重塑另一个提案的语法或语义,甚至被移除。

  那么,我们应该在生产环境中使用它们吗?还不是时候。不过,我们可以尝试它们,看看它们是否能够有效地解决问题。



这篇关于这三个新特性可能改变JavaScript未来的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程