文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

带你详解 this 的四个绑定规则

2022-11-22 23:40

关注

实际上,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-win.jpg所以,单纯的把 this 理解为指向函数自身是错误的。

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

4. this 的绑定规则

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

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

4.1 默认绑定

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

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

(扩展:如果使用严格模式(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

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(..) 方法

这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。 思考以下代码:

function foo() {
  console.log(this.a)
}

var obj = {
  a: 2
}

foo.call(obj) // 2

从 this 绑定的角度来说,call(..) 和 apply(..) 是一样的,它们的区别体现在参数上:第一个参数是相同的,后面的参数,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

(2)setTimeout()

setTimeout(function() {
  console.log(this); // window
}, 1000);

结论:显式绑定的 this,指向我们指定的绑定对象。

4.4 new 绑定

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

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

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

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

就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 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 的绑定对象。


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

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

【推荐学习:javascript高级教程】

以上就是带你详解 this 的四个绑定规则的详细内容,更多请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯