无人不识又无人不迷糊的this

火影  金牌会员 | 2024-5-15 10:05:42 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 870|帖子 870|积分 2610

本文分享自华为云社区《3月阅读周·你不知道的JavaScript | 无人不识又无人不迷糊的this》,作者: 叶一一。
关于this

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。
为什么要用this

随着开发者的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越杂乱,使用this则不会如许。
比如下面的例子:
  1. function identify() {
  2.   return this.name.toUpperCase();
  3. }
  4. function speak() {
  5.   var greeting = "Hello, I'm " + identify.call(this);
  6.   console.log(greeting);
  7. }
  8. var me = {
  9.   name: 'Kyle',
  10. };
  11. var you = {
  12.   name: 'Reader',
  13. };
  14. console.log(identify.call(me));
  15. console.log(identify.call(you));
  16. speak.call(me);
  17. speak.call(you);
复制代码
打印一下效果:

上面的代码可以在不同的上下文对象(me和you)中重复使用函数identify()和speak(),不消针对每个对象编写不同版本的函数。假如不使用this,那就需要给identify()和speak()显式传入一个上下文对象。
误解

有两种常见的对于this的表明,但是它们都是错误的。
1、指向自身
人们很容易把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被调用了多少次?
  14. console.log(foo.count);
复制代码
打印效果:

console.log语句产生了4条输出,证明foo(..)确实被调用了4次,但是foo.count仍然是0。显然从字面意思来明白this是错误的。
执行foo.count = 0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个函数对象。
2、它的作用域
第二种常见的误解是,this指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。
this在任何情况下都不指向函数的词法作用域。
  1. function foo() {
  2.   var a = 2;
  3.   this.bar();
  4. }
  5. function bar() {
  6.   console.log(this.a);
  7. }
  8. foo();
复制代码
直接打印上面的代码会得到一个报错:

这段代码试图通过this.bar()来引用bar()函数。这是不可能实现的,使用this不可能在词法作用域中查到什么。
每当开发者想要把this和词法作用域的查找混合使用时,一定要提示自己,这是无法实现的。
this到底是什么

this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(偶然候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
this全面解析

调用位置

在明白this的绑定过程之前,首先要明白调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
探求调用位置就是探求“函数被调用的位置”。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。
通过下面的代码来看什么是调用栈和调用位置:
  1. function baz() {
  2.   // 当前调用栈是:baz
  3.   // 因此,当前调用位置是全局作用域
  4.   console.log('baz');
  5.   bar(); // <-- bar的调用位置
  6. }
  7. function bar() {
  8.   // 当前调用栈是baz -> bar
  9.   // 因此,当前调用位置在baz中
  10.   console.log('bar');
  11.   foo(); // <-- foo的调用位置
  12. }
  13. function foo() {
  14.   // 当前调用栈是baz -> bar -> foo
  15.   // 因此,当前调用位置在bar中
  16.   console.log('foo');
  17. }
  18. baz(); // <-- baz的调用位置
复制代码
(2)函数是否通过call、apply(显式绑定)或者硬绑定调用?假如是的话,this绑定的是指定的对象。
  1. var a = 2;
  2. function foo() {
  3.   console.log(this.a);
  4. }
  5. foo(); // 2
复制代码
(3)函数是否在某个上下文对象中调用(隐式绑定)?假如是的话,this绑定的是那个上下文对象。
  1. function foo() {
  2.   console.log(this.a);
  3. }
  4. var obj = {
  5.   a: 2,
  6.   foo: foo,
  7. };
  8. obj.foo(); // 2
复制代码
(4)假如都不是的话,使用默认绑定。假如在严格模式下,就绑定到undefined,否则绑定到全局对象。
  1. function foo() {
  2.   console.log(this.a);
  3. }
  4. var obj = {
  5.   a: 2,
  6. };
  7. foo.call(obj); // 2
复制代码
绑定例外

在某些场景下this的绑定举动会出乎意料,你认为应当应用其他绑定规则时,现实上应用的可能是默认绑定规则。
被忽略的this

假如你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,现实应用的是默认绑定规则:
  1. something = new MyClass(..);
复制代码
那么什么情况下会传入null呢?
一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法偶然非常有用。
间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
  1. var bar = new foo();
复制代码
赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。根据我们之前说过的,这里会应用默认绑定。
软绑定

假如可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的本领。
[code]function foo() {  console.log('name: ' + this.name);}var obj = { name: 'obj' },  obj2 = { name: 'obj2' },  obj3 = { name: 'obj3' };var fooOBJ = foo.softBind(obj);fooOBJ(); // name: objobj2.foo = foo.softBind(obj);obj2.foo(); // name: obj2

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

火影

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表