草庐IT

从简单到复杂:深入了解 JavaScript 中的 this 绑定规则

CoderBin 2023-03-28 原文

前言

大家好,我是 CoderBin,在JavaScript中,this是一个非常重要的概念,属于进阶知识,不管是在面试还是日常开发中都是非常常用的。所以本次给大家总结了关于this的绑定规则,来帮助大家更加深入的掌握这个知识点。希望对大家有所帮助,谢谢。

创作不易,你们的点赞收藏留言就是我最大的动力?

如果文中有不对、疑惑的地方,欢迎各位小伙伴们在评论区留言指正?

1. 关于 this 的简单介绍

this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向什么。

任何足够先进的技术都和魔法无异。— Arthur C. Clarke

实际上,JavaScript 中 this 的机制并没有那么先进,但是开发者往往会把理解过程复杂化, 毫无疑问,在缺乏清晰认识的情况下,this 对你来说完全就是一种魔法。

2. 为什么使用 this?

const obj = { title: '华为云社区', reading() { console.log(this.title + ',一个帮助开发者成长的社区'); } } this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。

随着你的使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象有多重要

3. 关于this 的常见的误解

人们很容易把 this 理解成指向函数自身,JavaScript 的新手开发者通常会认为,既然把函数看作一个对象(JavaScript 中的所有函数 都是对象),那就可以在调用函数时存储状态(属性的值)。但结果通常让他们大吃一惊,例如下面这段代码

function foo() { // 让新添加的 count + 1 this.count++ } // 向函数对象 foo 添加了一个属性 count foo.count = 0 foo() console.log(foo.count); // 0 这段代码看起来没什么问题,但请把目光聚焦到最后一行,输出 foo.count 的结果竟然是0 ?!

疑问:为什么会这样?我明明向函数对象 foo 添加了一个属性 count,并且函数内部也写了 this.count++,为什么最后还是0呢?

解答:因为this.count 中此时的 this,根本不是指向 foo 函数自身,而是指向全局 window。再细心一点,我们可以发现,在 window 身上,被添加了个 count 属性,值为 NaN。 (为什么 this 指向 window 后面会阐述) 所以,单纯的把 this 理解为指向函数自身是错误的。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

4. this 的绑定规则

我们来看看在函数的执行过程中调用位置如何决定 this 的绑定对象。

找到调用位置,然后判断需要应用下面四条规则中的哪一条。我首先会分别解释 这四条规则,然后解释多条规则都可用时它们的优先级如何排列。

4.1 默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

function foo() { console.log(this.a) } var a = 2 foo() // 2
  1. 我们可以看到当调用 foo() 时,this.a 被解析成了全局变量 a。为什么?因为在本例中,函数调用时应用了 this 的默认绑定,因此 this 指向全局对象

  2. 那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看 foo() 是如何调 用的。在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则

  3. 这条规则也解释了上面 count 的代码中,为什么函数里面的this指向了window,因为 foo属于独立函数调用的,触发了默认绑定,从而指向全局window。(浏览器中全局是 window对象,node 中是空对象{})

  4. 属于默认绑定规则的还有:

    • 函数调用链(一个函数又调用另外一个函数)
    • 将函数作为参数,传入到另一个函数中
(扩展:如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此 this 会绑定到 undefined:)

结论:默认绑定的 this,都指向全局。

4.2 隐式绑定

4.2.1 一般的对象调用

这一条需要考虑的规则是调用位置是否有上下文对象,或者说是通过某个对象发起的函数调用

function foo() { console.log(this.a) } const obj = { a: 2, foo: foo } // 通过 obj 对象调用 foo 函数 obj.foo() // 2
  1. 调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
  2. foo() 被调用时,它的前面确实加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo()this 被绑定到 obj 上,因此 this.aobj.a 是一样的。

4.2.2 对象属性引用链

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:

function foo() { console.log(this.a) } var obj2 = { a: 2, foo: foo } var obj1 = { a: 1, obj2: obj2 } obj1.obj2.foo() // 2 最终 this 指向的是 obj2

4.2.3 隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上(取决于是否是严格模式)

第一种情况:将对象里的函数赋值给一个变量

function foo() { console.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo // 函数别名! var a = 'global' // a 是全局对象的属性 bar() // "global" 虽然 barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

第二种情况:传入回调函数时

function foo() { console.log(this.a) } function doFoo(fn) { // fn 其实引用的是 foo fn() // <-- 调用位置! } var obj = { a: 2, foo: foo } var a = 'global' // a 是全局对象的属性 doFoo(obj.foo) // "global" 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。

结论:隐式绑定的 this,指向调用函数的上下文对象。

4.3 显式绑定

4.3.1 使用 call(...) 和 apply(...)

如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢? 可以使用函数的 call(..)apply(..) 方法

  • JavaScript 提供的绝大多数函数以及你自 己创建的所有函数都可以使用 call(..)apply(..) 方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。 思考以下代码:

function foo() { console.log(this.a) } var obj = { a: 2 } foo.call(obj) // 2
  1. 通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
  2. 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。
从 this 绑定的角度来说,call(..) 和 apply(..) 是一样的,它们的区别体现在参数上:第一个参数是相同的,后面的参数,call为参数列表,apply为数组,他们内部的实现原理也不难理解,详细请看以下两个手写方法

4.3.2 硬绑定(一个函数总是显示的绑定到一个对象上)

由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法 Function.prototype.bind, 它的用法如下

function foo(num) { console.log(this.a, num) return this.a + num } var obj = { a: 2 } // 调用 bind() 方法,返回一个函数 var bar = foo.bind(obj) var b = bar(3) // 2 3 console.log(b) // 5 调用 bind(...) 方法,会返回一个新函数,那么这个新函数的 this,永远指向我们传入的obj对象

关于 bind 方法的简单实现,可以前往:手写 bind 方法,超级详细 ⚡⚡⚡

4.3.3 API调用的 “上下文(内置函数)

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。例如:

(1)数组方法 forEach()

function foo(el) { console.log(el, this.id) } var obj = { id: 'bin' }; [1, 2, 3].forEach(foo, obj) // 输出: // 1 bin // 2 bin // 3 bin
  • 调用 foo(..) 时把 this 绑定到 obj 身上
(2)setTimeout()

setTimeout(function() {   console.log(this); // window }, 1000);
  • 在使用 setTimeout 时会传入一个回调函数,而这个回调函数中的this一般指向 window,这个和 setTimeout 源码的内部调用有关,这个不再展开赘述
结论:显式绑定的 this,指向我们指定的绑定对象。

4.4 new 绑定

在 JavaScript 中,普通函数可以使用 new 操作符去调用,此时的普通函数则被称为 “构造函数”。没错,凡是由 new 操作符调用的函数,都称为 “构造函数”

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

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function foo(a) { this.a = a } var bar = new foo(2) console.log(bar.a) // 2 使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定

结论:new 绑定的 this,都指向通过 new 调用的函数的实例对象(就是该函数)

5. 绑定规则的优先级

现在我们已经了解了函数调用中 this 绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则呢?所以就需要有绑定规则的优先级。

它们之间的优先级关系为:

默认绑定 < 隐式绑定 < 显示绑定(bind) < new绑定

这里提前列出优先级,想看详细代码解析的可以往下看,也可以直接拖到最后面的例题部分

5.1 默认绑定的优先级最低

毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this

5.2 隐式绑定和显式绑定的优先级比较

测试一下即可知道,有以下代码:

function foo() { console.log(this.a) } var obj1 = { a: 1, foo: foo } var obj2 = { a: 2, foo: foo } // 同时使用隐式绑定和显示绑定 obj1.foo.call(obj2) // 2 可以看到,输出的结果为 2,说明 foo 函数内 this 指向的是 obj2,而 obj2 是通过显示绑定调用的,所以:显示绑定的优先级更高

5.3 隐式绑定和 new 绑定的优先级比较

有以下测试代码:

function foo() { console.log(this); } var obj = { title: "juejin", foo: foo } // 同时使用隐式绑定和new绑定 new obj.foo(); // foo对象 最后 foo 函数输出的 this 为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高

5.4 new 绑定和显示绑定的优先级比较

最后,new 绑定和显式绑定谁的优先级更高呢?

new绑定和call、apply是不允许同时使用的,只能和 bind 相比较,如下:

function foo() { console.log(this) } var obj = { title: "juejin" } var foo = new foo.call(obj); // 直接报错 但是 new 绑定可以和 bind 方法返回后的函数一起使用

function foo() { console.log(this); } var obj = { title: "juejin" } var bar = foo.bind(obj); var foo = new bar(); // foo 对象, 说明使用的是new绑定 最后 foo 函数输出的 this 为 foo 对象,说明new绑定优先级更高(否则应该输出 obj 对象),所以:new 绑定的优先级更高

优先级结论:默认绑定 < 隐式绑定 < 显示绑定(bind)< new绑定

6. 判断this

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

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定 到全局对象。 var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。 不过……凡事总有例外

7. 绑定例外

规则总有例外,这里也一样。在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

7.1 箭头函数

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。

7.2 被忽略的this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

function foo() { console.log(this.a) } var a = 2 foo.call(null) // 2 foo.call(undefined) // 2 foo.bind(null)(); 最后输出的结果都是 2,说明 this 指向的是全局 window

7.3 间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的间接引用,在这种情况下,调用这个函数会应用默认绑定规则。 间接引用最容易在赋值时发生:

function foo() { console.log(this.a) } var a = 2 var o = { a: 3, foo: foo } var p = { a: 4 } o.foo(); // 3 // 函数赋值 (p.foo = o.foo)() // 2 赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 属于独立函数调用,而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

8. this 判断例题

请说出例题中的输出结果

8.1 例题一

var name = "window"; var person = { name: "person", sayName: function () { console.log(this.name); } }; function sayName() { var sss = person.sayName; sss(); person.sayName(); (person.sayName)(); (b = person.sayName)(); } sayName(); 解析:

function sayName() { var sss = person.sayName; // 独立函数调用,没有和任何对象关联 sss(); // window // 关联 person.sayName(); // person (person.sayName)(); // person (b = person.sayName)(); // window }

8.2 例题二

var name = 'window' var person1 = { name: 'person1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var person2 = { name: 'person2' } person1.foo1(); person1.foo1.call(person2); person1.foo2(); person1.foo2.call(person2); person1.foo3()(); person1.foo3.call(person2)(); person1.foo3().call(person2); person1.foo4()(); person1.foo4.call(person2)(); person1.foo4().call(person2); 解析:

// 隐式绑定,肯定是person1 person1.foo1(); // person1 // 隐式绑定和显示绑定的结合,显示绑定生效,所以是person2 person1.foo1.call(person2); // person2 // foo2()是一个箭头函数,不适用所有的规则 person1.foo2() // window // foo2依然是箭头函数,不适用于显示绑定的规则 person1.foo2.call(person2) // window // 获取到foo3,但是调用位置是全局作用于下,所以是默认绑定window person1.foo3()() // window // foo3显示绑定到person2中 // 但是拿到的返回函数依然是在全局下调用,所以依然是window person1.foo3.call(person2)() // window // 拿到foo3返回的函数,通过显示绑定到person2中,所以是person2 person1.foo3().call(person2) // person2 // foo4()的函数返回的是一个箭头函数 // 箭头函数的执行找上层作用域,是person1 person1.foo4()() // person1 // foo4()显示绑定到person2中,并且返回一个箭头函数 // 箭头函数找上层作用域,是person2 person1.foo4.call(person2)() // person2 // foo4返回的是箭头函数,箭头函数只看上层作用域 person1.foo4().call(person2) // person1

9. 总结

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

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

每文一句:如果把生活比喻为创作的意境,那么阅读就像阳光。

ok,本次的分享就到这里,如果本章内容对你有所帮助的话可以点赞+收藏,希望大家都能够有所收获。有任何疑问都可以在评论区留言,大家一起探讨、进步!

有关从简单到复杂:深入了解 JavaScript 中的 this 绑定规则的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  9. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

  10. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

随机推荐