第一章: this 是什么? - 困惑

优质
小牛编辑
125浏览
2023-12-01

困惑

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

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

它自己

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

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

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

过一会儿我们将探索一个模式,来展示 this 是如何不让一个函数像我们可能假设的那样,得到它自身的引用的。

考虑下面的代码,我们试图追踪函数(foo)被调用了多少次:

  1. function foo(num) {
  2. console.log( "foo: " + num );
  3. // 追踪 `foo` 被调用了多少次
  4. this.count++;
  5. }
  6. foo.count = 0;
  7. var i;
  8. for (i=0; i<10; i++) {
  9. if (i > 5) {
  10. foo( i );
  11. }
  12. }
  13. // foo: 6
  14. // foo: 7
  15. // foo: 8
  16. // foo: 9
  17. // `foo` 被调用了多少次?
  18. console.log( foo.count ); // 0 -- 这他妈怎么回事……?

foo.count 依然0, 即便四个 console.log 语句明明告诉我们 foo(..) 实际上被调用了四次。这种挫败来源于对于 this(在 this.count++ 中)的含义进行了 过于字面化 的解释。

当代码执行 foo.count = 0 时,它确实向函数对象 foo 添加了一个 count 属性。但是对于函数内部的 this.count 引用,this 其实 根本就不 指向那个函数对象,即便属性名称一样,但根对象也不同,因而产生了混淆。

注意: 一个负责任的开发者 应当 在这里提出一个问题:“如果我递增的 count 属性不是我以为的那个,那是哪个 count 被我递增了?”。实际上,如果他再挖的深一些,他会发现自己不小心创建了一个全局变量 count(第二章解释了这是 如何 发生的!),而且它当前的值是 NaN。当然,一旦他发现这个不寻常的结果后,他会有一堆其他的问题:“它怎么是全局的?为什么它是 NaN 而不是某个正确的计数值?”。(见第二章)

与停在这里来深究为什么 this 引用看起来不是如我们 期待 的那样工作,并且回答那些尖锐且重要的问题相反,许多开发者简单地完全回避这个问题,转向一些其他的另类解决方法,比如创建另一个对象来持有 count 属性:

  1. function foo(num) {
  2. console.log( "foo: " + num );
  3. // 追踪 `foo` 被调用了多少次
  4. data.count++;
  5. }
  6. var data = {
  7. count: 0
  8. };
  9. var i;
  10. for (i=0; i<10; i++) {
  11. if (i > 5) {
  12. foo( i );
  13. }
  14. }
  15. // foo: 6
  16. // foo: 7
  17. // foo: 8
  18. // foo: 9
  19. // `foo` 被调用了多少次?
  20. console.log( data.count ); // 4

虽然这种方式“解决”了问题是事实,但不幸的是它简单地忽略了真正的问题 —— 缺乏对于 this 的含义和其工作方式上的理解 —— 反而退回到了一个他更加熟悉的机制的舒适区:词法作用域。

注意: 词法作用域是一个完善且有用的机制;我不是在用任何方式贬低它的作用(参见本系列的 “作用域与闭包”)。但在如何使用 this 这个问题上总是靠 ,而且通常都猜 ,并不是一个退回到词法作用域,而且从不学习 为什么 this 不跟你合作的好理由。

为了从函数对象内部引用它自己,一般来说通过 this 是不够的。你通常需要通过一个指向它的词法标识符(变量)得到函数对象的引用。

考虑这两个函数:

  1. function foo() {
  2. foo.count = 4; // `foo` 引用它自己
  3. }
  4. setTimeout( function(){
  5. // 匿名函数(没有名字)不能引用它自己
  6. }, 10 );

第一个函数,称为“命名函数”,foo 是一个引用,可以用于在它内部引用自己。

但是在第二个例子中,传递给 setTimeout(..) 的回调函数没有名称标识符(所以被称为“匿名函数”),所以没有合适的办法引用函数对象自己。

注意: 在函数中有一个老牌儿但是现在被废弃的,而且令人皱眉头的 arguments.callee 引用 指向当前正在执行的函数的函数对象。这个引用通常是匿名函数在自己内部访问函数对象的唯一方法。然而,最佳的办法是完全避免使用匿名函数,至少是对于那些需要自引用的函数,而使用命名函数(表达式)。arguments.callee 已经被废弃而且不应该再使用。

对于当前我们的例子来说,另一个 好用的 解决方案是在每一个地方都使用 foo 标识符作为函数对象的引用,而根本不用this

  1. function foo(num) {
  2. console.log( "foo: " + num );
  3. // 追踪 `foo` 被调用了多少次
  4. foo.count++;
  5. }
  6. foo.count = 0;
  7. var i;
  8. for (i=0; i<10; i++) {
  9. if (i > 5) {
  10. foo( i );
  11. }
  12. }
  13. // foo: 6
  14. // foo: 7
  15. // foo: 8
  16. // foo: 9
  17. // `foo` 被调用了多少次?
  18. console.log( foo.count ); // 4

然而,这种方法也类似地回避了对 this真正 理解,而且完全依靠变量 foo 的词法作用域。

另一种解决这个问题的方法是强迫 this 指向 foo 函数对象:

  1. function foo(num) {
  2. console.log( "foo: " + num );
  3. // 追踪 `foo` 被调用了多少次
  4. // 注意:由于 `foo` 的被调用方式(见下方),`this` 现在确实是 `foo`
  5. this.count++;
  6. }
  7. foo.count = 0;
  8. var i;
  9. for (i=0; i<10; i++) {
  10. if (i > 5) {
  11. // 使用 `call(..)`,我们可以保证 `this` 指向函数对象(`foo`)
  12. foo.call( foo, i );
  13. }
  14. }
  15. // foo: 6
  16. // foo: 7
  17. // foo: 8
  18. // foo: 9
  19. // `foo` 被调用了多少次?
  20. console.log( foo.count ); // 4

与回避 this 相反,我们接受它。 我们马上将会更完整地讲解这样的技术 如何 工作,所以如果你依然有点儿糊涂,不要担心!

它的作用域

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

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

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

  1. function foo() {
  2. var a = 2;
  3. this.bar();
  4. }
  5. function bar() {
  6. console.log( this.a );
  7. }
  8. foo(); //undefined

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

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

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

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