资讯专栏INFORMATION COLUMN

深入理解JavaScirpt的函数调用和"this"

bladefury / 1904人阅读

摘要:简单的函数调用显而易见,一直用调用函数将会非常烦人。规范说几乎总是被传递,但不在严格模式下时被调用函数应该将其更改为全局对象。实际上,规范有一个和都使用的原语内部称为。

过去很多年里,我看到过太多关于JavaScript函数调用的混淆。尤其是,很多人抱怨函数调用中this的语义令人困惑。
在我看来,通过理解核心函数调用原语,然后将其他所有调用函数的方法视为在原语之上的语法糖,如此便可澄清很多这类疑惑。事实上,这正是ECMAScript规范对此的看法。在某些方面,这篇文章是规范的简化,但基本思路是一样的。

核心原语

首先,我们先看一下函数调用的核心原语,Function对象的call方法[1]。调用方法方法相对简单。

从参数1到末尾创建一个参数列表(argList)

第一个参数(参数0)是thisValue

通过将this的值设为thisValueargList作为其参数列表调用函数

举例:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

hello.call("Yehuda", "world") //=> Yehuda says hello world

如你所见,我们通过将this设置为“Yehuda”和单个参数“world”来调用hello方法。这正是JavaScript中函数调用的核心原语。你可以认为所有其他方式的函数调用都可”去糖“得到这个原语。(“去糖”是指采用一种方便的语法并用更基本的核心原语来描述它)。

[1]在ES5规范中,call方法是用另一个更底层的原语来描述的,但它是在那个原语之上的简单封装,所以我在这里简化了一下。有关更多信息,请参阅本文末尾。

简单的函数调用

显而易见,一直用call调用函数将会非常烦人。JavaScript允许我们直接使用括号语法hello("world")来调用函数。当我们这样做时,调用“去糖”如下:

function hello(thing) {
  console.log("Hello " + thing);
}

// this:
hello("world")

// desugars to:
hello.call(window, "world");

仅在使用严格模式[2]的ECMAScript 5中,此行为将改变:

// this:
hello("world")

// desugars to:
hello.call(undefined, "world");

简短版本的说法是:fn(...args)这样的函数调用和fn.call(window [ES5-strict: undefined], ...args)是一模一样的
注意,对于行内声明的函数(function() {})()也是成立的:(function() {})()(function() {}).call(window [ES5-strict: undefined)是一模一样的。

[2]事实上,我撒了一点小谎。ECMAScript 5规范说undefined(几乎)总是被传递,但不在严格模式下时被调用函数应该将其thisValue更改为全局对象。这允许严格模式下调用者避免破坏现有的非严格模式库。

成员函数

调用方法的下一个非常普遍的方式是作为一个对象的一个成员 (person.hello())。在这种情况下,调用“去糖”如下:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this + " says hello " + thing);
  }
}

// this:
person.hello("world")

// desugars to this:
person.hello.call(person, "world");

注意,hello方法在这种形式下是如何附加到对象上是无关紧要的。请记住,我们之前将hello定义为一个独立函数。接下来我们看看如果动态地将其附加到对象上会发生什么:

function hello(thing) {
  console.log(this + " says hello " + thing);
}

person = { name: "Brendan Eich" }
person.hello = hello;

person.hello("world") // still desugars to person.hello.call(person, "world")

hello("world") // "[object DOMWindow]world"

注意,函数对其this值没有一贯的定义,它总是在调用时根据调用者调用的方式进行设置。

使用Function.prototype.bind

因为引用this值一贯不变的函数有时是很方便的,人们历来使用一个简单的闭包技巧将函数转换为this值一贯不变的对应函数:

var person = {
  name: "Brendan Eich",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var boundHello = function(thing) { return person.hello.call(person, thing); }

boundHello("world");

尽管我们的boundHello调用仍然“去糖”为boundHello.call(window, "world"),但我们改变方向并使用我们的原语call方法将this值更改回我们想要的值。
我们做些调整可以把这个技巧变为通用解法:

var bind = function(func, thisValue) {
  return function() {
    return func.apply(thisValue, arguments);
  }
}

var boundHello = bind(person.hello, person);
boundHello("world") // "Brendan Eich says hello world"

为了理解这一点,您只需要两个额外的知识。首先,arguments是一个类Array对象,它表示传递给函数的所有参数。其次,apply方法的工作原理和call原语除了它采用类Array对象而不是一次列出一个参数之外完全一样。
我们的bind方法简单地返回一个新函数。当它被调用时,我们的新函数只是调用传入的原始函数,并将原始值设置为其this值,当然它也传递参数。
因为这是一个有点常见的习惯用法,ES5在所有Function对象上引入了一个新方法bind,实现了此行为:

var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"

当您需要将原始函数作为回调传递时,此方法将非常有用:

var person = {
  name: "Alex Russell",
  hello: function() { console.log(this.name + " says hello world"); }
}

$("#some-div").click(person.hello.bind(person));

// when the div is clicked, "Alex Russell says hello world" is printed

确实,这有点笨,TC39(负责ECMAScript下一版本的委员会)将继续致力于一个更优雅、向后兼容的解决方案。

面向jQuery

由于jQuery中大量使用匿名回调函数,因此它在内部使用call方法将这些回调的this值设置为更有用的值。举个例子,在所有事件处理程序中(如不进行特殊干预),jQuery不接收window作为其this值,而是通过把设置事件处理程序的元素作为它第一个参数在回调函数上调用call
这非常有用,因为匿名回调函数中的默认this的值并不是特别有用,除了它给初学者对javascript的一种印象,this通常是一个奇怪的,经常变动至于难以解释的概念。
如果你理解了将“含糖”函数调用转换为“已去糖”的func.call(thisValue, ...args)的基本规则,那么你应该能够在并不是那么危险的JavaScriptthis水域中航行。

PS:我撒谎的部分

在个别地方,我从规范的确切措辞中略微简化了事实。可能最严重的欺骗是我称呼func.call为原语的说法。实际上,规范有一个func.call[obj.]func()都使用的原语(内部称为[[Call]])。
然而,还是看一下func.call的定义吧:

如果IsCallable(func)值为false,则抛出TypeError异常

argList为一个空的List

如果使用多个参数调用此方法,则从arg1开始,从左往右将每个参数追加为argList的最后一个元素

提供thisArg作为this的值,并将argList作为参数列表,返回调用func的内部方法[[Call]]的结果

如你所见,此定义本质上是一种很简单的JavaScript语义绑定到原语[[Call]]操作。
如果你看一下调用函数的定义,前七个步骤设置thisValueargList,最后一步是:“提供thisArg作为this的值,并将列表argList作为参数值,返回调用func的内部方法[[Call]]的结果。”
一旦确定了argListthisValue,它基本上是相同的措辞。
我在称call是一个原语时作了一些欺骗,但其含义基本上与我在文章开头提出的规范和引用的章节是一样的。
还有一些我没有在这里介绍的其他案例(最值得注意的是with)。

原文地址

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96465.html

相关文章

  • 学习js中'this'关键字

      在JavaScript中‘this’关键字是一个非常重要的概念,我们虽然知道它重要,但它也十分的晦涩难懂,也给我们学习造成不小的困扰。  什么是'this'关键字  'this'关键字是为每个执行上下文(每个函数)创建的一个特殊变量;所以一般来说,在使用'this'关键字的函数中,'this'永远是取其所有者的值。总结一句话是该函...

    3403771864 评论0 收藏0
  • 一道JS面试题所引发"血案",透过现象寻本质,再从本质看现象

    摘要:一看这二逼就是周杰伦的死忠粉看看控制台输出,确实没错就是对象。从根本上来说,作用域是基于函数的,而执行环境是基于对象的例如全局执行环境即全局对象。全局对象全局属性和函数可用于所有内建的对象。全局对象只是一个对象,而不是类。 觉得本人写的不算很烂的话,可以登录关注一下我的GitHub博客,博客会坚持写下去。 今天同学去面试,做了两道面试题,全部做错了,发过来给我看,我一眼就看出来了,因为...

    QiShare 评论0 收藏0
  • ahooks useRequest源码深入解读

      大家会发现,自从 React v16.8 推出了 Hooks API,前端框架圈并开启了新的逻辑复用的时代,从此无需在意 HOC 的无限套娃导致性能差的问题,同时也解决了 mixin 的可阅读性差的问题。这里也有对于 React 最大的变化是函数式组件可以有自己的状态,扁平化的逻辑组织方式,更加友好地支持 TS 类型声明。  在运用Hooks的时候,除了 React 官方提供的,同时也支持我们...

    3403771864 评论0 收藏0
  • 深入理解Redis 数据结构—简单动态字符串sds

    摘要:本文主要介绍的数据结构简单动态字符串简称。遵守字符串以空字符串结尾的惯例,保存的空字符串一个字节空间不计算在的属性里面。添加空字符串到字符串末尾等操作,都是由函数自动完成的,所以这个空字符对于使用者来说完全是透明的。Redis是用ANSI C语言编写的,它是一个高性能的key-value数据库,它可以作用在数据库、缓存和消息中间件。其中 Redis 键值对中的键都是 string 类型,而键...

    番茄西红柿 评论0 收藏2637
  • 【译】使用"BinaryAST"加快JavaScript脚本解析速度?

    摘要:是提出并积极开发的一种新的在线格式,旨在加快解析速度,同时保持原始的语义不变。它的实现方式是使用有效的二进制来表示代码和数据结构,并且存储和提供额外的信息来提前指导解析器工作。提升依赖于提升所有声明变量函数类。 原文:Faster script loading with BinaryAST?本文首发于公众号:符合预期的CoyPan JavaScirpt的冷启动 web应用的表现,越来...

    Hujiawei 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<