首页 > 编程语言 >JavaScript 的 this 指向问题深度解析

JavaScript 的 this 指向问题深度解析

时间:2024-09-18 09:50:19浏览次数:3  
标签:function 调用 obj 函数 指向 bind JavaScript test 解析

JavaScript 中的 this 指向问题有很多博客在解释,仍然有很多人问。上周我们的开发团队连续两个人遇到相关问题,所以我不得不将关于前端构建技术的交流会延长了半个时候讨论 this 的问题。

与我们常见的很多语言不同,JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的。换句话说,函数的调用方式决定了 this 指向

JavaScript 中,普通的函数调用方式有三种:直接调用、方法调用和 new 调用。除此之外,还有一些特殊的调用方式,比如通过 bind() 将函数绑定到对象之后再进行调用、通过 call()apply() 进行调用等。而 es6 引入了箭头函数之后,箭头函数调用时,其 this 指向又有所不同。下面就来分析这些情况下的 this 指向。

直接调用

直接调用,就是通过 函数名(...) 这种方式调用。这时候,函数内部的 this 指向全局对象,在浏览器中全局对象是 window,在 NodeJs 中全局对象是 global

来看一个例子:


  1. // 简单兼容浏览器和 NodeJs 的全局对象

  2. const _global = typeof window === "undefined" ? global : window;

  3. function test() {

  4.    console.log(this === _global);    // true

  5. }

  6. test();    // 直接调用

这里需要注意的一点是,直接调用并不是指在全局作用域下进行调用,在任何作用域下,直接通过 函数名(...) 来对函数进行调用的方式,都称为直接调用。比如下面这个例子也是直接调用


  1. (function(_global) {

  2.    // 通过 IIFE 限定作用域

  3.    function test() {

  4.        console.log(this === _global);  // true

  5.    }

  6.    test();     // 非全局作用域下的直接调用

  7. })(typeof window === "undefined" ? global : window);

bind() 对直接调用的影响

还有一点需要注意的是 bind() 的影响。 Function.prototype.bind() 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其 this 始终指向绑定的对象。还是来看例子:


  1. const obj = {};

  2. function test() {

  3.    console.log(this === obj);

  4. }

  5. const testObj = test.bind(obj);

  6. test();     // false

  7. testObj();  // true

那么 bind() 干了啥?不妨模拟一个 bind() 来了解它是如何做到对 this 产生影响的。


  1. const obj = {};

  2. function test() {

  3.    console.log(this === obj);

  4. }

  5. // 自定义的函数,模拟 bind() 对 this 的影响

  6. function myBind(func, target) {

  7.    return function() {

  8.        return func.apply(target, arguments);

  9.    };

  10. }

  11. const testObj = myBind(test, obj);

  12. test();     // false

  13. testObj();  // true

从上面的示例可以看到,首先,通过闭包,保持了 target,即绑定的对象;然后在调用函数的时候,对原函数使用了 apply 方法来指定函数的 this。当然原生的 bind() 实现可能会不同,而且更高效。但这个示例说明了 bind() 的可行性。

call 和 apply 对 this 的影响

上面的示例中用到了 Function.prototype.apply(),与之类似的还有 Function.prototype.call()。这两方法的用法请大家自己通过链接去看文档。不过,它们的第一个参数都是指定函数运行时其中的 this 指向。

不过使用 applycall 的时候仍然需要注意,如果目录函数本身是一个绑定了 this 对象的函数,那 applycall 不会像预期那样执行,比如


  1. const obj = {};

  2. function test() {

  3.    console.log(this === obj);

  4. }

  5. // 绑定到一个新对象,而不是 obj

  6. const testObj = test.bind({});

  7. test.apply(obj);    // true

  8. // 期望 this 是 obj,即输出 true

  9. // 但是因为 testObj 绑定了不是 obj 的对象,所以会输出 false

  10. testObj.apply(obj); // false

由此可见, bind() 对函数的影响是深远的,慎用!

方法调用

方法调用是指通过对象来调用其方法函数,它是 对象.方法函数(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是,同样需要注意 bind() 的影响。


  1. const obj = {

  2.    // 第一种方式,定义对象的时候定义其方法

  3.    test() {

  4.        console.log(this === obj);

  5.    }

  6. };

  7. // 第二种方式,对象定义好之后为其附加一个方法(函数表达式)

  8. obj.test2 = function() {

  9.    console.log(this === obj);

  10. };

  11. // 第三种方式和第二种方式原理相同

  12. // 是对象定义好之后为其附加一个方法(函数定义)

  13. function t() {

  14.    console.log(this === obj);

  15. }

  16. obj.test3 = t;

  17. // 这也是为对象附加一个方法函数

  18. // 但是这个函数绑定了一个不是 obj 的其它对象

  19. obj.test4 = (function() {

  20.    console.log(this === obj);

  21. }).bind({});

  22. obj.test();     // true

  23. obj.test2();    // true

  24. obj.test3();    // true

  25. // 受 bind() 影响,test4 中的 this 指向不是 obj

  26. obj.test4();    // false

这里需要注意的是,后三种方式都是预定定义函数,再将其附加给 obj 对象作为其方法。再次强调,函数内部的 this 指向与定义无关,受调用方式的影响。

方法中 this 指向全局对象的情况

注意这里说的是方法中而不是方法调用中。方法中的 this 指向全局对象,如果不是因为 bind(),那就一定是因为不是用的方法调用方式,比如


  1. const obj = {

  2.    test() {

  3.        console.log(this === obj);

  4.    }

  5. };

  6. const t = obj.test;

  7. t();    // false

t 就是 objtest 方法,但是 t() 调用时,其中的 this 指向了全局。

之所以要特别提出这种情况,主要是因为常常将一个对象方法作为回调传递给某个函数之后,却发现运行结果与预期不符——因为忽略了调用方式对 this 的影响。比如下面的例子是在页面中对某些事情进行封装之后特别容易遇到的问题:


  1. class Handlers {

  2.    // 这里 $button 假设是一个指向某个按钮的 jQuery 对象

  3.    constructor(data, $button) {

  4.        this.data = data;

  5.        $button.on("click", this.onButtonClick);

  6.    }

  7.    onButtonClick(e) {

  8.        console.log(this.data);

  9.    }

  10. }

  11. const handlers = new Handlers("string data", $("#someButton"));

  12. // 对 #someButton 进行点击操作之后

  13. // 输出 undefined

  14. // 但预期是输出 string data

this.onButtonClick 作为一个参数传入 on() 之后,事件触发时,理论上是对这个函数进行的直接调用,而不是方法调用,所以其中的 this 会指向全局对象 —— 但实际上由于调用事件处理函数的时候, this 指向会绑定到触发事件的 DOM 元素上,所以这里的 this 是指向触发事件的的 DOM 元素(注意: this 并非 jQuery 对象),即 $button.get(0)(注意代码前注释中的假设)。

要解决这个问题有很多种方法:


  1. // 这是在 es5 中的解决办法之一

  2. var _this = this;

  3. $button.on("click", function() {

  4.    _this.onButtonClick();

  5. });

  6. // 也可以通过 bind() 来解决

  7. $button.on("click", this.onButtonClick.bind(this));

  8. // es6 中可以通过箭头函数来处理,在 jQuery 中慎用

  9. $button.on("click", e => this.onButtonClick(e));

不过请注意,将箭头函数用作 jQuery 的回调时造成要小心函数内对 this 的使用。jQuery 大多数回调函数(非箭头函数)中的 this 都是表示调用目标,所以可以写 $(this).text() 这样的语句,但 jQuery 无法改变箭头函数的 this 指向,同样的语句语义完全不同。

new 调用

在 es6 之前,每一个函数都可以当作是构造函数,通过 new 调用来产生新的对象(函数内无特定返回值的情况下)。而 es6 改变了这种状态,虽然 class 定义的类用 typeof 运算符得到的仍然是 "function",但它不能像普通函数一样直接调用;同时, class 中定义的方法函数,也不能当作构造函数用 new 来调用。

而在 es5 中,用 new 调用一个构造函数,会创建一个新对象,而其中的 this 就指向这个新对象。这没有什么悬念,因为 new 本身就是设计来创建新对象的。


  1. var data = "Hi";    // 全局变量

  2. function AClass(data) {

  3.    this.data = data;

  4. }

  5. var a = new AClass("Hello World");

  6. console.log(a.data);    // Hello World

  7. console.log(data);      // Hi

  8. var b = new AClass("Hello World");

  9. console.log(a === b);   // false

箭头函数中的 this

先来看看 MDN 上对箭头函数的说明

An arrow function expression has a shorter syntax than a function expression and does not bind its own this, arguments, super, or new.target. Arrow functions are always anonymous. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

这里已经清楚了说明了,箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含它的那个函数或函数表达式中的 this。比如


  1. const obj = {

  2.    test() {

  3.        const arrow = () => {

  4.            // 这里的 this 是 test() 中的 this,

  5.            // 由 test() 的调用方式决定

  6.            console.log(this === obj);

  7.        };

  8.        arrow();

  9.    },

  10.    getArrow() {

  11.        return () => {

  12.            // 这里的 this 是 getArrow() 中的 this,

  13.            // 由 getArrow() 的调用方式决定

  14.            console.log(this === obj);

  15.        };

  16.    }

  17. };

  18. obj.test();     // true

  19. const arrow = obj.getArrow();

  20. arrow();        // true

示例中的两个 this 都是由箭头函数的直接外层函数(方法)决定的,而方法函数中的 this 是由其调用方式决定的。上例的调用方式都是方法调用,所以 this 都指向方法调用的对象,即 obj

箭头函数让大家在使用闭包的时候不需要太纠结 this,不需要通过像 _this 这样的局部变量来临时引用 this 给闭包函数使用。来看一段 Babel 对箭头函数的转译可能能加深理解:


  1. // ES6

  2. const obj = {

  3.    getArrow() {

  4.        return () => {

  5.            console.log(this === obj);

  6.        };

  7.    }

  8. }    


  1. // ES5,由 Babel 转译

  2. var obj = {

  3.    getArrow: function getArrow() {

  4.        var _this = this;

  5.        return function () {

  6.            console.log(_this === obj);

  7.        };

  8.    }

  9. };

另外需要注意的是,箭头函数不能用 new 调用,不能 bind() 到某个对象(虽然 bind() 方法调用没问题,但是不会产生预期效果)。不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含它的最近的一层函数或函数表达式)绑定的 this

标签:function,调用,obj,函数,指向,bind,JavaScript,test,解析
From: https://blog.csdn.net/qq_35430208/article/details/142177157

相关文章

  • 14个你可能不知道的JavaScript调试技巧
    熟悉工具可以让工具在工作中发挥出更大的作用。尽管江湖传言JavaScript很难调试,但如果你掌握了几个技巧,就能用很少的时间来解决错误和bug.文中已经列出了14个你可能不知道的调试技巧,但是可能需要你牢记在心,以便在下次需要调试JavaScript代码时使用!一起来看大多数技巧都适......
  • mongodb 中rs.stauts()命令参数解析
    转载请注明出处:rs.status()命令用于获取MongoDB副本集的状态信息。它提供了关于副本集中各个节点的详细信息,包括节点的健康状况、角色、选举状态等。以下是查看一个mongo集群状态返回的参数:rs0:PRIMARY>rs.status(){"set":"rs0","date":ISODa......
  • JavaScript 必备知识点以及网页功能实现案例
    JavaScript是一种广泛应用于网页开发的编程语言,以下是一些必备的重要知识点:一、基本语法变量声明使用 let、const 和 var 声明变量。let 和 const 是ES6引入的新方式,具有块级作用域,可避免变量提升带来的问题。const 声明的变量是常量,不能被重新赋值。例如:......
  • JavaScript -- 数组数据类型
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=d......
  • JavaScript -- 数组的基本操作
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=d......
  • JavaScript:对组织值进行排序
    要对组织值进行排序,你可以使用JavaScript中的数组排序方法 sort()。下面是一些示例代码,展示如何对不同类型的组织值进行排序:示例1:对数字数组进行排序constnumbers=[5,2,9,1,5,6];//使用sort()方法进行升序排序numbers.sort((a,b)=>a-b);console.log(n......
  • 前端JavaScript面试重难点: 闭包+内存泄漏+垃圾回收机制
    前置知识!!!闭包是Javascript语言的一个重难点,也是它的特色,很多高级应用都要依靠闭包来实现。在各种专业文献上学习"闭包"的时候,就一个感觉–“抽象”!特别是学习内存泄漏的时候,没想明白为什么使用闭包的时候不及时清除函数中的元素会导致内存泄漏,直到我的......
  • 深入解析 Apache Ranger
    一.概述1.什么是ApacheRanger?ApacheRanger是一个为大数据平台提供集中化安全管理的开源框架,专门用于确保Hadoop生态系统中的数据安全。Ranger通过提供细粒度的访问控制和监控,帮助组织实现对数据的全面安全管理,确保数据访问的透明性、可控性和合规性。2.背景与发......
  • Vue2 和 Vue3 区别 — 源码深度解析
    Vue2和Vue3区别—源码深度解析Vue.js作为前端领域中的一款热门框架,其每一次版本迭代都牵动着无数开发者的心。今天,我们将深度解析Vue2与Vue3之间的最主要区别,通过源码对比,带你领略这两个版本背后的奥秘。文章目录Vue2和Vue3区别—源......
  • Zookeeper 3.8.4 安装和参数解析
    安装zookeeper之前必须先安装JDK,有关Linux环境JDK可以参考我以前写的博文1、关于Linux服务器配置java环境遇到的问题2、Linux环境安装openJDK3、Centos7.3云服务器上安装Nginx、MySQL、JDK、Tomcat环境文章目录1.zookeeper安装2.参数解析1.zookeeper安......