JavaScript中this的绑定规则

前言

我们知道浏览器运行环境下在全局作用域下的this是指向window的,但是开发中却很少在全局作用域下去使用this,通常都是在函数中进行使用,而函数使用不同方式进行调用,其this的指向是不一样的。JavaScript中函数在被调用时,会先创建一个函数执行上下文(FEC),而这个上下文中记录着函数的调用栈、活动对象(AO)以及this等等。那么this到底指向什么呢?下面一起来看看this的四个绑定规则。

1.this四个绑定规则

1.1.默认绑定

在进行独立函数调用时,this就会使用默认绑定,而默认绑定的值就是全局的window。独立函数调用可以简单认为函数没有被绑定到某个对象上进行调用。

满足默认绑定的函数调用情况:

  • 全局定义的函数,直接在全局进行调用;

    function foo() {
    console.log(this)
    } foo() // window
  • 多个函数进行嵌套调用,虽然以下函数嵌套了一层又一层,原则上还是独立函数调用;

    function foo1() {
    console.log(this) // window
    }
    function foo2() {
    console.log(this) // window
    foo1()
    }
    function foo3() {
    console.log(this) // window
    foo2()
    } foo3()
  • 函数的返回值为一个函数,通过调用该函数将返回值赋值给某个变量,最后调用这个变量(最后fn的调用也是独立函数调用,与函数的定义位置无关);

    function foo() {
    return function() {
    console.log(this)
    }
    } const fn = foo()
    fn() // window
  • 将对象中的方法赋值给别人,别人再进行调用,虽然是对象中的方法再次赋值,最后被赋值的对象都是进行独立函数调用的;

    • obj中的bar方法作为参数传递到foo函数中,然后进行调用(最后被传递的参数进行了独立调用)

      function foo(func) {
      func()
      } const obj = {
      bar: function() {
      console.log(this)
      }
      } foo(obj.bar) // window
    • obj中的bar方法赋值给fn变量,然后进行调用(最后被赋值的fn进行了独立调用)

      const obj = {
      bar: function() {
      console.log(this)
      }
      } const fn = obj.bar
      fn() // window

1.2.隐式绑定

隐式绑定是开发中比较常见的,通过某个对象对函数进行调用,也就是说函数的调用是通过对象发起的,常见为对象中方法的调用(这个对象会被js引擎绑定到函数中的this里面)。

满足隐式绑定的函数调用情况:

  • 直接调用对象中的方法;

    const obj = {
    foo: function() {
    console.log(this)
    }
    } obj.foo() // obj对象
  • 将函数定义在全局,然后再赋值给对象属性,再通过对象调用;

    function bar() {
    console.log(this)
    } const obj = {
    foo: bar
    } obj.foo() // obj对象
  • 调用对象中属性值为对象中的方法(最后foo函数的调用发起者为obj2);

    const obj1 = {
    name: 'obj1',
    obj2: {
    name: 'obj2',
    foo: function() {
    console.log(this)
    }
    }
    } obj1.obj2.foo() // obj2对象

1.3.显示绑定

如果我们不希望使用隐式绑定的对象,而想在调用函数时给this绑定上自己想绑定的东西,那么这样的行为就称为显示绑定,显示绑定主要借助于三个方法,分别为call、apply和bind。

1.3.1.简介
  • call 方法:使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。
  • apply方法:调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
  • bind方法:创建一个新的函数,在 bind 被调用时,这个新函数的this被指定为bind的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
1.3.2.call、apply绑定this

JS中所有的函数都有call和apply方法属性(主要是其原型链上实现了这两个方法),可以说这两个函数就是给this准备的。简单介绍一下这两个函数的区别:在进行函数调用时,都可以使用call和apply进行调用,其传入的第一个参数都是一样的,也就是我们希望给this绑定的值,后面的参数apply对应为包含多个参数的数组,call对应为参数列表

  • foo函数不仅可以直接进行调用,还可以通过call和apply进行调用,并且在执行foo函数时,可以明确绑定this;

    function foo() {
    console.log(this)
    } const obj = {
    name: 'curry',
    age: 30
    } foo() // window
    foo.call(obj) // obj对象
    foo.apply(obj) // obj对象
  • 当被调用函数有参数需要传入时,就可以在call和apply后面传入对应的参数;

    function sum(x, y) {
    console.log(this, x + y)
    } const obj = {
    name: 'curry',
    age: 30
    } sum(10, 20) // window 30
    sum.call(obj, 10, 20) // obj对象 30
    sum.apply(obj, [10, 20]) // obj对象 30
  • 上面的示例都是给this绑定一个对象,其实还可以给this绑定不同数据类型;

    function foo() {
    console.log(this)
    } // 1.绑定字符串
    foo.call('aaa')
    foo.apply('bbb') // 2.绑定字符串
    foo.call(111)
    foo.apply(222) // 3.绑定数组
    foo.call([1, 2, 3])
    foo.apply(['a', 'b', 'c'])

1.3.3.bind绑定this

call和apply可以帮助我们调用函数明确this的绑定对象,而bind与这两种的用法不太一样。如果希望一个函数的this可以一直绑定到某个对象上,那么bind就可以派出用场了。

  • 上面提到了使用bind对this进行绑定时,会给我们返回一个新函数,而这个新函数的this就绑定为我们指定的对象;

    function foo() {
    console.log(this)
    } const obj = {
    name: 'curry',
    age: 30
    } const newFoo = foo.bind(obj)
    newFoo() // obj对象
  • 使用bind绑定的this是不会被我们上面提到的绑定规则所改变的,比如,使用bind绑定的对象为obj1,不管是使用call、apply还是对象方法调用(隐式绑定),都不会改变this的指向了;

    function foo() {
    console.log(this)
    } const obj1 = {
    name: 'curry',
    age: 30
    }
    const obj2 = {
    name: 'kobe',
    age: 24
    } const newFoo = foo.bind(obj1)
    newFoo() // obj1对象 newFoo.call(obj2) // obj1对象
    newFoo.apply(obj2) // obj1对象 const obj3 = {
    bar: newFoo
    }
    obj3.bar() // obj1对象

1.4.new绑定

JavaScript中的函数不仅可以通过上面的方法进行调用,还可以使用new关键字来调用函数,当使用new来调用的函数,一般称为构造函数(ES6中的类),以这样的方式来调用的函数this所绑定的值,就叫做new绑定。

构造函数一般用于创建对象,通过new调用构造函数可以返回一个实例对象,而this绑定就是这个实例对象。那么使用new调用时,函数内部会进行哪些操作呢?如下:

  • 1.函数内部创建一个全新的对象(空对象)

  • 2.这个新对象内部的[[prototype]]属性(也就是对象原型)会被赋值为该构造函数的prototype属性

  • 3.构造函数内部的this,会指向这个创建出来的新对象

  • 4.执行构造函数中的代码(函数体代码);

  • 5.如果该构造函数没有返回其它对象,则返回创建出来的新对象;

    function Student(sno, name, age) {
    this.sno = sno
    this.name = name
    this.age = age console.log('this: ', this)
    } const stu1 = new Student(1, 'curry', 30) // this: {sno: 1, name: "curry", age: 30}
    console.log(stu1) // Student {sno: 1, name: "curry", age: 30} const stu2 = new Student(2, 'kobe', 24) // this: {sno: 2, name: "kobe", age: 24}
    console.log(stu2) // Student {sno: 2, name: "kobe", age: 24} console.log(stu1.__proto__ === Student.prototype) // true
    console.log(stu2.__proto__ === Student.prototype) // true

2.JS内置函数的this绑定

  • setTimeout:第一个参数在内部是进行独立函数调用的;

    setTimeout(function() {
    console.log(this) // window
    }, 1000) // ------相当于------
    function mySetTimeout(callback, delay) {
    callback() // 独立函数调用
    }
  • DOM事件监听:事件触发后的回调函数中的this是指向当前DOM对象的;

    const boxDiv = document.querySelector('.box')
    boxDiv.onclick = function() {
    console.log(this) // boxDiv(DOM对象)
    }
    boxDiv.addEventListener('click', function() {
    console.log(this) // boxDiv(DOM对象)
    })
  • 数组内置方法:数组中的forEach、map、filter、find等这些方法都可以传入第二个参数,这个参数即为内部函数的this指向。

    const arr = [1, 2, 3, 4, 5]
    
    const obj = {
    name: 'curry',
    age: 30
    } arr.forEach(function() {
    console.log(this) // obj对象
    }, obj) arr.map(function() {
    console.log(this) // obj对象
    }, obj) arr.filter(function() {
    console.log(this) // obj对象
    }, obj)

3.this绑定规则优先级

上面提到了this的四种绑定规则,如果在函数调用时,使用到了多种绑定规则,最终函数的this指向什么呢?那么这里就涉及到this绑定的优先级,优先级高的规则决定this的最终绑定。

this四种绑定规则优先级如下:

  • 默认绑定优先级最低:函数调用存在其它规则时,就会遵循其它规则来绑定其this;

  • 显示绑定优先级高于隐式绑定

    const obj1 = {
    name: 'obj1',
    foo: function() {
    console.log(this)
    }
    }
    const obj2 = {
    name: 'obj2'
    } obj1.foo.call(obj2) // obj2对象
    obj1.foo.apply(obj2) // obj2对象 const newFoo = obj1.foo.bind(obj2)
    newFoo() // obj2对象
  • 显示绑定中bind高于call和apply

    function foo() {
    console.log(this)
    } const obj1 = {
    name: 'curry',
    age: 30
    }
    const obj2 = {
    name: 'kobe',
    age: 24
    } const newFoo = foo.bind(obj1)
    newFoo() // obj1对象
    newFoo.call(obj2) // obj1对象
    newFoo.apply(obj2) // obj1对象
  • new绑定优先级高于隐式绑定和显示绑定

    // 1.new绑定高于隐式绑定
    const obj1 = {
    foo: function() {
    console.log(this)
    }
    } const f = new obj1.foo() // foo函数对象 // 2.new绑定高于显示绑定
    function foo(name) {
    console.log(this)
    } const obj2 = {
    name: 'obj2'
    } const newFoo = foo.bind(obj2)
    const nf = new newFoo(123) // foo函数对象

总结

  • new绑定 > 显示绑定(apply/call/bind) > 隐式绑定 > 默认绑定
  • 注意new关键字不能和apply、call一起使用,所以不太好进行比较,默认为new绑定是优先级最高的;

4.特殊情况下的this绑定

在特殊情况下,this的绑定不一定满足上面的绑定规则,主要有以下特殊情况:

  • 显示绑定的忽略:在显示绑定中,如果给call、apply和bind第一个参数传入null或者undefined,那么这样的显示绑定会被忽略,最终使用默认绑定,也就是全局的window;

    function foo() {
    console.log(this)
    } foo.call(null) // window
    foo.call(undefined) // window foo.apply(null) // window
    foo.apply(undefined) // window const newFoo1 = foo.bind(null)
    const newFoo2 = foo.bind(undefined)
    newFoo1() // window
    newFoo2() // window
  • 间接函数的引用:创建一个函数的间接引用,该情况也使用默认绑定。如下代码中给obj2创建一个bar属性并赋值为obj1中foo函数时直接进行调用;

    const obj1 = {
    name: 'obj1',
    foo: function() {
    console.log(this)
    }
    }
    const obj2 = {
    name: 'obj2'
    } // 被认为是独立函数调用
    ;(obj2.bar = obj1.foo)() // window
  • ES6中的箭头函数:箭头函数不会使用上面的四种绑定规则,也就是说不绑定this,箭头函数的this是根据它外层作用域中的this绑定来决定的;

    • 数组内置方法中回调使用箭头函数:

      const names = ['curry', 'kobe', 'klay']
      const obj = {
      name: 'obj'
      } names.map(() => {
      console.log(this) // window
      }, obj)
    • 多层对象中的方法属性使用箭头函数:

      const obj1 = {
      name: 'obj1',
      obj2: {
      name: 'obj2',
      foo: () => {
      console.log(this)
      }
      }
      } obj1.obj2.foo() // window
    • 函数的返回值为箭头函数:

      function foo() {
      return () => {
      console.log(this)
      }
      } const obj = {
      name: 'curry',
      age: 30
      } const bar = foo.call()
      bar() // obj对象

5.node环境下全局this的指向

以上提到的内容都是在浏览器环境中进行测试的,在浏览器环境下的this是指向全局window的,那么在node环境中全局this指向什么呢?

  • 在node环境下打印一下全局this:

  • 为什么全局this打印为一个空对象?

    • 当我们js文件被node执行时,该js文件会被视为一个模块;

    • node加载编译这个模块,将模块中的所有代码放入到一个函数中;

    • 然后会执行这个函数,在执行这个函数时会通过apply绑定一个this,而绑定的this就为{}

  • 那为什么node中还是可以使用想window中的全局方法呢?像setTimeout、setInterval等。

    • 因为node环境中也是存在全局对象的,通过打印globalThis就可以进行查看;

JavaScript中this的绑定规则的更多相关文章

  1. JavaScript中正则表达式判断匹配规则以及常用的方法

    JavaScript中正则表达式判断匹配规则以及常用的方法: 字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在. 正则表达式是一种用来匹配字符串的强有力的武器.它的设计思想 ...

  2. js中this的绑定规则及优先级

    一.   this绑定规则 函数调用位置决定了this的绑定对象,必须找到正确的调用位置判断需要应用下面四条规则中的哪一条. 1.1 默认绑定 看下面代码: function foo() { cons ...

  3. JavaScript中的this指向规则

    首先,JavaScript的this指向问题并非传说中的那么难,不难的是机制并不复杂,而被认为不好理解的是逻辑关系和容易混淆的执行上下文.这篇博客也就会基于这两个不好理解的角度来展开,如要要严格的来对 ...

  4. 理解Javascript中的事件绑定与事件委托

    最近在深入实践js中,遇到了一些问题,比如我需要为动态创建的DOM元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下js中的事件绑定与事件委托. 事件绑定   ...

  5. JavaScript中的this绑定丢失及解决方法

    经常犯的错误:混淆了this绑定规则. 代码如下: var obj = { id: 'vexekefo', cool() { console.log(this.id); } }; var id = ' ...

  6. javascript中的this绑定问题

    this的绑定规则 1 默认绑定: function foo(){ console.log(this.a); } var a = 2 ; foo(); 调用 foo() 的时候其实相当于 window ...

  7. 【RegExp】JavaScript中正则表达式判断匹配规则以及常用方法

    字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在. 正则表达式是一种用来匹配字符串的强有力的武器.它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符 ...

  8. javascript中的双向绑定

    阅读目录 一:发布订阅模式实现数据双向绑定 二:使用Object.defineProperty 来实现简单的双向绑定. 前言: 双向数据绑定的含义:可以将对象的属性绑定到UI,具体的说,我们有一个对象 ...

  9. JavaScript 中 onload 事件绑定多个方法的优化建议

    页面加载完毕时会触发 onload 事件.基于内容(HTML)要与行为(JavaScript)分离的编码思想,我们需要将一些对页面的初始化操作写在方法内,并通过window.onload = func ...

随机推荐

  1. Python原生数据结构增强模块collections

    collections简介 python提供了4种基本的数据结构:list.tuple.dict.set.基本数据结构完全可以hold住所有的场景,但是在处理数据结构复杂的场景时,这4种数据结构有时会 ...

  2. CSS基础-4 定位

    CSS定位和浮动 css定位:改变页面的位置 定位机制有以下三种 普通流 浮动 绝对布局 定位的属性: position:把元素放在一个静态的.相对的.绝对的.或固定的位置中 top          ...

  3. TypeScript中文教程基础部分上----翻译自TS官方

    为什么使用TS? js中每一个值在不同的操作运行中表现出一系列不同的行为,比如说下面这个例子: message.toLowerCase();message(); 逐行看下,第一行调用了message的 ...

  4. Tool_Fiddler安装和使用

    一.简介 Fiddler(中文名称:小提琴)是一个HTTP的调试代理,以代理服务器的方式,监听系统的Http网络数据流动, Fiddler可以也可以让你检查所有的HTTP通讯,设置断点,以及Fiddl ...

  5. JMeter_请求header

    在接口调试的时候,请求参数确认正确无误,但是请求失败! 通过对比header,发现header缺少一些字段(token)以及传入的值不正确(Content-Type) 增加这些字段信息后,接口调试成功 ...

  6. c# - 接口的写法与基本调用

    1.前言 接口与Java基本一样 2.操作 (1)看路径结果 (2) 接口源码: namespace ConsoleApp1 { public interface ILogin { void Eat( ...

  7. Sentry 企业级数据安全解决方案 - Relay 操作指南

    内容整理自官方文档 本篇回顾了我们在自托管外部使用 Relay 时的操作指南,即在您的硬件上运行的 Relay 并将事件转发到 sentry.io. 系列 Sentry 企业级数据安全解决方案 - R ...

  8. hyperf 如何对AMQP消息进行手动消费?

    转发自白狼栈:查看原文 在使用 hyperf 官方自带的 AMQP 队列时你会发现,不需要我们再额外启动进程对消息进行消费.这是因为默认情况下,使用 @Consumer 注解时,hyperf 会为我们 ...

  9. 金融云原生漫谈(三)|银行云原生基础设施构建:裸金属VS虚拟机

    在金融行业数字化转型的驱动下,国有银行.股份制银行和各级商业银行也纷纷步入容器化的进程.   如果以容器云上生产为目标,那么整个容器云平台的设计.建设和优化对于银行来说是一个巨大的挑战.如何更好地利用 ...

  10. 51 Nod 1006 最长公共子序列(LCS & DP)

    原题链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1006 题目分析: 首先先知道LCS问题,这有两种: Long ...