当前位置: 首页 > news >正文

理解 JavaScript 中的 this

前言

理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码。

本文是这系列的第三篇,往期文章:

  1. 理解 JavaScript 中的作用域
  2. 理解 JavaScript 中的闭包

什么是 this

消除误解

在解释什么是this之前,需要先纠正大部分人对this的误解,常见的误解有:

  1. 指向函数自身。
  2. 指向它所在的作用域。

关于为何会误解的原因这里不多讲,这里只给出结论,有兴趣可以自行查询资料。

this 在任何情况下都不指向函数的词法作用域。你不能使用 this 来引用一个词法作用域内部的东西。

this 到底是什么

排除了一些错误理解之后,我们来看看 this到底是一种什么样的机制。

this是在运行时(runtime)进行绑定的,而不是在编写时绑定的,它的上下文(对象)取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。( PS:所以this并不等价于执行上下文)

this 全面解析

前面 我们排除了一些对于 this的错误理解并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置。

调用位置

通常来说,寻找调用位置就是寻找“函数被调用的位置“,其中最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

下面我们来看看到底什么是调用栈和调用位置:

function foo(){
    // 当前调用栈是:foo // 因此,当前调用位置是全局作用域 console.log("foo"); bar(); // <-- bar 的调用位置 } function bar(){ // 当前调用栈是 foo -> bar console.log("bar"); } foo(); // <-- foo 的调用位置 

你可以把调用栈想象成一个函数调用链, 就像我们在前面代码段的注释中所写的一样。但是这种方法非常麻烦并且容易出错。 另一个查看调用栈的方法是使用浏览器的调试工具。 绝大多数现代桌面浏览器都内置了开发者工具,其中包含 JavaScript 调试器。

绑定规则

在找到调用位置后,则需要判定代码属于下面四种绑定规则中的哪一种,然后才能对this进行绑定。 注意: this绑定的是上下文对象,并不是函数自身也不是函数的词法作用域

默认绑定

这是最常见的函数调用类型:独立函数调用

对函数直接使用而不带任何修饰的函数引用进行调用,简单点一个函数直接是func()这样调用,不同于通过对象属性调用例如obj.func(),也没有通过 new 关键字new Function(),也没有通过applycallbind强制改变this指向。

当被用作独立函数调用时(不论这个函数在哪被调用,不管全局还是其他函数内),this默认指向到Window。(注意:在严格模式下this不再默认指向全局,而是undefined)。

示例代码:

function foo(){
    console.log(this.name); } var name = "window"; foo(); // window 

隐式绑定

函数被某个对象拥有或者包含,也就是函数被作为对象的属性所引用,例如obj.func(),此时this会绑定到该对象上,这就是隐式绑定。

示例代码:

var obj = {
    name : "obj",
    foo : function(){ console.log(this.name); } } obj.foo(); // obj 

隐式丢失

大部分的this绑定问题就是被“隐式绑定”的函数会丢失绑定对象,也就是说它会应用“默认绑定”,从而把this绑定到Windowundefined上,这取决于是否是严格模式。

最常见的情况就是把对象方法作为回调函数进行传递时:

var obj = {
    name : "obj",
    foo : function(){ console.log(this.name); } } var name = "window"; setTimeout(obj.foo,1000); // 一秒后输出 window 

显式绑定

我们可以通过applycallbind方法来显示地修改this的指向。

关于这三个方法的定义(它们第一个参数都是接受this的绑定对象):

  1. apply:调用函数,第二个参数传入一个参数数组。
  2. call:调用函数,其余参数正常传递。
  3. bind:返回一个已经绑定this的函数,其余参数正常传递。

比如我们可以使用bind方法解决上一节“隐式丢失”中的例子:

var obj = {
    name : "obj",
    foo : function(){ console.log(this.name); } } var name = "window"; setTimeout(obj.foo.bind(obj),1000); // 一秒后输出 obj 

new 绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

示例代码:

function foo(a) { 
  this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2 

优先级

直接上结论:

new 绑定=显示绑定>隐式绑定>默认绑定

判断 this: 现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

  1. 使用 new 绑定,this绑定的是新创建的对象。

    var bar = new foo();
    
  2. 通过call之类的显式绑定,this绑定的是指定的对象。

    var bar = foo.call(obj2);
    
  3. 在某个上下文对象中调用(隐式绑定),this 绑定的是那个上下文对象。

    var bar = obj1.foo();
    
  4. 如果都不是的话,使用默认绑定。this绑定到Windowundefined上,这取决于是否是严格模式。

    var bar = foo();
    

    对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。

this 词法

ES6 中介绍了一种无法使用上面四条规则的特殊函数类型:箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。(而传统的 this 与函数作用域没有任何关系,它只与调用位置的上下文对象有关)。

重要:

  • 箭头函数最常用于回调函数中,例如事件处理器或者定时器.
  • 箭头函数可以像bind 一样确保函数的this被绑定到指定对象
  • 箭头函数用更常见的词法作用域取代了传统的this机制。

示例代码:

var obj = {
    name : "obj",
    foo : function(){ setTimeout(()=>{ console.log(console.log(this.name)); // obj },1000); } } obj.foo(); 

这在 ES6 之前是这样解决的:

var obj = {
    name : "obj",
    foo : function(){ var self = this; setTimeout(function(){ console.log(console.log(self.name)); // obj },1000); } } obj.foo(); 

总结

总之如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。

ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this绑定(无论 this绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

(转载于:https://www.v2ex.com/t/529532#reply1)

转载于:https://www.cnblogs.com/MisterZZL/p/10416069.html

相关文章:

  • 块级、内联、内联块级
  • Spring Boot 运作原理
  • 虚拟机网络不通故障解决
  • 原型模式深入探讨-【设计模式4】
  • 撕掉普通程序员的标签,这才是真正的大数据工程师!
  • Spring基于XML方式配置事务
  • 程序员最讨厌的9句话,你可有补充?
  • 一些关于Rust在2019年的思考
  • 通信类
  • 力扣(LeetCode)56
  • mybatis-plus 3.1.0 发布,划重点一大波升级
  • JS里的Array
  • IDEA 插件开发入门教程
  • python人工智能机器人工具书籍: Python Robotics Projects - 2018
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • 002-读书笔记-JavaScript高级程序设计 在HTML中使用JavaScript
  • javascript面向对象之创建对象
  • Java方法详解
  • Object.assign方法不能实现深复制
  • PHP 7 修改了什么呢 -- 2
  • session共享问题解决方案
  • Spring Cloud(3) - 服务治理: Spring Cloud Eureka
  • springboot_database项目介绍
  • XForms - 更强大的Form
  • Yeoman_Bower_Grunt
  • 多线程事务回滚
  • 分布式任务队列Celery
  • 分布式事物理论与实践
  • 跨域
  • 面试遇到的一些题
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 为物联网而生:高性能时间序列数据库HiTSDB商业化首发!
  • 学习笔记DL002:AI、机器学习、表示学习、深度学习,第一次大衰退
  • 原创:新手布局福音!微信小程序使用flex的一些基础样式属性(一)
  • 终端用户监控:真实用户监控还是模拟监控?
  • ​​​​​​​Installing ROS on the Raspberry Pi
  • # 飞书APP集成平台-数字化落地
  • #QT项目实战(天气预报)
  • (03)光刻——半导体电路的绘制
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (附源码)计算机毕业设计大学生兼职系统
  • (六)软件测试分工
  • (十二)springboot实战——SSE服务推送事件案例实现
  • (四)JPA - JQPL 实现增删改查
  • (原創) 如何刪除Windows Live Writer留在本機的文章? (Web) (Windows Live Writer)
  • .net core webapi Startup 注入ConfigurePrimaryHttpMessageHandler
  • .NET Micro Framework初体验
  • .Net 中Partitioner static与dynamic的性能对比
  • .NET 中创建支持集合初始化器的类型
  • .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)...
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • .netcore 获取appsettings
  • .vue文件怎么使用_vue调试工具vue-devtools的安装