# 细谈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()
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 在函数中执行

在函数中,this永远指向最后调用函数的那个对象。

  1. 在非严格模式(默认绑定)
function func(){
    console.log(this)
}
func() //Window
1
2
3
4
  1. 在严格模式(默认绑定)
function func(){
    console.log(this)
}
func() //undefined
1
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()
1
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
1
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
1
2
3
4
5
6

# 在定时器中使用

setTimeout(function() {
    console.log(this);
},0)// Window,setInterval也一样
1
2
3

如果没有特殊指向,定时器的回调函数中this的指向都是Window。这是因为JS的定时器方法是定义在Window下的。

# 在箭头函数中使用

箭头函数的绑定无法被修改。箭头函数的this为父作用域的this,不是调用时的this。箭头函数的this指向是静态的,在声明的时候就已经确定了。箭头函数中的this指向是由其所属函数或全局作用域决定的。

  1. 在全局环境中使用:
var func = () => {
    console.log(this)
}
func() //Window
1
2
3
4
  1. 作为一个对象的一个函数使用:(隐式绑定)
var obj = {
    name: 'hh',
    func: function() {
        console.log(this);
    }
}
obj.func();//obj

var obj = {
    name: 'hh',
    func: () => {
        console.log(this)
    }
}
obj.func(); //Window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 作为对象的特殊情况,结合定时器来使用:
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
1
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})
1
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();
1
2
3
4
5
6
7
8
9

fn()最后仍然是被window调用的,所以this指向的也是windowthis的指向并不是在创建的时候就确定并一成不变的,在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没有加上括号进行立即执行,有点类似于上个例子的赋值变量进行调用
1
2
3
4
5
6
7
8

# 改变this的指向

  • 像上述部分例子一样使用箭头函数
  • 在函数内部定义一个变量_this = this
  • 使用applycallbind(显示绑定)
  • 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

func2 中,首先设置var _this = this;,这里的 this 是调用 func2 的对象a,为了防止在 func2 中的 setTimeoutwindow 调用而导致的在 setTimeout 中的 thiswindow。我们将 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

因为bind返回一个新函数,所以要加多一个括号手动调用它

# call

需要注意的是,指定的this并不一定是该函数执行时真正的this。如果这个函数处于非严格模式下,则指定为nullundefinedthis值会自动指向全局对象(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
1
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))
1
2
3
4
5

# bind,call,apply进阶例子

  1. 循环中利用闭包来处理回调
for(var i=0;i<10;i++){
    (function(j){
        setTimeout(function(){
            console.log(j)
        },600)
    })(i)
}
1
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)
}
1
2
3
4
5
6
  1. 实现继承
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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 实现硬绑定
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
1
2
3
4
5
6
7
8
9
10
11
12
13

我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foothis绑定到了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
    10
    • call/apply是直接执行,bind返回一个函数

数组形式

img

单个传递形式

img

# 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)
1
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)
1
2
3
4
5
6
7
8

上述代码通过bindbar函数中的this绑定为obj1对象。执行bar(2)后,obj1.a值为2,即执行bar(2)后,obj1对象为{a:2}。当再使用bar作为构造函数时,执行如下代码输出3

var bar = new bar(3)
console.log(baz.a)
1
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))
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以上代码输出2。由于foo中的this绑定到了obj1上,所以bar(引用箭头函数)中的this也会绑定到obj1上,箭头函数的绑定无法被修改。

# 总结

  1. 对于没有挂载在任何对象上的函数,在非严格模式下 this 就是指向 window 的。
  2. 匿名函数的this永远指向window
  3. 不要根据this的英文语法角度错误理解成指向函数自身。
  4. 当一个函数被调用时,会创建一个活动记录(也可以成为执行上下文),this就是这记录的一个属性。,所以this指向什么完全取决于函数在哪里被调用。

# 补充

根据《你不知道的JavaScript上卷》一书中写的:可以根据优先级来判断函数在某个调用位置应用的是哪条this指向规则(在箭头函数下无效)

  1. 函数是否在new中调用,如果是的话this绑定的是新创建的对象。
  2. 函数是否通过显示绑定或者硬绑定调用,如果是的话,this指向指定对象
  3. 函数是否在某个上下文中调用(隐式绑定),如果是的话,this指向那个上下文对象
  4. 如果都不是,使用默认绑定,严格模式下就指向undefined,否则绑定到全局对象。

TIP

下面三种特殊情况,this100%指向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
1
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
  1. 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
1
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
//}
1
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))
1
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)
1
2
3
4
5
6
7
8
9

因为事件处理程序中this指向绑定的dom对象,所以打印undefined。要打印6可以改成dom.addEventListener('click',foo,click())。打印5则改为箭头函数。