JS 函数的 this 指向

1. this 在全局作用域下的指向

在浏览器中,this在全局作用域下指向 window

console.log(this) // window

var title = "hello"
console.log(this.title) // hello
console.log(window.title) // hello

2. this 的四个绑定规则

2.1 规则一:默认绑定

独立函数调用的情况下会使用默认绑定

独立函数调用简单来说就是它没有绑定到某个对象上进行调用,简单看几个例子

// 例1
function foo() {
console.log(this)
}
foo() // window
// 例2
function foo1() {
console.log(this);
}

function foo2() {
foo1()
}

function foo3() {
foo2()
}

foo3() // window
// 例3
var obj = {
name: 'hello',
foo: function() {
console.log(this);
}
}

var bar = obj.foo
bar() // window

以上的函数调用的时候,都是没有被绑定到对象中调用的,所以 this 都指向 window

2.2 规则二:隐式绑定

函数通过某个对象进行调用的时候,这个对象会被 js 引擎绑定到函数中的 this 里

下面看几个例子:

// 1.
function foo() {
console.log(this);
}
var obj = {
name: 'hello',
fn: foo
}

obj.fn() // obj 对象
// 2.
var obj1 = {
foo: function() {
console.log(this);
}
}

var obj2 = {
title: 'hello',
bar: obj1.foo
}

obj2.bar() // obj2对象

2.3 显式绑定

利用 call,apply,bind手动指定 this 的绑定,这种明确的绑定称为显式绑定

(1) call,apply

call,apply 都是可以调用函数的

fn.call(自定的this指向,参数1,参数2) fn.apply(this指向,[参数1,参数2])

两者的区别在于传参的方式,下面例子可以看出来 call 传入参数列表,apply是数组

function sum(num1, num2, num3) {
console.log(num1 + num2 + num3, this);
}

var obj = {
title: "hello"
}

sum.call("123", 20,30,40) // 手动让this指向"123"
sum.apply(obj, [20,30,40]) // 手动让this指向obj

image-20220327114447032.png

(2) bind

bind不会调用函数,而是返回一个新的对象。

function sum(num1, num2, num3) {
console.log(num1 + num2 + num3, this);
}

// var obj = {
// title: "hello"
// }

// sum.call("123", 20,30,40) // 手动让this指向"123"
// sum.apply(obj, [20,30,40]) // 手动让this指向obj

var newSum = sum.bind('aaa',10,20,30)
newSum() // 60 String {'aaa'}

咦?newSum 不也是独立函数调用吗,怎么不指向 window 了

这就是默认绑定和显式绑定bind的冲突,显式绑定的优先级更高!

2.4 new绑定

function Person(name, age) {
this.name = name
this.age = age
// 实际上这里是会把 this 返回出去的,即return this(默认)
// this = 创建出来的对象
}

// new 会创建一个全新的对象
var p1 = new Person("jenny", 12)
var p2 = new Person("tony", 15)

3. 一些内置函数的 this 绑定

有时候我们会调用一些 JS 的内置函数,这些函数要求我们传入另外一个函数,并且我们不会自己去调用这些函数,而是 JS 内部会帮助我们执行。

这个时候,this 的绑定就跟内部帮我们指向函数的方式有关系了。

看几个例子吧

3.1 setTimeout

内部通过 apply 调用函数,并绑定了this对象,是window

setTimeout(function() {
console.log(this); // window
}, 2000)
3.2 数组的内置函数 forEach

默认情况下传入的函数是自动调用函数(默认绑定)

var names = ['aaa', 'bbb','ccc']
names.forEach(function(item) {
console.log(this); // 三次 window
})

也可以通过传入第二个参数,改变this指向

var names = ['aaa', 'bbb','ccc']
names.forEach(function(item) {
console.log(this); // 三次 'obj'
}, 'obj')
3.3 点击事件

在发生点击时,回调函数被调用,会将 this 绑定到该函数中

<div class="box"> </div>
var box = document.querySelector(".box")
box.onclick = function() {
console.log(this); // box对象
}

所以说,某些内置的函数,我们很难确定它内部是如何调用传入的回调函数的,可以自己测试一下,记一下,看源码当然更好啦

4. 几个规则的优先级

从高到低:new绑定 > 显式绑定(bind) > 隐式绑定 > 默认绑定

new绑定和call,apply是不允许同时使用的,(毕竟都会调用函数),所以不存在谁的优先级更高

  • 显式绑定高于隐式绑定

    // 显式绑定和隐式绑定
    function foo() {
    console.log(this);
    }

    var obj = {
    name: 'obj',
    foo: foo
    }

    obj.foo() // obj 对象
    obj.foo.call("aaa") // 'aaa'
  • new 绑定高于隐式绑定

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

    var obj = {
    name: 'hello',
    foo: foo
    }

    new obj.foo() // 输出 foo 对象而不是 obj
  • new 绑定高于bind

    function foo() {
    console.log(this, title);
    }

    var obj = {
    name: 'hello'
    }

    var bar = foo.bind(obj) // 显式绑定obj
    new bar() // 打印 foo对象 而不是 obj

5. 两种特殊情况

5.1 在显式绑定中传入 null 或 undefined

这时候,显式绑定会被忽略,使用默认规则

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

var obj = {
name: "hello",
foo: foo
}

foo.call(obj) // obj
foo.call(null) // window
foo.call(undefined) // window

5.2 间接函数引用

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

var obj1 = {
name: 'obj1',
foo: foo
}

var obj2 = {
name: 'obj2',
}

// 先来看一下这个赋值的结果, 是 foo 函数
console.log((obj2.foo = obj1.foo)); // foo函数
// 这种叫做间接函数引用,是会把赋值语句右边的结果返回,然后直接调用,是独立函数调用
(obj2.foo = obj1.foo)() // 相当于直接调用 foo 函数,所以是默认绑定,输出window

6. 箭头函数的this

箭头函数不绑定 this,也就是说它没有自己的 this,而是根据外层作用域来决定this

var name = "hello"

var foo = () => {
console.log(this); // 在这里this的外层作用域就是 window
}

var obj = {
foo: foo
}

obj.foo() // window
obj.foo.call("aaa") // window

再来看一个案例:

使用setTimeout模拟一个网络请求,如果我们想把返回来的数据保存到 data 中,该怎么做

var obj = {
data: '',
getData: function() {
setTimeout(() => {
console.log(this); // obj 对象
// 这里的 this 不是应该指向window吗?
// 如果我们这里使用的是普通的函数 function() {console.log(this)}
// 那么这个函数在内部调用的时候,会绑定this,就是window
// 但是我们使用了箭头函数,它不绑定this,会往上层作用域中找到对应的this
// 所以找到了 obj
this.data = 'hello' // --> obj.data = "hello"
}, 2000);
}
}
obj.getData()

7. this 的面试题

如果每题都会了,那么 this 绑定的问题,就是你的送分题而不是送命题了哈哈哈

建议把我的答案跟注释都删掉,自己一点点理清哦

题一

var name = "window"; // window.name = "window"

var person = {
name: "person",
sayName: function () {
console.log(this.name);
}
};

function sayName() {
var sss = person.sayName;
sss(); // window 很明显是独立函数调用,没有与任何对象关联
person.sayName(); // person 隐式绑定,与person关联
(person.sayName)(); // person 同上(加括号只是代表这是一个整体)
// console.log((b = person.sayName)); // 这里实际上就是sayName这个函数
(b = person.sayName)(); // 间接函数引用,是独立函数调用, 输出 window
}

sayName();

题二

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对象
person1.foo1(); // person1

// 显式绑定person2
person1.foo1.call(person2); // person2

// 箭头函数不适用任何规则, 向上层作用域中找this
person1.foo2(); // window
person1.foo2.call(person2); // window

// person1.foo3()返回了一个函数,然后独立调用
person1.foo3()(); // window

// person1.foo3.call(person2) 返回的是一个函数,然后独立调用
person1.foo3.call(person2)(); // window

// person1.foo3()返回一个函数,然后显式绑定到 person2
person1.foo3().call(person2); // person2

// person1.foo4()返回一个箭头函数,往上层作用域找,找到foo4中绑定的this,是 person1
person1.foo4()(); // person1
// foo4显式绑定到 person2
person1.foo4.call(person2)(); // person2
// person1.foo4()返回箭头函数,往上层作用域找
person1.foo4().call(person2); // person1

题三

var name = 'window'
function Person (name) {
this.name = name

this.foo1 = function () {
console.log(this.name)
},

this.foo2 = () => console.log(this.name),

this.foo3 = function () {
return function () {
console.log(this.name)
}
},

this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1 隐式绑定
person1.foo1.call(person2) // person2 显式绑定

person1.foo2() // person1 隐式绑定
// 箭头函数不适用显式绑定规则,直接向上层作用域找
person1.foo2.call(person2) // person1

// person1.foo3() 返回一个函数,在全局调用
person1.foo3()() // window
person1.foo3.call(person2)() // window // 同理
// person1.foo3() 返回的函数使用 .call 显式绑定 person2
person1.foo3().call(person2) //person2

// person1.foo4() 返回一个箭头函数,再调用,向上层作用域找
person1.foo4()() // person1

// person1.foo4.call(person2) 返回箭头函数,并且foo4显式绑定this为person2
// 再调用这个箭头函数,向上找就找到 foo4 的this 为person2
person1.foo4.call(person2)() // person2

// person1.foo4() 返回箭头函数,不适用显式绑定,向上找到 person1
// 注意这里跟上面的区别,这里的foo4调用不是.call调用的,而是.foo4()这样调用的
// call是来调用箭头函数的,而箭头函数不适用显式绑定,向上找到的是person1
person1.foo4().call(person2) // person1

题四

var name = 'window'
function Person (name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')

// person1.obj.foo1()返回一个函数,在全局中调用
person1.obj.foo1()() // window
// person1.obj.foo1.call(person2) 返回一个函数 在全局中调用
person1.obj.foo1.call(person2)() // window
// person1.obj.foo1() 返回一个函数,显式绑定person2
person1.obj.foo1().call(person2) // person2


// 箭头函数调用,向上找到 foo2 中的this是obj
person1.obj.foo2()() // obj
// foo2调用的时候显式绑定person2,箭头函数向上层找到的就是person2
person1.obj.foo2.call(person2)() // person2
// 箭头函数不适用 显式绑定,向上找找到 obj
person1.obj.foo2().call(person2) // obj