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

时间: 2018-10-26阅读: 1053标签: 语言
作者|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"); //false

obj.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"); //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

结论

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

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


英文原文:https://www.heartinternet.uk/blog/3-features-that-could-change-the-future-of-javascript/


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

链接: http://www.fly63.com/article/detial/1194

未来有望干掉 Python 和 JavaScript 的编程语言

Python 和 JavaScript 是两门非常有影响力的编程语言,二者都是我们在打造跨平台应用时会用到的主流语言。由于 Python 和 JavaScript 都是脚本语言,因此它们有很多共同的特性,都需要解释器来运行,都是动态类

javascript是动态语言吗?

JavaScript一种动态类型、弱类型、基于原型的语言。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML网页上使用,用来给HTML网页增加动态功能。

JavaScript 的现状_以及对 JavaScript 的批评有哪些呢?

JavaScript 已经成为了一门一流的编程语言,使用 JavaScript 的程序员们正变得无所不能。质疑 JavaScript 是否是一种「真正的」编程语言的时代已经过去,现在的问题是,你有没有准备好投入到这门语言中,进行真正的学习。

别了,JavaScript;你好WebAssembly

作为JavaScript替代,一种Web开发的新形式已经浮出水面:WebAssembly.Web开发与JavaScript开发向来是同义词。就是说,直到现在。但一种新的Web开发形式已然出现,声言会取代JavaScript

javascript为什么会火?javascript的未来怎么样?

javascript和其它面向对象语言越来越像,自从javascript引入了class以后,语法和格式一些和其它面向对象的语言越来越像,比如java,C#等,使学习javascript的学习成本很低,用的人自然越来越多。其它的特性也会慢慢的引入javascritpt

angular多语言配置

angular的国际化方案,采用ngx-translate来实现。我们希望可以在一个固定的文件里面配置对应的翻译文件,然后在每个用到的组件里面使用它,随意我们需要借助TranslateHttpLoader来加载翻译文件。

Js的三大组成部分

JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。通常JavaScript脚本是通过嵌入在HTML中来实现自身的功能的。

WebAssembly的前世今身

接触WebAssembly之后,在google上看了很多资料。感觉对WebAssembly的使用、介绍、意义都说的比较模糊和笼统。感觉看了之后收获没有达到预期,要么是文章中的例子自己去实操不能成功,要么就是不知所云

javascript为什么是弱类型?

弱类型语言也称为弱类型定义语言。与强类型定义相反。弱类型语言允许变量类型的隐式转换,允许强制类型转换等,如字符串和数值可以自动转化;而强类型语言一般不允许这么做。

不同语言在函数内部定义函数

在 LeetCode 刷题的时候,题解有的大佬给出的答案很优秀,是 python 的,想抄作业发现有的功能函数都定义在答案函数的内部,主要是闭包操作外部变量方便。不同语言在函数内部定义函数稍有不同

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!