JavaScript函数调用展开语法糖

3
我找到了this一篇文章,说JavaScript函数调用实际上只是func.call(...)的语法糖。
我想问一下这是否真的正确,因为我在规范中没有找到类似的内容(没有仔细查看,只是简单浏览并搜索了call()关键字)。
如果有人能启发我,那就太好了,因为如果这篇文章是真的,我对EcmaScript语言的敬佩可能会降低。

对于任何想要用-1标记此问题的人,我愿意进行讨论,并非常乐意知道,为什么将问题投票为负是如此重要。 - Charlie Scene
我正在撰写答案。我认为这是一个非常合适的问题。 - samanime
在ES5规范中,call方法是用另一个更低级的原语来描述的,但它只是在那个原语之上添加了一个非常薄的包装器,所以我在这里做了一些简化。因此,是的,根据这种简化,它是正确的。提示:作者所说的原语是[[Call]]。 - Bergi
但就我对该语言的理解而言,它并不等同于Function.prototype.call。更重要的是,我测试了TypeScript编译器,并让它生成与ES3兼容的结果(它可以生成的最低版本),但作者在文章中提到的“desugaring”并不存在。 - Charlie Scene
确实。它并不是针对这个进行解糖/转换等操作。它只是一个函数obj.func()的结果是obj成为func的上下文。因此从实际意义上讲,你可以说它与obj.func.call(obj)相同,但它并不会实际进行代码的解糖/转换。 - samanime
2个回答

4

我明白为什么这篇文章会这样说,某种程度上也是这样的。

他们这么说的原因可能是因为当函数直接使用时是没有绑定的,也就是说,如果我创建一个对象:

const a = { say: function() { console.log(this.message) } };
a.message = 'hello';
a.say();

当我运行函数 a.say() 时,say() 函数内的 this 上下文是 a。这相当于 a.say.call(a)
由于我没有显式地绑定该函数,如果我将其传递给其他地方调用它,它不一定会再次具有 a 的上下文:

const a = { say: function() { console.log(this.message) } };
const b = {};
a.message = 'hello';
b.message = 'goodbye';
b.say = a.say;
b.say();

请注意,即使我复制了a的say()方法,并使用b调用该方法,它最终仍然是以b作为其上下文(this)被调用的。因此,这本质上等同于a.say.call(b)。
如果您明确地绑定函数,则始终会使用一个固定的上下文:

const a = { say: function() { console.log(this.message); } };
const b = {};
a.message = 'hello';
b.message = 'goodbye';
b.say = a.say.bind(a);
b.say();

使用bind()方法绑定上下文后,即使我将该函数传递到其他地方,它也始终使用a的上下文。

话虽如此,我不确定为什么你会认为这是有缺陷的。实际上,我认为这是语言的一个非常强大和灵活的特性。它允许您执行像这样的操作:

Array.prototype.slice.call(document.querySelectorAll('div'))
  .forEach(el => console.log(el.innerHTML));
<div>A</div>
<div>B</div>
<div>C</div>

在这个例子中,document.querySelectorAll()返回一个NodeList,通常情况下它没有forEach()方法。然而,由于它具有足够的兼容性,我可以使用call()将其传递给Array.prototype.slice,将其转换为一个包含这些元素的Array。(你也可以直接将它传递给forEach(),但我更喜欢用slice()进行转换,然后再做其他操作。)

感谢您的回答。似乎有些误解。我完全同意您,因为整个“动态 this”机制是一个很好的设计方案。我的问题是JavaScript运行时是否按原样消耗foo(111),还是它们需要做过多的工作并在代码的每个位置将此调用更改为foo.call(global,111)。 - Charlie Scene
有点像,但并不需要太多工作。它只是在运行时确定上下文,这是一件非常容易的事情。就像在global/window级别的所有其他东西一样,像那样调用时隐含了global,因此上下文是全局的,但确定它并不需要太多工作。它不会改变,只是在函数执行时确定。 - samanime
有没有可能影响程序的执行性能(我假设是即时编译和随后立即执行)? - Charlie Scene
不是的。这是JS的基本要素,所以它会非常快速。你运行的所有内容都有一个上下文,并且该上下文始终是已知的。在那篇文章中,当他谈论desugaring时,他只是为了说明目的而说的。这甚至不是JIT编译。这只是一个查找。这就是函数的工作方式。它与OO语言确定类方法上下文的方式并没有什么区别。 - samanime
非常感谢!我接受了你的答案,如果你不介意的话。现在我正在探索语言的深层次部分,但很遗憾并不是每个问题都能得到如此完整的答案。我想知道你是否可以推荐一些值得研究的东西,因为MDN现在已经不够用了,阅读规范还是很困难的。再次感谢。 - Charlie Scene
很遗憾,我不确定这两者之间有什么折中办法。 我通常使用MDN,而当我需要深入了解时,查看规范是唯一可靠的去处。祝好运。=) - samanime

0
每个函数的[[Call]]内部方法在我们调用函数时被激活(通过调用表达式,例如foo() 或使用call和apply方法)。 ~ Dmitry Soshnikov,关于.bind的文章

仅为某些对象定义的内部属性

内部属性 - [[Call]]

描述 - 执行与对象相关联的代码。通过函数调用表达式调用。参数是一个this对象和包含传递给函数调用表达式的arguments的列表。实现此内部方法的对象是可调用的。

~ 请参见Annotated ECMAScript 5.1中的第9表here
还请参见13.2 Creating Function Objects, 13.2.1 [[Call]],
绑定15.3.4.5, 15.3.4.5.1,5 [[Call]]

那里定义的所有步骤当然都是内部的,因此没有实际的“解糖”转换发生,但是这些内部步骤确实类似于它。


简而言之

我想简单来说,你可以把他在文章中所说的理解为:

如何模拟屋顶下正在发生的事情以更好地理解概念。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接