理解JavaScript函数调用和this

3次阅读

共计 2715 个字符,预计需要花费 7 分钟才能阅读完成。

原文链接:原文链接

多年来,我已经看到许多关于 JavaScript 函数调用的困惑。特别是,许多人抱怨函数调用中的 this 语义令人困惑。

在我看来,通过理解核心函数调用语句,然后在该原语之上查看以糖为例调用功能的所有其他方式,可以消除许多此类混淆。

核心原函数

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

  1. 从参数 1 到结尾创建参数列表(argList
  2. 第一个参数是thisValue
  3. 将此函数设置为此 thisValue 并将 argList 作为其参数列表来调用该函数.

例如:

function hello(thing){console.log(this + "says hello" + thing)
}
hello.call("Mandy", "world") // Mandy says hello world

我们调用 hello 方法,他
它的 this 被设置成 Mandy,还有一个参数world 这是 JavaScript 函数调用的核心原语。您可以将所有其他函数调用视为对该核心原语的替代(“替代”是采用一种方便的语法,并以更基本的核心原语进行描述)。

简单函数调用

显然,一直使用 call 调用函数会很烦人。JavaScript使我们可以使用语法 (hello("world")) 直接调用函数。当我们这样做时,调用将会被替代。

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

// this: 
hello("world")

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

当使用严格模式 (use strict) 时,相当于

// this: 
hello("world")

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

函数调用
fn(...args) 相当于
fn.call(window[ES5-strict:undefined], ...args)

请注意,对于内联声明的函数也是如此:
(function() {})() 相当于
(function() {}).call(window [ES5-strict: undefined)

成员方法

方法调用中另一种个非常常见的调用方式是:方法作为对象的成员调用(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 定义为独立函数。让我们看看如果我们动态地将 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")
// [object Object] says hello world

hello("world") // "[object DOMWindow]world"
// [object Window] says hello 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"); // Brendan Eich says hello world

即使我们的 boundHello("world") 调用仍然相当于 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 下一版本的委员会)继续致力于开发一种更加优雅,仍然向后兼容的解决方案。

正文完
 0

理解JavaScript函数调用和this

3次阅读

共计 3705 个字符,预计需要花费 10 分钟才能阅读完成。

前言

我看到很多人都有关于 JavaScript 函数调用的困惑。特别是,很多人抱怨函数调用中的语义令人困惑。

在我看来,通过理解核心函数调用原语,可以清除很多这种混淆,然后查看在该原语之上调用函数作为语法糖的所有其他方法。

事实上,这正是 ECMAScript 规范对此的看法。

在某些地方,这篇文章对 ECMAScript 规范做了一些简化,但基本思路是一样的。


核心原语

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

  1. 从第一个参数到最后一个参数创建一个参数列表(argList)
  2. 第一个参数是 thisValue
  3. 通过调用指向 thisValue 的 this 和表示参数列表的 argList 调用该函数

举例:

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

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

正如你所看到的,我们在调用 hello 时通过 call 使 this 指向 ”Cloudy”,同时为它传递一个 ”world” 参数。这是 JavaScript 函数调用的核心原语。你可以将所有其他函数调用视为对该原语的“脱糖(desugars)”。(所谓“脱糖(desugars)”就是采用更为简的语法并用更基本的核心原语来描述它)。

[1]在 ES5 规范中,调用方法是用另一个更低级的原语来描述的,但它在该原语之上是一个非常薄的包装器,所以我在这里简化了一下。有关更多信息,请参阅本文末尾。

简单的函数调用

显然,一直调用函数会非常烦人。JavaScript 允许我们使用 parens 语法(hello(“world”)直接调用函数。当我们这样做时,调用脱糖:

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

// this:
hello("world")

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

但在 ECMAScript 5 严格模式 [2] 时中会有不一样的结果:

// this:
hello("world")

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

简而言之就是:
一个函数调用

 fn(...args) 

就等价于

fn.call(window [ES5-strict: undefined], ...args).

请注意,对于内联声明的函数也是如此:

(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 的函数:

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”),现在我们重新使用我们的原始调用方法将此值更改回我们想要的值。

我们可以通过一些调整使这个技巧达到通用目的:

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 方法只返回一个新函数。调用它时,我们的新函数只调用传入的原始函数,将原始值设置为此值。它也通过参数传递。

因为这是一个有点常见的习惯用法,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 下一版本的委员会)继续研究更优雅,仍然向后兼容的解决方案。

PS: 一些 cheat

在一些地方,我从规范的确切措辞中略微简化了现实。可能最重要的 cheat 是我称 func.call 为“原始”的方式。实际上,规范有一个原语(内部称为 [[Call]])func.call 和[obj.func() 都使用。

但是,看一下 func.call 的定义:

  1. 如果 IsCallable(func)为 false,则抛出 TypeError 异常。
  2. 让 argList 为空 List。
  3. 如果使用多个参数调用此方法,则从 arg1 开始以从左到右的顺序将每个参数附加为 argList 的最后一个元素
  4. 返回调用 func 的 [[Call]] 内部方法的结果,提供 thisArg 作为此值,并将 argList 作为参数列表。

如您所见,此定义本质上是一种非常简单的 JavaScript 语言绑定到原始 [[Call]] 操作。

如果你看一下调用函数的定义,前七个步骤设置 thisValue 和 argList,最后一步是:“返回在 func 上调用 [[Call]] 内部方法的结果,将 thisValue 作为此值并提供列表 argList 作为参数值。”

一旦确定了 argList 和 thisValue,它基本上是相同的措辞。

我在调用 call 一个原语时作了一些 cheat,但其含义基本上与我在本文开头提取规范并引用章节和诗句时的含义相同。
还有一些我没有在这里介绍的其他案例。


原文请参考:
《Understanding JavaScript Function Invocation and “this”》— 作者 Yehuda Katz


推荐阅读:
【专题:JavaScript 进阶之路】
JavaScript 之“use strict”
JavaScript 之 new 运算符
JavaScript 之 call() 理解
JavaScript 之对象属性


我是 Cloudy,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流前端各种问题!

正文完
 0