JavaScript之例题中彻底理解this

本文共 2025 字,看完只需 8 分钟

概述

前面的文章讲解了 JavaScript 中的执行上下文,作用域,变量对象,this 的相关原理,但是我后来在网上看到一些例题的时候,依然没能全做对,说明自己有些细节还没能掌握,本文就结合例题进行深入实践,讨论函数在不同的调用方式 this 的指向问题。

老规矩,先给结论 1 和 结论2:

this 始终指向最后调用它的对象

“箭头函数”的this,总是指向定义时所在的对象,而不是运行时所在的对象。

特别提示:
本文的例子,最好自己在浏览器控制台中去试一遍,看完过两天就会忘的,一定要实践。

一、隐式绑定
// 例 1
var name = "window";

function foo() {
    var name = "inner";

    console.log(this.name);
}

foo();  // ?
复制代码

输出:

window

例 1 中,非严格模式,由于 foo 函数是在全局环境中被调用,this 会被默认指向全局对象 window;

所以符合了我们的结论一:

this 始终指向最后调用它的对象

二、一般函数和箭头函数的对象调用
// 例 2
var name = "window";

var person = {
    name: "inner",
    show1: function () {
        console.log(this.name);
    },
    show2: () => {
        console.log(this.name);
    }
}

person.show1();  // ?
person.show2();  // ?
复制代码

输出:

inner
window

person.show1() 输出 inner 没毛病,person.show2() 箭头函数为什么会输出 window 呢。MDN 中对 this 的定义是:

箭头函数不绑定 this, 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。

再看本文前面给的结论:

“箭头函数”的this,总是指向定义时所在的对象,而不是运行时所在的对象。

由于 JS 中只有全局作用域和函数作用域,箭头函数在定义时的上一层作用域是全局环境,全局环境中的 this 指向全局对象本身,即 window。

三、call
// 例 3
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()  // ?
person1.show1.call(person2)  // ?

person1.show2()  // ?
person1.show2.call(person2)  // ?

person1.show3()()  // ?
person1.show3().call(person2)  // ?
person1.show3.call(person2)()  // ?

person1.show4()()  // ?
person1.show4().call(person2)  // ?
person1.show4.call(person2)()  // ?
复制代码

输出:

person1
person2

window
window

window
person2
window

person1
person1
person2

上面 10 行打印,你对了几个呢?

首先:
person1.show1()person1.show1.call(person2) 输出结果应该没问题,call 的作用就是改变了调用的对象 为 person2

其次:
person1.show2()person1.show2.call(person2),由于调用的是箭头函数,和本文例 2 中是一样的,箭头函数定义时 this 指向的是上一层,也就是全局对象, 并且 箭头函数不绑定自己的 this, 所以通过 call()apply() 方法调用箭头函数时,只能传递参数,不能传递新的对象进行绑定。故打印的值都是 window。

进而:

function foo () {
    return function () {
      console.log(this.name)
    }
  }

foo()();
复制代码

博客前面的文章有讲过闭包,上面这段代码也是典型的闭包运用,可以看作:

function foo () {
    return function () {
      console.log(this.name)
    }
  }

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

所以,很明显,被返回的内部函数其实是在全局环境下被调用的。回到前面看我们的结论 1,this 始终指向最后调用函数的对象,这句话的关键词应该是什么?我觉得应该是 调用,什么时候调用,谁调用。

再回过头来看:
person1.show3()() 输出 window,因为内部函数在全局环境中被调用。

person1.show3().call(person2) 输出 person2, 因为内部函数被 person2 对象调用了。

person1.show3.call(person2)() 输出 window,也是因为内部函数在全局环境中被调用。

最后:
重点理解结论 2:

“箭头函数”的this,总是指向定义时所在的对象,而不是运行时所在的对象。

show4: function () {
    return () => console.log(this.name)
  }
复制代码

这段代码中,箭头函数是在 外层函数 show4 执行后才被定义的。为什么?可以翻看我前面关于作用域链,执行上下文,变量对象的文章,函数在进入执行阶段时,会先查找内部的变量和函数声明,将他们作为变量对象的属性,关联作用域链,并绑定 this 指向。

所以:
person1.show4()() 输出 person1,因为外部函数在执行时的 this 为 person1, 此时定义了内部函数,而内部函数为外部函数的 this。

person1.show4().call(person2) 输出 person1,箭头函数不会绑定 this, 所以 call 传入 this 指向无效。

person1.show4.call(person2)() 输出 person2,因为外部函数在执行时的 this 为 person2,此时定义了内部函数,而内部函数为外部函数的 this。

四、构造函数中的 this
// 例 4
var name = 'window'

function Person (name) {
  this.name = name;
  this.show1 = function () {
    console.log(this.name)
  }
  this.show2 = () => console.log(this.name)
  this.show3 = function () {
    return function () {
      console.log(this.name)
    }
  }
  this.show4 = function () {
    return () => console.log(this.name)
  }
}

var personA = new Person('personA')
var personB = new Person('personB')

personA.show1()  // 
personA.show1.call(personB)  // 

personA.show2()  // 
personA.show2.call(personB)  // 

personA.show3()()  // 
personA.show3().call(personB)  // 
personA.show3.call(personB)()  // 

personA.show4()()  // 
personA.show4().call(personB)  // 
personA.show4.call(personB)()  //
复制代码

输出:

personA
personB

personA
personA

window
personB
window

personA
personA
personB

例 4 和 例 3 大致一样,唯一的区别在于两点:

  1. 构造函数中 this 指向被创建的实例
  2. 构造函数,也是函数,所以存在作用域,所以里面的箭头函数,它们的 this 指向,来自于上一层,就不再是全局环境 window, 而是构造函数 的 this。
五、setTimeout 函数
// 例 5
function foo(){
  setTimeout(() =>{
    console.log("id:", this.id)
      setTimeout(() =>{
        console.log("id:", this.id)
      }, 100);
  }, 100);
}

foo.call({id: 111});  // 
复制代码

输出:

111
111

注意一点:
setTimeout 函数是在全局环境被 window 对象执行的,但是 foo 函数在执行时,setTimtout 委托的匿名箭头函数被定义,箭头函数的 this 来自于上层函数 foo 的调用对象, 所以打印结果才为 111;

六、setTimeout 函数 2
// 例 6
function foo1(){
  setTimeout(() =>{
    console.log("id:", this.id)
      setTimeout(function (){
        console.log("id:", this.id)
      }, 100);
  }, 100);
}


function foo2(){
  setTimeout(function() {
    console.log("id:", this.id)
      setTimeout(() => {
        console.log("id:", this.id)
      }, 100);
  }, 100);
}

foo1.call({ id: 111 });  // ?
foo2.call({ id: 222 });  // ?
复制代码

输出:

111
undefined

undefined
undefined

例 5 中已经提到,setTimeout函数被 window 对象调用,如果 是普通函数,内部的 this 自然指向了全局对象下的 id, 所以为 undefined,如果是箭头函数,this 指向的就是外部函数的 this。

七、嵌套箭头函数
// 例 7
function foo() {
  return () => {
    return () => {
      return () => {
        console.log("id:", this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()();  // 
var t2 = f().call({id: 3})();  // 
var t3 = f()().call({id: 4});  // 
复制代码

输出:

1
1
1

这段代码是为了巩固我们的结论2:

“箭头函数”的this,总是指向定义时所在的对象,而不是运行时所在的对象。

  1. foo.call({}) 在执行时,内部的第一层箭头函数才被定义
  2. 箭头函数无法绑定 this, 所以 call 函数指定 this 无效
  3. 箭头函数的 this 来自于上一层作用域(非箭头函数作用域)的 this
总结

有本书中有提到,当理解 JavaScript 中的 this 之后,JavaScript 才算入门,我深以为然。

原因是,要彻底理解 this, 应该是建立在已经大致理解了 JS 中的执行上下文,作用域、作用域链,闭包,变量对象,函数执行过程的基础上。

有兴趣深入了解上下文,作用域,闭包相关内容的同学可以翻看我之前的文章。

参考链接:

1:this、apply、call、bind
2: 从这两套题,重新认识JS的this、作用域、闭包、对象
3: 关于箭头函数this的理解几乎完全是错误的
4: 深入JS系列

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

掘金专栏 JavaScript 系列文章
  1. JavaScript之变量及作用域
  2. JavaScript之声明提升
  3. JavaScript之执行上下文
  4. JavaScript之变量对象
  5. JavaScript原型与原型链
  6. JavaScript之作用域链
  7. JavaScript之闭包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值传递
  11. JavaScript之例题中彻底理解this
  12. JavaScript专题之模拟实现call和apply
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值