[译] 让咱们一块儿解决“this”难题 — 第一部分

难道咱们就不能完全搞清楚“this”吗?在某种程度上,几乎全部的 JavaScript 开发人员都曾经思考过“this”这个事情。对我来讲,每当“this”出来捣乱的时候,我就会千方百计地去解决掉它,但事后就把它忘了,我想你应该也曾遇到过相似的场景。可是今天,让咱们弄明白它,让咱们一次性地完全解决“this”的问题,一劳永逸。前端

前几天,我在图书馆遇到了一个意想不到的事情。android

这本书的整个第二章都是关于“this”的,我颇有自信地通读了一遍,可是发现其中有些地方讲到的“this”,我竟然搞不懂它们是什么,须要去猜想。真的是时候检讨一下我过分自信的愚蠢行为了。我再次把这一章重读了好几遍,发觉这里面的内容是每一个 Javascript 开发人员都应该了解的。ios

所以,我尝试着用一种更完全的方式和更多的示例代码来展现 凯尔·辛普森 在他的这本书 你不知道的 Javascript 中描述的那些规范。git

在这里我不会通篇只讲理论,我会直接以曾经困扰过个人困难问题为例开始讲起,我但愿它们也是你感到困难的问题。但无论这些问题是否会困挠你,我都会给出解释说明,我会一个接一个地向你介绍全部的规则,固然还会有一些追加内容。github

在开始以前,我假设你已经了解了一些 JavaScript 的背景知识,当我讲到 global、window、this、prototype 等等的时候,你知道它们是什么意思。这篇文章中,我会同时使用 global 和 window,在这里它们就是一回事,是能够互换的。后端

在下面给出的全部代码示例中,你的任务就是猜一下控制台输出的结果是什么。若是你猜对了,就给你本身加一分。准备好了吗?让咱们开始吧。数组

Example #1

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

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

function baz() {  
 console.log(this);   
}

foo();
复制代码

你被难住了吗?为了测试,你固然能够把这段代码复制下来,而后在浏览器或者 Node 的运行环境中去运行看看结果。再来一次,你被难住了吗?好吧,我就再也不问了。但说真的,若是你没被难住,那就给你本身加一分。浏览器

若是你运行上面的代码,就会在控制台中看到 global 对象被打印出三次。为了解释这一点,让我来介绍 第一个规则,默认绑定。规则规定,当一个函数执行独立调用时,例如只是 funcName();,这时函数的“this”被指向 global 对象。函数

须要理解的是,在调用函数以前,“this”并无绑定到这个函数,所以,要找到“this”,你应该密切注意该函数是如何调用,而不是在哪里调用。全部三个函数 foo();bar(); 和 baz();_ 都是独立的调用,所以这三个函数的“this”都指向全局对象。post

Example #2

‘use strict’;
function foo() {
 console.log(this); 
 bar();
}
function bar() {
 console.log(this); 
 baz();
}
function baz() {
 console.log(this); 
}
foo();
复制代码

注意下最开始的“use strict”。在这种状况下,你以为控制台会打印什么?固然,若是你了解 strict mode,你就会知道在严格模式下 global 对象不会被默认绑定。因此,你获得的打印是三次 undefined 的输出,而再也不是 global

回顾一下,在一个简单调用函数中,好比独立调用中,“this”在非严格模式下指向 global 对象,但在严格模式下不容许 global 对象默认绑定,所以这些函数中的“this”是 undefined。

为了使咱们对默认绑定概念理解得更加具体,这里有一些示例。

Example #3

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

foo();
复制代码

foo 先被调用,而后又调用 barbar 将“this”打印到控制台中。这里的技巧是看看函数是如何被调用的。foobar 都被单独调用,所以,他们内部的“this”都是指向 global 对象。可是因为 bar 是惟一执行打印的函数,因此咱们看到 global 对象在控制台中输出了一次。

我但愿你没有回答 foobar。有没有?

咱们已经了解了默认绑定。让咱们再作一个简单的测试。在下面的示例中,控制台输出什么?

Example #4

var a = 1;

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

foo();
复制代码

输出结果是 undefined?是 1?仍是什么?

若是你已经很好地理解了以前讲解的内容,那么你应该知道控制台输出的是“1”。为何?首先,默认绑定做用于函数 foo。所以 foo 中的“this”指向 global 对象,而且 a 被声明为 global 变量,这就意味着 a 是 global 对象的属性(也称之为全局对象污染),所以 this.avar a 就是同一个东西。

随着本文的深刻,咱们将会继续研究默认绑定,可是如今是时候向你介绍下一个规则了。

Example #5

var obj = {  
 a: 1,   
 foo: function() {  
  console.log(this);   
 }  
};

obj.foo();
复制代码

这里应该没有什么疑问,对象“obj”会被输出在控制台中。你在这里看到的是 隐式绑定。规则规定,当一个函数被做为一个对象方法被调用时,那么它内部的“this”应该指向这个对象。若是函数调用前面有多个对象( obj1.obj2.func() ),那么函数以前的最后一个对象(obj3)会被绑定。

须要注意的一点是函数调用必须有效,那也就是说当你调用 obj.func() 时,必须确保 func 是对象 obj 的属性。

所以,在上面的例子中调用 obj.foo() 时,“this”就指向 obj,所以 obj 被打印输出在控制台中。

Example #6

function logThis() {  
 console.log(this);  
}

var myObject = {  
 a: 1,   
 logThis: logThis  
};

logThis();  
myObject.logThis();
复制代码

你被难住了?我但愿没有。

跟在 myObject 后面的这个全局调用 logThis() 经过 console.log(this) 打印的是 global 对象;而 myObject.logThis() 打印的是 myObject 对象。

这里须要注意一件有趣的事情:

console.log(logThis === myObject.logThis); // true
复制代码

为何不呢?它们固然是相同的函数,可是你能够看到 如何调用_logThis_ 会让其中的“this”发生改变。当 logThis 被单独调用时,使用默认绑定规则,可是当 logThis 做为前面的对象属性被调用时,使用隐式绑定规则。

无论采用哪条规则,让咱们看看是怎么处理的(双关语)。

Example #8

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

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

foo();
复制代码

控制台输出什么?首先,你可能会问咱们能够调用“_this.bar()”吗?固然能够,它不会致使错误。

就像示例 #4 中的 var a 同样,bar 也是全局对象的属性。由于 foo 被单独调用了,它内部的“this”就是全局对象(默认绑定规则)。所以 foo 内部的 this.bar 就是 bar。但实际的问题是,控制台中输出什么?

若是你猜的没错,“undefined”会被打印出来。

注意 bar 是如何被调用的?看起来,隐式绑定在这里发挥做用。隐式绑定意味着 bar 中的“this”是其前面的对象引用。bar 前面的对象引用是全局对象,在 foo 里面是全局对象,对不对?所以在 bar 中尝试访问 this.a 等同于访问 [global object].a。没有什么意外,所以控制台会输出 undefined。

太棒了!继续向下讲解。

Example #7

var obj = {  
 a: 1,   
 foo: function(fn) {  
  console.log(this);  
  fn();  
 }  
};

obj.foo(function() {  
 console.log(this);  
});
复制代码

请不要让我失望。

函数 foo 接受一个回调函数做为参数。咱们所作的就是在调用 foo 的时候在参数里面放了一个函数。

obj.foo( function() { console.log(this); } );
复制代码

可是请注意 foo如何 被调用的。它是一个单独调用吗?固然不是,所以第一个输出到控制台的是对象 obj 。咱们传入的回调函数是什么?在 foo 内部,回调函数变为 fn ,注意 fn如何 被调用的。对,所以 fn 中的“this”是全局对象,所以第二个被输出到控制台的是全局对象。

但愿你不会以为无聊。顺便问一下,你的分数怎么样?还能够吗?好吧,此次我准备难倒你了。

Example #8

var arr = [1, 2, 3, 4];

Array.prototype.myCustomFunc = function() {
 console.log(this);
};

arr.myCustomFunc();
复制代码

若是你还不知道 Javascript 里面的 .prototype 是什么,那你就权且把它和其余对象等同看待,但若是你是 JavaScript 开发者,你应该知道。你知道吗?努努力,再去多读一些关于原型链相关的书籍吧。我在这里等着你。

那么打印输出的是什么?是 Array.prototype 对象?错了!

这是和以前相同的技巧,请检查 custommyfunc如何 被调用的。没错,隐式绑定把 arr 绑定到 myCustomFunc,所以输出到控制台的是 arr[1,2,3,4]

我说的,你理解了吗?

Example #9

var arr = [1, 2, 3, 4];

arr.forEach(function() {  
 console.log(this);  
});
复制代码

执行上述代码的结果是,在控制台中输出了 4 次全局对象。若是你错了,也不要紧。请再看示例#7。还没理解?下一个示例会有所帮助。

Example #10

var arr = [1, 2, 3, 4];

Array.prototype.myCustomFunc = function(fn) {  
 console.log(this);  
 fn();  
};

arr.myCustomFunc(function() {  
 console.log(this);   
});
复制代码

就像示例 #7 同样,咱们将回调函数 fn 做为参数传递给函数 myCustomFunc。结果是传入的函数会被独立调用。这就是为何在前面的示例(#9)中输出全局对象,由于在 forEach 中传入的回调函数被独立调用。

相似地,在本例中,首先输出到控制台的是 arr,而后是输出的是全局对象。我知道这看上去有点复杂,但我相信若是你能再多用点心,你会弄明白的。

让咱们继续使用这个数组的示例来介绍更多的概念。我想我会在这里使用一个简称,WGL 怎么样?做为 WHAT.GETS.LOGGED 的简称?好吧,在我开始老生常谈以前,下面是另一个例子。

Example #11

var arr = [1, 2, 3, 4];

Array.prototype.myCustomFunc = function() {  
 console.log(this);

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

};

arr.myCustomFunc();
复制代码

那么,输出是?

答案和示例 #10 彻底同样。轮到你了,说一说为何首先输出的是 arr?你看到第一个 console.log(this) 的下面有一段复杂的代码,它被称为 IIFE(当即调用的函数表达式)。这个名字不用再过多解释了,对吧?被 (…)(); 这样形式封装的函数会当即被调用,也就是说等同于被独立调用,所以它内部的“this”是全局变量,因此输出的是全局变量。

要来新概念了!让咱们看看你对 ES2015 的熟悉程度。

Example #12

var arr = [1, 2, 3, 4];

Array.prototype.myCustomFunc = function() {  
 console.log(this);

 (function() {  
  console.log(‘Normal this : ‘, this);  
 })();

 (() =\> {  
  console.log(‘Arrow function this : ‘, this); })(); }; arr.myCustomFunc(); 复制代码

除了 IIFE 后面的增长了 3 行代码以外,其余代码与示例 #11 彻底相同。它实际上也是一种 IIFE,只是语法稍有不一样。嗨,这是箭头函数。

箭头函数的意思是,这些函数中的“this”是一个词法变量。也就是说,当将“this”与这种箭头函数绑定时,函数会从包裹它的函数或做用域中获取“this”的值。包裹咱们这个箭头函数的函数里面的“this”是 arr。所以?

// This is WGL
arr [1, 2, 3, 4]
Normal this : global
Arrow function this : arr [1, 2, 3, 4]
复制代码

若是我用箭头函数重写示例 #9 会怎么样?控制台输出什么呢?

var arr = [1, 2, 3, 4];

arr.forEach(() => {
 console.log(this);
});
复制代码

上面的这个例子是额外追加的,因此即便你猜对了也不用增长分数。你还在算分吗?书呆子。

如今请仔细关注如下示例。我会不惜一切代价让你弄懂他们 :-)。

Example #13

var yearlyExpense = {

 year: 2016,

 expenses: [
   {‘month’: ‘January’, amount: 1000}, 
   {‘month’: ‘February’, amount: 2000}, 
   {‘month’: ‘March’, amount: 3000}
  ],

 printExpenses: function() {
  this.expenses.forEach(function(expense) {
   console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +    this.year);
   });
  }

};

yearlyExpense.printExpenses();
复制代码

那么,输出是?多点时间想想。

这是答案,但我但愿你在阅读解释以前先本身想一想。

1000 spent in January, undefined  
2000 spent in February, undefined  
3000 spent in March, undefined
复制代码

这都是关于 printExpenses 函数的。首先注意下它是如何被调用的。隐式绑定?是的。因此 printExpenses 中的“this”指向的是对象 yearlycost。这意味着 this.expensesyearlyExpense 对象中的 expenses 数组,因此这里没有问题。如今,当它在传递给 forEach 的回调函数中出现“this”时,它固然是全局对象,请参考例 #9。

注意,下面的“修正”版本是如何使用箭头函数进行改进的。

var expense = {

 year: 2016,

 expenses: [
   {‘month’: ‘January’, amount: 1000}, 
   {‘month’: ‘February’, amount: 2000}, 
   {‘month’: ‘March’, amount: 3000}
  ],

 printExpenses: function() {
   this.expenses.forEach((expense) => {
    console.log(expense.amount + ‘ spent in ‘ + expense.month + ‘, ‘ +  this.year);
   });
  }

};

expense.printExpenses();
复制代码

这样咱们就获得了想要的输出结果:

1000 spent in January, 2016  
2000 spent in February, 2016  
3000 spent in March, 2016
复制代码

到目前为止,咱们已经熟悉了隐式绑定和默认绑定。咱们如今知道函数被调用的方式决定了它里面的“this”。咱们还简要地讲了箭头函数以及它们内部的“this”是怎样定义的。

在咱们讨论其余规则以前,你应该知道,有些状况下,咱们的“this”可能会丢失隐式绑定。让咱们快速地看一下这些例子。

Example #14

var obj = {  
 a: 2,  
 foo: function() {  
  console.log(this);  
 }  
};

obj.foo();

var bar = obj.foo;  
bar();
复制代码

不要被这里面的花哨代码所分心,只需注意函数是如何被调用的,就能够弄明白“this”的含义。你如今必定已经掌握这个技巧了吧。首先 obj.foo() 被调用,由于 foo 前面有一个对象引用,因此首先输出的是对象 objbar 固然是被独立调用的,所以下一个输出是全局变量。提醒你一下,记住在严格模式下,全局对象是不会默认绑定的,所以若是你在开启了严格模式,那么控制台输出的就是 undefined,而再也不是全局变量。

bar 和 foo 是对同一个函数的引用,惟一区别是它们被调用的方式不一样。

Example #15

var obj = {  
 a: 2,  
 foo: function() {  
  console.log(this.a);  
 }  
};

function doFoo(fn) {  
 fn();  
}

doFoo(obj.foo);
复制代码

这里也没什么特别的。咱们是经过把 obj.foo 做为 doFoo 函数的参数(doFoo 这个名字听起来颇有趣)。一样, fnfoo 是对同一个函数的引用。如今我要重复一样的分析过程, fn 被独立调用,所以 fn 中的“this”是全局对象。而全局对象没有属性 a,所以咱们在控制台中获得了 undifined 的输出结果。

到这里,咱们这部分就讲完了。在这一部分中,咱们讨论了将“this”绑定到函数的两个规则。默认绑定和隐式绑定。咱们研究了如何使用“use strict”来影响全局对象的绑定,以及如何会让隐式绑定的“this”失效。我但愿在接下来的第二部分中,你会发现本文对你有所帮助,在那里咱们将介绍一些新规则,包括 new 和显式绑定。那里再见吧!


在咱们结束以前,我想用一个“简单”的例子来做为这一部分的收尾,当我开始使用 Javascript 时,这个例子曾经让我感到很是震惊。Javascript 里面也并非全部的东西都是美的,也有看起来很糟糕的东西。让咱们看看其中的一个。

var obj = {  
 a: 2,  
 b: this.a * 2  
};

console.log( obj.b ); // NaN
复制代码

它读起来感受很好,在 obj 里面,“this”应该是 obj,所以是 this.a 应该是 2。嗯,错了。由于在这个对象里面的“this”是全局对象,因此若是你像这么写…

var myObj = {  
 a: 2,  
 b: this  
};

console.log(myObj.b); // global
复制代码

控制台输出的就是全局对象。你可能会说“可是,myObj 是全局对象的属性(示例 #4 和示例 #8),不对吗?”是的,绝对正确。

console.log( this === myObj.b ); // true 
console.log( this.hasOwnProperty(‘myObj’) ); //true
复制代码

“也就是说,若是我像这样写的话,它就能够!”

var myObj = {  
 a: 2,  
 b: this.myObj.a * 2  
};
复制代码

遗憾的是,不是这样的,这会致使逻辑错误。上面的代码是不正确的,编译器会抱怨它找不到未定义的属性 a为何会这样?我也不太清楚。

幸运的是,getters(隐式绑定)能够给咱们提供帮助。

var myObj = {  
 a: 2,  
 get b() {  
  return this.a * 2  
 }  
};

console.log( myObj.b ); // 4
复制代码

你坚持到最后了!作得好。第二部分,咱们再见。

若是你发现这篇文章颇有用,你能够推荐并分享给其余开发者。我常常发表文章,在 TwitterMedium 上关注我,以便在这种状况发生时获得通知。

谢谢你的阅读,祝你愉快!

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索