作者|Willian Martins
译者|无明
出处丨前端之巅
你想不想知道下一波令人愉快无比的 JavaScript 特性?你乃至都不知道自己须要这些特性。现在,我要向你展示三个可能会改变你编写 JavaScript 代码办法的提案。
在开始之前,我要先做一个小小的免责声明:
所有这些特性都在开拓和谈论当中。我的目的是为这些特性做一些宣扬,并让人们知道 TC39 正在努力探求共识、修复所有语法和语义,并让它们能够不才一版 ECMAScript 中发布。如果你有任何疑问、见地或想要供应支持,请访问 TC39 提案存储库(https://github.com/tc39/proposals ) ,为你支持的特性添加星标,提出你的疑问,并参与个中。
在开始先容第一个提案之前,我想问一个大略的问题:
“this”是什么?ECMAScript 中的 this 与很多其他编程措辞中的 this 具有不同的语义,在其他编程措辞中,this 常日指的是词法浸染域。让我们通过一些小例子来解释这个问题:
全局浸染域中的“this”
在这个例子中,this 的值是什么?
console.info(this);
在全局浸染域内,this 指的是全局工具,如浏览器中的 window、Web Worker 的 self 和 NodeJS 中的 module.exports 工具。
函数浸染域中的“this”
在函数浸染域中,this 的行为取决于函数的调用办法,因此预测它的值会很蹊跷。通过以下示例,我们可以更好地理解它:
<button id=\"大众button\"大众> </button><script> class MeowctComponent { constructor() { this.paw = document.getElementById('button'); } meow() { console.info(' on this: ', this.paw); } } const cat = new MeowctComponent(); cat.paw.addEventListener('click', cat.meow);</script>
在上面的例子中,我创建了一个 MeowctComponent,它只有一个指向 button 元素的 paw 属性和一个名为 meow 的方法,它将在掌握台打印 paw 实例的属性。
只有在单击按钮时才会实行 meow 方法,因此,this 将按钮作为高下文,并且由于按钮没有 paw 属性,它将在掌握台中打印 undefined。
要修复这个行为,我们可以利用 Function.prototype.bind() 方法将 this 显式绑定到 cat 实例,如下所示:
<button id=\"大众button\"大众>Meow</button><script>class MeowctComponent {constructor() {this.paw = document.getElementById('button');}meow() {console.info(' on this: ', this.paw);}}const cat = new MeowctComponent();cat.paw.addEventListener('click', cat.meow.bind(cat));</script>
.bind() 方法将绑定函数返回给第一个参数,也便是高下文。现在,由于我们将 cat.meow 方法绑定到 cat 实例,以是 meow 方法中的 this.paw 精确指向了 button 元素。
除了 Function.prototype.bind() 方法,我们还可以利用箭头函数来得到相同的效果。它保留了高下文中 this 的值,而无需显式绑定高下文,如下所示:
<button id=\"大众button\"大众> Meow</button><script>class MeowctComponent {constructor() {this.paw = document.getElementById('button');}meow() {console.info(' on this: ', this.paw);}}const cat = new MeowctComponent();cat.paw.addEventListener('click', () => cat.meow());</script>
对付大多数情形,箭头函数办理了须要显式绑定 this 的问题,但仍旧有两种情形须要利用显式绑定。
利用 this 调用已知函数以供应高下文
let hasOwnProp = Object.prototype.hasOwnProperty;let obj = Object.create(null);obj.hasOwnProperty('x') // Type Error...hasOwnProp.call(obj, \公众x\公众); //falseobj.x = 100;hasOwnProp.call(obj, \"大众x\"大众); // true
obj 工具没有扩展 Object.prototype,但我们须要利用 Object.prototype 的 hasOwnProperty 方法检讨 obj 是否含有 x 属性。为了实现这一点,我们必须调用 call 方法,并将 obj 作为第一个参数显式地传给它,但常日我们彷佛不会这么做。
提取方法
当我们须要从工具(如上面的 MeowctComponent)中提取方法时,就会碰着第二种情形:
<button id=\"大众button\"大众> </button><script>class MeowctComponent {constructor() {this.paw = document.getElementById('button');}meow() {console.info(' on this: ', this.paw);}}const cat = new MeowctComponent();cat.paw.addEventListener('click', cat.meow.bind(cat));</script>
这些场景都是展示第一个 TC39 提案的完美示例。
绑定操作符::Bind 操作符包含了一个新的操作符::(双冒号),为前面两种场景供应了语法糖。它有两种格式:二元和一元。
在利用二元形式时,绑定操作符将创建一个函数,将左侧部分与右侧的 this 绑定在一起,如下所示:
let hasOwnProp = Object.prototype.hasOwnProperty;let obj = Object.create(null);obj.hasOwnProperty('x') // Type Error...obj::hasOwnProp(\"大众x\公众); //falseobj.x = 100;obj::hasOwnProp(\公众x\"大众); // true
在利用一元形式时,操作符将创建一个绑定到给定引用根本的函数作为 this 变量的值,如下所示:
...cat.paw.addEventListener('click', ::cat.meow);// which desugars tocat.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 tosanitize(document.getElementById('user-input').value) which will be thesubject of the next step .../|> textualizeNumbers/ ^ ... that will be added as a parameter of this step which desugars totextualizeNumbers(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 whichfinally desugars toconsole.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 Errorx |> 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); // 7maxGreaterThanZero(-1, -3); // 0
上面的代码是之前问题的另一种可能的办理方案。我们嵌套了两个 Math.max 方法,里面那个返回 numbers 参数中最大的数字。如果里面的那个方法返回小于零的数字,那么表面的那个方法将确保返回零。我们可以利用省略号标记得到相同的效果。
const maxGreaterThanZero = Math.max(0, ...);maxGreaterThanZero(1, 3, 5, 7); // 7maxGreaterThanZero(-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结论
如你所见,这些提案中有很多方面仍未终极确定。采取个中一个提案可能会导致重塑另一个提案的语法或语义,乃至被移除。
那么,我们该当在生产环境中利用它们吗?还不是时候。不过,我们可以考试测验它们,看看它们是否能够有效地办理问题。
英文原文
https://www.heartinternet.uk/blog/3-features-that-could-change-the-future-of-javascript/