JavaScript中this机制是如何真正工作的

 更新时间:2023年11月04日 14:24:15   投稿:yin  
JavaScript中this机制提供了更优雅的方式来隐含地“传递”一个对象引用,导致更加干净的API设计和更容易的复用,this既不是函数自身的引用,也不是函数词法作用域的引用,this实际上是在函数被调用时建立的一个绑定,它指向什么是完全由函数被调用的调用点来决定的

this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。this 既不是函数自身的引用,也不是函数 词法 作用域的引用。this 实际上是在函数被调用时建立的一个绑定,它指向 什么 是完全由函数被调用的调用点来决定的。

当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的 this 引用。

为什么要用 this

如果对于那些老练的 JavaScript 开发者来说 this 机制都是如此的令人费解,那么有人会问为什么这种机制会有用?它带来的麻烦不是比好处多吗?在讲解 如何 有用之前,我们应当先来看看 为什么 有用。

让我们试着展示一下 this 的动机和用途:

function identify() {
	return this.name.toUpperCase();
}

function speak() {
	var greeting = "Hello, I'm " + identify.call( this );
	console.log( greeting );
}

var me = {
	name: "Kyle"
};

var you = {
	name: "Reader"
};

identify.call( me ); // KYLE
identify.call( you ); // READER

speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER

如果这个代码段 如何 工作让你困惑,不要担心!我们很快就会讲解它。只是简要地将这些问题放在旁边,以便于我们可以更清晰的探究 为什么。

这个代码片段允许 identify() 和 speak() 函数对多个 环境 对象(me 和 you)进行复用,而不是针对每个对象定义函数的分离版本。

与使用 this 相反地,你可以明确地将环境对象传递给 identify() 和 speak()

function identify(context) {
	return context.name.toUpperCase();
}

function speak(context) {
	var greeting = "Hello, I'm " + identify( context );
	console.log( greeting );
}

identify( you ); // READER
speak( me ); // Hello, I'm KYLE

然而,this 机制提供了更优雅的方式来隐含地“传递”一个对象引用,导致更加干净的API设计和更容易的复用。

你的使用模式越复杂,你就会越清晰地看到:将执行环境作为一个明确参数传递,通常比传递 this 执行环境要乱。当我们探索对象和原型时,你将会看到一组可以自动引用恰当执行环境对象的函数是多么有用。

this的困惑

我们很快就要开始讲解 this 是如何 实际 工作的,但我们首先要摒弃一些误解——它实际上 不是 如何工作的。

在开发者们用太过于字面的方式考虑“this”这个名字时就会产生困惑。这通常会产生两种臆测,但都是不对的。

第一种常见的倾向是认为 this 指向函数自己。至少,这是一种语法上的合理推测。

为什么你想要在函数内部引用它自己?最常见的理由是递归(在函数内部调用它自己)这样的情形,或者是一个在第一次被调用时会解除自己绑定的事件处理器。

初次接触 JS 机制的开发者们通常认为,将函数作为一个对象(JavaScript 中所有的函数都是对象!),可以让你在方法调用之间储存 状态(属性中的值)。这当然是可能的,而且有一些有限的用处,但这本书的其余部分将会阐述许多其他的模式,提供比函数对象 更好 的地方来存储状态。

this的作用域

对 this 的含义第二常见的误解,是它不知怎的指向了函数的作用域。这是一个刁钻的问题,因为在某一种意义上它有正确的部分,而在另外一种意义上,它是严重的误导。

明确地说,this 不会以任何方式指向函数的 词法作用域。作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说是对的。但是 JavasScript 代码不能访问作用域“对象”。它是 引擎 的内部实现。

考虑下面代码,它(失败的)企图跨越这个边界,用 this 来隐含地引用函数的词法作用域:

function foo() {
	var a = 2;
	this.bar();
}

function bar() {
	console.log( this.a );
}

foo(); //undefined

这个代码段里不只有一个错误。虽然它看起来是在故意瞎搞,但你看到的这段代码,提取自在公共社区的帮助论坛中被交换的真实代码。真是难以想象对 this 的臆想是多么的误导人。

首先,试图通过 this.bar() 来引用 bar() 函数。它几乎可以说是 碰巧 能够工作,我们过一会儿再解释它是 如何 工作的。调用 bar() 最自然的方式是省略开头的 this.,而仅使用标识符进行词法引用。

然而,写下这段代码的开发者试图用 this 在 foo() 和 bar() 的词法作用域间建立一座桥,使得bar() 可以访问 foo()内部作用域的变量 a。这样的桥是不可能的。 你不能使用 this 引用在词法作用域中查找东西。这是不可能的。

每当你感觉自己正在试图使用 this 来进行词法作用域的查询时,提醒你自己:这里没有桥。

this调用点(Call-site)

为了理解 this 绑定,我们不得不理解调用点:函数在代码中被调用的位置(不是被声明的位置)。我们必须考察调用点来回答这个问题:这个 this 指向什么?

一般来说寻找调用点就是:“找到一个函数是在哪里被调用的”,但它不总是那么简单,比如某些特定的编码模式会使 真正的 调用点变得不那么明确。

考虑 调用栈(call-stack) (使我们到达当前执行位置而被调用的所有方法的堆栈)是十分重要的。我们关心的调用点就位于当前执行中的函数 之前 的调用。

我们来展示一下调用栈和调用点:

function baz() {
    // 调用栈是: `baz`
    // 我们的调用点是 global scope(全局作用域)

    console.log( "baz" );
    bar(); // <-- `bar` 的调用点
}

function bar() {
    // 调用栈是: `baz` -> `bar`
    // 我们的调用点位于 `baz`

    console.log( "bar" );
    foo(); // <-- `foo` 的 call-site
}

function foo() {
    // 调用栈是: `baz` -> `bar` -> `foo`
    // 我们的调用点位于 `bar`

    console.log( "foo" );
}

baz(); // <-- `baz` 的调用点

在分析代码来寻找(从调用栈中)真正的调用点时要小心,因为它是影响 this 绑定的唯一因素。

注意: 你可以通过按顺序观察函数的调用链在你的大脑中建立调用栈的视图,就像我们在上面代码段中的注释那样。但是这很痛苦而且易错。另一种观察调用栈的方式是使用你的浏览器的调试工具。大多数现代的桌面浏览器都内建开发者工具,其中就包含 JS 调试器。在上面的代码段中,你可以在调试工具中为 foo() 函数的第一行设置一个断点,或者简单的在这第一行上插入一个 debugger 语句。当你运行这个网页时,调试工具将会停止在这个位置,并且向你展示一个到达这一行之前所有被调用过的函数的列表,这就是你的调用栈。所以,如果你想调查this 绑定,可以使用开发者工具取得调用栈,之后从上向下找到第二个记录,那就是你真正的调用点。

到此这篇关于JavaScript中this机制是如何真正工作的的文章就介绍到这了,更多相关JavaScript中this关键字内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论