🍎 JavaScript this 指向问题
相关问题:
- JavaScript 中对函数
this
的理解?
# 概念
this
关键字时函数运行时自动生成的一个 内部对象。只能在函数内部使用,总是指向 调用它 的对象。 this
可以更加使用便捷的方式来引用对象,进行一些 API 设计时,代码更加简洁和易于复用。
绝大多数情况下,函数的调用方式决定了 this
的值(调用时绑定)。同时 this
在函数执行的过程中,一旦被确定了就不能再更改。
this
是什么:
任何函数本质上都是通过某个对象来调用的,⭐️ 如果没有制定就是全局作用域下的
window
。所有函数内部都有一个变量
this
;它的值是调用函数的当前对象;
# this
指向什么?
在全局作用域下:
- 浏览器中
this
没有指定时,为window
; - 所以全局作用域下,一般
this
指向的是window
;
但是开发中很少在全局作用域下使用 this
,一般在函数中使用 this
。
在函数被调用时,都会创建一个 执行上下文。这个执行上下文中记录函数的调用栈、函数的调用方法、传入的参数信息等; this
是函数内部其中的一个属性。
函数在定义时,默认会给
this
绑定一个值;this
的绑定和定义的位置(编写的位置)没有关系;this
的绑定与调用方式和调用的位置有关系。this
是在运行时被绑定的。
# 绑定规则
根据不同的使用场景, this
会有不同的值。
# 默认绑定
在独立的函数中调用,意味着函数没有绑定到每个对象上进行调用。可以分为两种情况:
普通函数调用。函数直接被调用,并没有任何的对象关联。这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的
this
指向全局对象window
。此时全局环境中定义的变量,在函数中可以通过
this
获取。(针对var
生命的变量)🌰 例子:
var name = "Simon" function person() { return this.name } console.log(person()) // Simon
1
2
3
4
5调用
this
的对象是函数,而函数本身没有name
这个变量,所以往外找,最终找到指向window
。严格模式下,不能讲全局对象用于默认绑定,
this
会绑定到undefined
。函数调用链:所有的函数调用都没有被绑定到某个对象上。
🌰 例子:
function test1() { console.log(this); // window test2(); } function test2() { console.log(this); // window test3() } function test3() { console.log(this); // window } test1();
1
2
3
4
5
6
7
8
9
10
11
12
13
14将函数作为参数,传入到另一个函数中:
🌰 例子:
function foo(func) { func() } function bar() { console.log(this) // window } foo(bar)
1
2
3
4
5
6
7
8
9🌰 🌟 例子 :
function foo(func) { func() } var obj = { name: "why", bar: function() { console.log(this); // window } } foo(obj.bar);
1
2
3
4
5
6
7
8
9
10
11
12此时的函数虽然在对象之中,但是真正调用函数的位置,并没有进行任何的对象绑定,只是一个独立函数的调用,所以这里的
this
仍然为window
。
# 隐式绑定
函数还可以作为某个对象的方法调用,此时 this
指向的就是这个上级对象。可以分为以下的情况:
通过对象调用函数:
🌰 例子:
function foo() { console.log(this) // obj } var obj = { name: "Simon", foo: foo } obj.foo()
1
2
3
4
5
6
7
8
9
10🌰 例子:
function foo() { console.log(this) // obj1 } var obj1 = { name: "Simon", foo: foo } var obj2 = { name: "Simon2", obj1: obj1 } obj2.obj1.foo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15此时,
foo
调用的位置仍然是obj1
,所以这里this
绑定的对象仍然是obj1
。隐式丢失:
function foo() { console.log(this) // window } var obj = { name: "Simon", foo: foo } var bar = obj1.foo bar()
1
2
3
4
5
6
7
8
9
10
11因为这里
foo
被调用的位置时bar
,而bar
在进行调用的时候没有绑定任何的对象,也就是没有形成隐形绑定。相当于默认绑定。
隐式绑定有一个前提条件:必须在调用的对象内部有一个对函数的引用(例如一个属性)。如果没有这样的引用,在进行调用的时候,会报找不到该函数的错误。正是通过这个引用,间接地将 this
绑定到这个对象上。
# 显式修改绑定
如果不希望在对象内部包含这个函数的引用,又希望在这个对象上强制调用。使用 apply
/ call
/ bind
方法。改变函数调用的对象,它们的第一个参数表示改变后调用这个函数的对象,因此 this
指向这个对象,这个过程明确的指定了 this
指向的对象所以称为显式绑定。
JavaScript 的所有函数都可以使用
call
/apply
方法。
call
/apply
:两者的区别知识参数上的传递有区别:🚃 JavaScript 装饰器模式和转发 | notebook (simon1uo.github.io) (opens new window),功能类似。
🌰 例子:
function foo() { console.log(this) } foo.call(window) // window foo.call({name: "simon"}) // {name: "simon"} foo.call(123) // Number 对象存放123
1
2
3
4
5
6
7bind
:如果希望函数总是显式绑定到一个对象上,这样每次调用函数时就不用每次都绑定this
到一个特定的对象上。🌰 例子:
function foo() { console.log(this) // window } var obj = { name: "simon" } var bar = foo.bind(obj) bar() // obj bar() // obj
1
2
3
4
5
6
7
8
9
10
11
12
# 内置函数
在 JavaScript 的内置函数(或者第三方库中的内置函数),这些内置函数允许传入另一个函数(回调函数),此时我们不会显式调用这些函数,而是 JavaScript 内部帮助我们执行。
setTimeout()
:🌰 例子:
setTimeout(function() { console.log(this) // window }, 1000)
1
2
3与
setTimeout
的内部调用有关。setTimeout
内部通过apply
绑定this
对象,并且绑定的是window
对象。forEach()
:🌰 例子:
var names = ['abc', 'def', 'ghi'] names.forEach(function(name) { console.log(this) // window*3 })
1
2
3
4默认情况下传入的函数是自动调用函数(默认绑定);
可以根据
forEach(value, thisArg)
的方法传入第二个参数,改变传入的对象:var obj = {name: "why"}; names.forEach(function(name) { console.log(this) // window*3 }, obj)
1
2
3
4HTML 元素的点击:
🌰 例子 /
div
:<div class="box"></div>
1var box = document.querySelector(".box"); box.onclick = function() { console.log(this); // box对象 }
1
2
3
4因为发生点击时,执行传入的回调函数被调用时,会将
box
对象绑定到该函数中。
传入到内置函数的回调函数 this
:
- 某些内置函数很难判断。一方面可以分析源码,另一方面通过经验判断。无论如何最终都是根据之前的 默认绑定或者隐式 / 显式绑定。
# new
绑定
JavaScript 通过构造器 new
关键字生成一个实例对象,此时 this
指向这个实例对象。
步骤:
- 创建一个全新的对象;
- 这个新对象会被执行
Prototype
连接;- 这个新对象会绑定到函数调用的
this
上(this
的绑定在这个步骤完成);- 如果函数没有返回其他对象,表达式会返回这个新对象;
🌰 例子:
function test() {
this.x = 1
}
var obj = new test()
obj.x // 1
2
3
4
5
6
之所以
obj.x
为1
,是因为new
改变了this
的指向。
🌰 例子 / 特殊情况:
function fn() {
this.user = 'xxx'
return {}
}
var a = new fn()
console.log(a.user) // undefined
2
3
4
5
6
此时
new
的过程中遇到了一个return
,此时this
指向的是返回的对象。当返回的是一个简单类型时,
this
指向实例对象:function fn() { this.user = 'xxx' return 1 } var a = new fn() console.log(a.user) // xxx
1
2
3
4
5
6当返回的是
null
时,虽然也是对象,但是此时this
指向实例对象。
# 绑定的优先级
显示绑定 > new
绑定 > 隐式绑定 > 默认绑定
- 因为存在其他规则是,就会使用其他规则绑定
this
。而不是默认绑定,所以默认绑定的优先级最低。
🌰 例子 / 隐式绑定与显式绑定比较:
function foo() {
console.log(this.a)
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo() // 2
obj2.foo() // 3
obj1.foo.call(obj2) // 3
obj2,foo.call(obj1) // 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🌰 例子 / new
绑定与隐式绑定比较:
function foo(something) {
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2
obj1.foo.call(obj2, 3) // 3
console.log(obj2.a) // 3
var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🌰 例子 / new
绑定与显示绑定比较:
function foo(something) {
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3);
console.log(obj1.a) // 2
console.log(baz.a) // 3
2
3
4
5
6
7
8
9
10
11
12
bar
被绑定到 obj1 上,但是new bar(3)
并没有像我们预计的那样把obj1.a
修改为 3。但是,new
修改了绑定调用bar()
中的this
# 其他情况绑定规则
# 忽略规则绑定
如果在显式绑定中,绑定对象时传入的参数为 undefined
或者 null
,那么这个显式绑定就会被忽略,然后使用默认规则。
🌰 例子:
function foo() {
console.log(this)
}
var obj = {
name = "simon"
}
foo.call(obj) // obj
foo.call(undefined) // window
foo.call(null) // window
var bar = foo.bind(null)
bar() // window
2
3
4
5
6
7
8
9
10
11
12
13
14
# 间接函数引用
创建函数的间接引用,这时使用默认绑定规则。
🌰 例子:
var num1 = 100
var num2 = 0
var result = (num2 = num1)
console.log(result) // 100
2
3
4
引申到:
function foo() {
console.log(this)
}
var obj1 = {
name = "obj1",
foo: foo
}
var obj2 = {
name = "obj2"
}
obj1.foo() // obj1
(obj2.foo = obj1.foo)() // window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
因为赋值
(obj2.foo = obj1.foo)
的结果是foo
函数。所以foo
函数被直接调用,使用默认绑定规则。
# 箭头函数的 this
指向
ES6 中箭头函数的语法,不使用四种绑定规则(不绑定 this
),而是由外层作用域决定 this
。
🌰 例子:
var obj = {
data: [],
getData: function() {
var _this = this;
setTimeout(function() {
// 模拟获取到的数据
var res = ["abc", "cba", "nba"];
_this.data.push(...res);
}, 1000);
}
}
obj.getData();
2
3
4
5
6
7
8
9
10
11
12
13
使用
setTimeout
来模拟网络请求,需要通过this
获取obj
对象存放到data
。但是setTimeout
中的函数直接的this
绑定的是window
,所以需要在外层定义var _this = this
。
如果使用 箭头函数,可以直接使用 this
。因为箭头函数不会绑定 this
对象,那么 this
引用就会从上层作用域找到对应的 this
:
var obj = {
data: [],
getData: function() {
setTimeout(() => {
// 模拟获取到的数据
var res = ["abc", "cba", "nba"];
this.data.push(...res);
}, 1000);
}
}
obj.getData();
2
3
4
5
6
7
8
9
10
11
12
如果 getData
也是箭头函数:
var obj = {
data: [],
getData: () => {
setTimeout(() => {
console.log(this)
}, 1000);
}
}
obj.getData();
2
3
4
5
6
7
8
9
10
那么
this
会从上层作用域找,找到全局作用域。所以为window
。