一.前言
🤡大家好,我是一溪风月
程序员界的底层打工仔,昨天测试看到我的代码里面使用了this
就对我说,你这样用this
肯定有问题,我都不需要测,你不行的话让我来,我笑一下,没有怼他,打工人自然是点到为止,我说停停,年轻人不讲武德,话说JS中到底有多少种this
的绑定方式,好像仔细思考了下确实不能清晰的直接回答出来,既然有疑惑,那么就解决疑惑,今天这篇文章就来系统的总结下JS中this
的绑定方式,最后顺便🥴解决下和this
相关的面试题。
一.隐式绑定
🥴直接函数调用
:直接函数调用又叫做默认绑定,直接函数调用是直接对函数进行调用,此时在运行时this
的指向,会发现在默认函数绑定的情况下是指向顶级对象的,仅仅说结论没有意义,我们来用代码演示一下(使用的node.js环境)首先我们看下第一种方式,直接进行函数调用。
function foo(){
console.log(this);
}
foo()
// Object [global]
我们可以看到这种直接进行函数调用的this
是指向顶级对象的,那么如果我们进行函数的嵌套调用哪?那是不是就不指向顶级对象了?那么我们来试试看。
function foo1 () {
console.log(this)
foo2()
}
function foo2 () {
console.log(this)
foo3()
}
function foo3 () {
console.log(this)
}
foo1()
// Object [global]
// Object [global]
我们会发现它仍然指向的是顶级对象。
function foo (fn) {
fn()
}
let obj = {
name: "zzz",
bar: function () {
console.log(this)
}
}
foo(obj.bar)
// Object [global]
我们将一个函数定义在一个对象里面,然后进行函数的直接调用,然后执行看下结果,发现也是,指向的顶级对象,其实这个非常好理解,因为this
的绑定的准则就是谁直接调用它,它指向谁
,而在默认函数绑定的情况下,没有一个特别明确的调用对象,所以this
就指向顶级对象。
🦊通过对象调用
:在隐式绑定中还有另外一种情况就是,在一个对象中,由这个对象去执行函数的调用,调用者是这个对象,那么按照this
指向的准则,那么它指向的就是这个对象,首先我们使用最常用的方式,使用对象进行调用这个函数,然后我们会发现,它的结果指向的是这个对象。
function foo(){
console.log(this);
}
let obj= {
name:"zzz",
bar:foo
}
obj.bar()
// { name: 'zzz', bar: [Function: foo] }
嘿嘿,那如果是进行对象的嵌套的方式调用哪?我们来测试下。
function foo () {
console.log(this)
}
let obj = {
name: "zzz",
bar: foo
}
let obj2 = {
name: "sss",
obj: obj
}
obj2.obj.bar()
// { name: 'zzz', bar: [Function: foo] }
🚨注意:如果对调用了函数的对象进行了二次的赋值,对这个赋值后的这个变量进行调用哪?其实结果依然是一样的,谁直接调用它,this指向谁
,这么做其实直接调用者并不是这个对象,那么this
指向的仍然还是顶级对象。
function foo () {
console.log(this)
}
let obj = {
name: "zzz",
foo: foo
}
let baz = obj.foo
baz()
// Object [global]
二.显式绑定
🐸什么是显示绑定哪?显示绑定就是我们自己对this
进行指定具体的指向,这种操作的方式我们称之为显式绑定,显示绑定常用的就是三个函数分别是call
apply
bind
我们分别来对它进行讲解。
🦊因为call
和apply
是基本一样的,不同点在于参数列表的传递方式不同,call
是一个一个参数进行传递的,而apply
是通过一个数组进行传递的,我们这里不具体讲解他们的使用方式,我们会在本文的最后讲解下他们的使用方式,首先我们使用call
和apply
来进行this
指向的更改。
function foo () {
console.log(this)
}
let obj = {
name:"zzz",
age:13
}
// 使用call和apply将当前的this指向更改为obj
foo.call(obj)
// { name: 'zzz', age: 13 }
我们可以看到,当我们使用call
函数绑定了obj
此时this
的指向就被更改为指向obj
function foo () {
console.log(this)
}
let obj = {
name: "zzz",
age: 13
}
foo.apply(obj)
// { name: 'zzz', age: 13 }
我们使用apply
的结果也一样,也是指向自己手动绑定的对象。
🤡在显示绑定中,比较特殊的,需要单独将讲解一下的其实是bind
,因为bind
虽然没有前两个函数常用,但是功能特别的强大,使用的场景也比较复杂,所以我们单独来讲解下。
function foo () {
console.log(this)
}
let obj = {
name: "zzz",
age: 13
}
let baz = foo.bind(obj)
baz()
// { name: 'zzz', age: 13 }
首先我们从使用方式上来看,bind
函数的使用方式和前两个不一样,因为bind
返回了一个新的函数,我们称之为绑定函数(BF),但是其实结果依然是一样的,指向的依然是这个手动绑定的对象,我们在下文讲解多种规则混合绑定优先级的时候会再进行相应的测试。
三.new绑定
🐻在真正开始讲解new
绑定的时候我们首先先讲一下new
的时候在JS中究竟做了什么事情。
- 在执行new的时候会创建一个全新的对象,也就是一个空对象。
- 这个对象会指向
prototype
连接。 - 这个对象会被绑定到函数调用的
this
上面。 - 如果函数本身没有返回其他值,会返回这个新对象。
function foo (name) {
console.log(this) // foo {}
this.name = name // foo { name: 'zzz' }
}
// 实例化函数
let obj = new foo("zzz")
console.log(obj)
我们尝试着写下上面的这个案例,你会发现,this
指向的是实例化的这个对象,所以我们可以得出结论,在进行new
绑定的时候this
指向的就是这个实例化的对象。
四.绑定规则优先级
🦉在讲解上面几个绑定规则的时候,你会发现我们都是单个绑定规则进行测试和使用的,但是实际上他们完全有可能多种规则同时使用,这个时候就会涉及到绑定规则的优先级的区别,先说结论。
- 默认绑定的优先级最低。
- 显式绑定的优先级高于隐式绑定。
new
绑定的优先级高于bind
的绑定,new
和call
apply
无法放在一起使用。
😆既然我们讲了上述的结论,那么我们就手动来测试下不同规则之间的优先级的高低。
- 显示绑定的优先级高于隐式绑定:如下这个例子,当我们进行隐式绑定后进行显示绑定,查看结果是abc。
function foo () {
console.log(this)
}
let obj = {
foo: foo
}
obj.foo.call("abc")
// abc
- 测试下
bind
是否也高于默认绑定,我们发现bind
也是高于隐式绑定的。
function foo () {
console.log(this)
}
let baz = foo.bind("abc")
let obj = {
name: "why",
bar: baz
}
obj.bar()
- new绑定优先级高于隐式绑定:我们发现
this
指向的是一个空对象,所以new绑定优先级高于隐式绑定。
function foo () {
console.log(this)
}
let obj = {
name: "why",
bar: foo
}
new obj.bar()
// {}
- 既然new绑定和显式绑定都高于隐式绑定,那么new绑定和显示绑定谁的优先级高哪?首先我们应该知道new绑定无法和
call
和apply
进行一起使用,所以不能存在谁的优先级高,谁的优先级低,但是bind
可以和他们一起使用。
function foo () {
console.log(this)
}
let baz = foo.bind("bac")
new baz()
// {}
我们发现执行的结果是一个新的空对象,我们就可以得出结果,new绑定是比bind
的绑定的优先级高的。
- 比较下
apply
和bind
的优先级别,我们从比较的结果上看,bind
的优先级是比call
和apply
高的
function foo () {
console.log(this)
}
let baz = foo.bind("bac")
baz.apply("ddd")
// [String: 'bac']
五.绑定规则之外的一些情况
🐻其实我们了解的这些绑定规则足以应付平时的开发了,但是无论在当下的世界还是神话传说中都会有那么几个不在规则之内,所以this
也不例外,那么我们就就来看下这些this
都指向什么地方。
- 在显式绑定中我们如果给定一个数据,那么
this
就会指向这个数据,但是当我们传入的是null
或者undefined
的时候哪? 我们会发现指向的是window,其实当我们传入这两个参数的时候,会忽略显式绑定,但是在严格模式下指向的是null
或者undefined
function foo () {
console.log(this)
}
foo.apply(null)
// window
- 间接函数引用(可以看下你不知道的JavaScript这本书),虽然我们在平时中不会这样写,但是也做好了解下
let obj1 = {
name: "obj1",
foo: function () {
console.log("foo", this)
}
}
let obj2 = {
name: "obj2",
}
obj2.foo = obj1.foo
obj2.foo()
// foo { name: 'obj2', foo: [Function: foo] }
let obj1 = {
name: "obj1",
foo: function () {
console.log("foo", this)
}
}
let obj2 = {
name: "obj2",
};
(obj2.foo = obj1.foo)();
// window
- 箭头函数的绑定情况,箭头函数中是没有
this
这个东西的,所以你拿到的this是上级的this,如下的代码我们会发现我们根本无法绑定到this
,因为箭头函数中没有this。
let baz = ()=>{
console.log(this);
}
baz.call("aaa")
六.内置函数绑定的思考与经验
🥴但是你在实际开发中可能会发现,有时候我们根本不知道有些内置函数的内部是如何被调用的,这个时候我们大部分事件其实是靠经验判断的,我们来看几个常用的内置函数的this
的绑定。
- 定时器函数:内部的
this
指向的是window
setTimeout(() => {
console.log("定时器函数", this)
}, 1000
)
// window
- 按钮的点击监听:
this
指向的是btn
let btn = document.querySelector("btn")
btn.onclick = function () {
console.log("btn的点击", this)
}
btn.addEventListener('click', function () {
console.log(this)
})
- forEach:
this
一般指向的是window
但是其实forEach
还有另外一个参数,可以指定this
的指向。
let name = ["abc", "cba", "bbb"]
name.forEach(item => {
console.log(this)
})
// window
let name = ["abc", "cba", "bbb"]
name.forEach(item => {
console.log(this)
},"aaa")
// aaa
七.this相关面试题
🦊以下是一些四道常见的this
的面试题,感兴趣的同学可以尝试着做一下,答案结果已经以注释的方式标识
😶🌫️面试题一
🥴面试题二
🦉面试题三
🦄面试题四
九.总结
🐸这篇文章我们细致的讲解和总结了this
的常见的绑定方式,以及一些绑定规则在一起使用的时候的规则优先级,以及一些在开发中遇到的一些指向不太清晰的情况的相应的经验,除此之外我们还学习了一些规则之外的情况,相信在你学习了和了解了这些规则之后,以后无论遇到什么this
绑定的情况都能够解决。
原文链接:https://juejin.cn/post/7351682240746831909 作者:一溪风月