# 细谈this问题
# 关于this对象
在《JavaScript高级程序设计》一书中说明了this
对象是运行时基于函数的执行环境绑定的:在全局函数中,this
等于window
,而当函数被作为某个对象的方法调用时,this
等于那个对象。
不过,匿名函数的执行环境具有全局性,因此其this
对象通常指向window
。
var name = 'lin jia heng'
var object = {
name: 'My Object',
getNameFunc: function () {
return function () {
return this.name; //匿名函数的执行环境具有全局性,所以this指向window
}
}
}
alert(object.getNameFunc()()) // lin jia heng(非严格模式)
var object = {
name: 'My Object',
getNameFunc: function () {
console.log(this.name) // My Object
}
}
object.getNameFunc()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
而下面的例子就可以成功返回“My Object”
var name = 'lin jia heng'
var object = {
name: 'My Object',
getNameFunc: function () {
return this.name; //当函数被作为某个对象的方法调用时,this等于那个对象
}
}
alert(object.getNameFunc()) // My Object
var name = 'lin jia heng'
var object = {
name: 'My Object',
getNameFunc: function () {
let that = this // 改变this指向
return function () {
return that.name;
}
}
}
alert(object.getNameFunc()()) // My Object
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 在函数中执行
在函数中,
this
永远指向最后调用函数的那个对象。
- 在非严格模式(默认绑定)
function func(){
console.log(this)
}
func() //Window
2
3
4
- 在严格模式(默认绑定)
function func(){
console.log(this)
}
func() //undefined
2
3
4
这就验证了第一个例子中为什么特意表明在非严格模式下才成立了。
var me = {
name: 'xiuyan',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var you = {
name: 'xiaoming',
hello: function() {
var targetFunc = me.hello
targetFunc()
}
}
var name = 'BigBear'
// 调用位置
you.hello()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
targetFunc
调用时没有为他指明任何对象前缀,js引擎仍然会认为targetFunc
是一个挂载在window
上的方法,进而把this
指向window
对象。
# 作为一个构造函数使用
在JS中,为了实现类我们需要定义一些构造函数,在调用一个构造函数的时候加上new
这个关键字:
function Person(name) {
this.name = name;
console.log(this)
}
var p1 = new Person('kk');
// Person
2
3
4
5
6
此时this
指向这个构造函数调用的时候实例化出来的对象,如果构造函数显示return
返回一个值,且返回的是一个对象(返回复杂类型),那么this
就指向这个返回的对象,如果返回就是this
那么也是指向当前实例化对象,因为构造函数中的this
本来就代表当前实例化对象;如果return
一个基本数据类型的值,则this
仍然指向实例出来的对象。
当然,构造函数其实也是一个函数,如果将构造函数当成普通函数来调用,this
指向Window
function Person(name) {
this.name = name;
console.log(this)
}
var p1 = Person('kk');
// Window
2
3
4
5
6
# 在定时器中使用
setTimeout(function() {
console.log(this);
},0)// Window,setInterval也一样
2
3
如果没有特殊指向,定时器的回调函数中this
的指向都是Window
。这是因为JS的定时器方法是定义在Window
下的。
# 在箭头函数中使用
箭头函数的绑定无法被修改。箭头函数的
this
为父作用域的this
,不是调用时的this
。箭头函数的this
指向是静态的,在声明的时候就已经确定了。箭头函数中的this
指向是由其所属函数或全局作用域决定的。
- 在全局环境中使用:
var func = () => {
console.log(this)
}
func() //Window
2
3
4
- 作为一个对象的一个函数使用:(隐式绑定)
var obj = {
name: 'hh',
func: function() {
console.log(this);
}
}
obj.func();//obj
var obj = {
name: 'hh',
func: () => {
console.log(this)
}
}
obj.func(); //Window
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 作为对象的特殊情况,结合定时器来使用:
var obj = {
name: 'hh',
func: function() {
setTimeout(function() {
console.log(this);
},0)
}
}
obj.func();// Window
var obj = {
name: 'hh',
func: function() {
setTimeout(() => {
console.log(this);
},0)
}
}
obj.func();// obj
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
setTimeout(() => {
console.log('id:',this.id) // id:42
},100)
}
var id = 21;
foo.call({id:42})
2
3
4
5
6
7
箭头函数中的
this
的值取决于该函数外部非箭头函数的this
的值,否则this
的值会被设置为全局对象Window
,且不能通过call()
,apply()
和bind()
方法来改变this
的值。--《深入理解ES6》。
# 赋值给另外一个变量进行调用
var name = "windowsName";
var a = {
name: null,
fn: function() {
console.log(this.name); //windowsName
}
}
var f = a.fn;
f();
2
3
4
5
6
7
8
9
fn()
最后仍然是被window
调用的,所以this
指向的也是window
。this
的指向并不是在创建的时候就确定并一成不变的,在es5中this
永远指向最后调用它的那个对象。
'use strict' //严格模式下
let user = {
name: 'John',
hi(){alert(this.name);},
bye() {alert('Bye');}
};
user.hi();//John
(user.name == 'John' ? user.hi : user.bye)();// this指向undefined,所以此处报错,此处的函数user.hi没有加上括号进行立即执行,有点类似于上个例子的赋值变量进行调用
2
3
4
5
6
7
8
# 改变this的指向
- 像上述部分例子一样使用箭头函数
- 在函数内部定义一个变量
_this = this
- 使用
apply
,call
,bind
(显示绑定) new
实例化一个对象(new
绑定)
# 使用_this = this
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
var _this = this;
setTimeout( function() {
_this.func1()
},100);
}
};
a.func2() // Cherry
2
3
4
5
6
7
8
9
10
11
12
13
14
在
func2
中,首先设置var _this = this
;,这里的this
是调用func2
的对象a
,为了防止在func2
中的setTimeout
被window
调用而导致的在setTimeout
中的this
为window
。我们将this
(指向变量a
) 赋值给一个变量_this
,这样,在func2
中我们使用_this
就是指向对象a
了。
# 使用apply,call,bind(显示绑定)
# bind
如果你想将某个函数绑定新的
this
指向并且固定传入几个变量可以在绑定的时候就传入,之后调用新函数传入的参数都会排在后面。不管我们给函数bind
几次,fn
中的this
永远由第一次bind
决定
const obj = {}
function test(...args){
console.log(this === obj)//true, 因为bind方法把this指向了obj对象
console.log(args) // ['lin','jiaheng,'linjiaheng']
}
const newFn = test.bind(obj, 'lin', 'jiaheng')//因为bind返回一个新函数,所以要赋值给一个新变量
newFn('jiahenglin')
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)() // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
因为bind返回一个新函数,所以要加多一个括号手动调用它。
# call
需要注意的是,指定的
this
并不一定是该函数执行时真正的this
。如果这个函数处于非严格模式下,则指定为null
和undefined
的this
值会自动指向全局对象(window
),同时值为原始值(数学,字符串,布尔值)的this
会指向该原始值的自动包装对象【理解为 如果你传入一个原始值(字符串,布尔类型或者数字类型)来当作this
的绑定对象,这个原始值就会被转换成它的对象形式(也就是new String(...)
,new Boolean(...)
或者new Number(...)
,这通常称为装箱】。
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1() // 没用利用call改变this的指向的话,这里的this就是Window
}.call(a),100);
}
};
a.func2() // Cherry
2
3
4
5
6
7
8
9
10
11
12
# apply
第一个参数和
call
的一样,第二个参数一定是传入一个数组格式的,最终调用函数时候这个数组会拆成一个一个参数传入
因为apply函数传参的特性,所以可以实现一个全是数字的数组找出最大最小值的效果
const arr = [1,2,3,4,45,43,22]
const max = Math.max.apply(null,arr)
console.log(max)//45
// 也可以使用ES6的拓展运算符
console.log(Math.max(...arr))
2
3
4
5
# bind,call,apply进阶例子
- 循环中利用闭包来处理回调
for(var i=0;i<10;i++){
(function(j){
setTimeout(function(){
console.log(j)
},600)
})(i)
}
2
3
4
5
6
7
每次循环都会产生一个立即执行的函数,函数内部的局部变量j保存不同时期i的值,循环过程中,
setTimeout
回调按顺序放入事件队列中,等for
循环结束后,堆栈中没用同步的代码,就去事件队列中,执行对应的回调,打印出j的值
同理可以利用bind
,每次都创建新的函数,并且已经预先设置了参数
function func(i){
console.log(i)
}
for(var i=0;i<10;i++){
setTimeout(func.bind(null,i),600)
}
2
3
4
5
6
- 实现继承
var Person = function(name,age){
this.name = name;
this.age = age;
}
var P1 = function(name,age) {
//借用构造函数方式实现继承
//利用call继承了Person
Person.call(this,name,age)
}
P1.prototype.getName = function(){
console.log('name:'+this.name+',age:'+this.age);
}
var newPerson = new P1('popo',20);//name:popo,age:20
newPerson.getName();
2
3
4
5
6
7
8
9
10
11
12
13
14
- 实现硬绑定
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function() {
foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar不可能再修改它的this
bar.call(window); // 2
2
3
4
5
6
7
8
9
10
11
12
13
我们创建了函数
bar()
,并在它的内部手动调用了foo.call(obj)
,因此强制把foo
的this
绑定到了obj
。无论之后如何调用函数bar
,它总会手动在obj
上调用foo
。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
# call apply bind
三者联系
共同点:
- 三者都能改变
this
的指向,且第一个参数都是this
指向的对象 - 三者都采用后续传参的方式
- 三者都能改变
不同点:
call
的传参方式是单个传递,数组也可以。apply
则是传递数组,bind
没有规定
var obj = { name: '小鹿', age: 22, address: '小鹿学动画' } function print() { console.log(this) console.log(arguments) // 单个传或者数组形式传结果如下 } print.call(obj, 1,2,3);
1
2
3
4
5
6
7
8
9
10call/apply
是直接执行,bind
返回一个函数
数组形式
单个传递形式
# this
优先级
function foo(a) {
console.log(this.a)
}
const obj1 = {
a:1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2)
obj2.foo.call(obj1)
2
3
4
5
6
7
8
9
10
11
12
13
输出分别为
2,1
,也就是说call,apply
的显式绑定一般优先级更高。
function foo(a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
2
3
4
5
6
7
8
上述代码通过
bind
将bar
函数中的this
绑定为obj1
对象。执行bar(2)
后,obj1.a
值为2
,即执行bar(2)
后,obj1
对象为{a:2}
。当再使用bar
作为构造函数时,执行如下代码输出3
var bar = new bar(3)
console.log(baz.a)
2
bar
函数本身是通过bind
方法构造函数,其内部已经将this
绑定为obj1
,当它再次作为构造函数通过new
调用时,返回的实例就已经和obj1
解绑了。也就是说,new
绑定修改了bind
绑定中的this
指向,因此new
绑定的优先级比显式bind
绑定的更高。
function foo() {
return a => {
console.log(this.a)
}
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1)
console.log(bar.call(obj2))
2
3
4
5
6
7
8
9
10
11
12
13
14
以上代码输出
2
。由于foo
中的this
绑定到了obj1
上,所以bar
(引用箭头函数)中的this
也会绑定到obj1
上,箭头函数的绑定无法被修改。
# 总结
- 对于没有挂载在任何对象上的函数,在非严格模式下
this
就是指向window
的。 - 匿名函数的
this
永远指向window
。 - 不要根据
this
的英文语法角度错误理解成指向函数自身。 - 当一个函数被调用时,会创建一个活动记录(也可以成为执行上下文),
this
就是这记录的一个属性。,所以this
指向什么完全取决于函数在哪里被调用。
# 补充
根据《你不知道的JavaScript上卷》一书中写的:可以根据优先级来判断函数在某个调用位置应用的是哪条
this
指向规则(在箭头函数下无效)
- 函数是否在
new
中调用,如果是的话this
绑定的是新创建的对象。 - 函数是否通过显示绑定或者硬绑定调用,如果是的话,
this
指向指定对象 - 函数是否在某个上下文中调用(隐式绑定),如果是的话,
this
指向那个上下文对象 - 如果都不是,使用默认绑定,严格模式下就指向
undefined
,否则绑定到全局对象。
TIP
下面三种特殊情况,this
会100%
指向window
:
- 立即执行函数(
IIFE
) setTimeout
中传入的函数setInterval
中传入的函数
# this
的题目
// 声明位置
var me = {
name: 'xiuyan',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var you = {
name: 'xiaoming',
hello: me.hello
}
// 调用位置
me.hello() // xiuyan
you.hello() // xiaoming
var me = {
name: 'xiuyan',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var name = 'BigBear'
var hello = me.hello
// 调用位置
me.hello() // xiuyan
hello() // BigBear
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
this
在箭头函数中类似词法作用域,由书写位置决定
var a = 1;
var obj = {
a: 2,
func2: () => {
console.log(this.a)
},
func3: function() {
console.log(this.a)
}
}
// func1
var func1 = () => {
console.log(this.a)
}
// func2
var func2 = obj.func2
// func3
var func3 = obj.func3
func1()
func2()
func3()
obj.func2()
obj.func3()
//1,1,1,1,2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let obj = {
a:20,
b:this.a + 10,
say:function() {
return(this.a + 10)
}
}
console.log(obj.b);//?
console.log(obj.say(),obj.say)//?
// NaN
// 30 function(){
// return this.a + 10
//}
2
3
4
5
6
7
8
9
10
11
12
13
var a = 123
const foo = () => a => {
console.log(this.a)
}
const obj1 = {
a:2
}
const obj2 = {
a:3
}
var bar = foo.call(obj1)
console.log(bar.call(obj2))
2
3
4
5
6
7
8
9
10
11
12
代码会输出
123
。如果代码第一行是const a =123
则输出undefined
,因为const
定义的变量没有挂载到全局对象上。
window.data = 5
var foo = {
data: 6,
click() {
console.log(this.data)// undefined
}
}
let dom = document.getElementsByClassName('test')[0]
dom.addEventListener('click',foo.click)
2
3
4
5
6
7
8
9
因为事件处理程序中
this
指向绑定的dom
对象,所以打印undefined
。要打印6
可以改成dom.addEventListener('click',foo,click())
。打印5
则改为箭头函数。