this指针的理解

427 阅读4分钟

js中有四条绑定规则来确定 this 的绑定对象。

普通函数的this指向是调用它的那个对象

假设我们已经找到函数的被调用位置,我们还要确定用下面四条绑定规则中的哪一条,来确定 this 的绑定对象。在这里,先分别解释这四条规则,然后解释多条规则都可用时它们的优先级如何排列。

第一条规则:默认绑定

1、默认绑定下 this 会指向全局对象

function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
foo(); // 2

2、但是如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,this 会绑定到 undefined,因此以上的代码会报错:

"use strict";
function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
foo(); // TypeError: this is undefined

3、但是如果我们显式地用 window 调用 foo 函数,则以上代码不会报错:

"use strict";
function foo() {     
    console.log( this.a ); 
} 
var a = 2; 
window.foo(); // 2

这是因为我们应用了第二条规则——隐式绑定

第二条规则:隐式绑定

如果一个函数中有 this ,这个函数有被上一级的对象所调用,那么 this 指向的就是上一级的对象;this 是在运行时被确定,而不是在定义时被确定

function foo() {
    console.log( this.a ); 
} 
var obj = {
    a: 2,     
    foo: foo 
};
obj.foo(); // 2

2、this 指向的是被调用方法的上一级对象,而不是它的最外层对象,

function foo() {
    console.log( this.a ); 
} 
var obj2 = {
    a: 22,     
    foo: foo 
}; 
var obj1 = {     
    a: 12,     
    obj2: obj2 
}; 
obj1.obj2.foo(); // 22

3、this 是在运行时被确定,而不是在定义时被确定,

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var bar = obj.foo; // 函数别名! 
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"

4、在方法的参数中传入函数时也需要特别注意,传入函数的 this 也指向其方法被调用的上一级对象

function foo() {
    console.log( this.a ); 
}
function doFoo(fn) {     // fn其实引用的是foo     
    fn(); // <-- 调用位置! 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"  

再例如:

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

在上面的的代码片段中,有时候我们就想打印 obj 中的 a 属性,这时候我们应该怎么修改呢?

这就需要我们应用第三条规则——显式绑定

第三条规则:显式绑定

以上的代码可以如下修改来访问到 obj 中的 a 属性:

function foo() {
    console.log( this.a ); 
} 
var obj = {     
    a: 2,     
    foo: foo 
}; 
var a = "oops, global"; // a是全局对象的属性
setTimeout( obj.foo.bind(obj), 100 ); // 2

这个代码片段中用了 bind() 方法来显式修改 this 的指向,与 bind() 方法有类似功能的还有 call() 方法和 apply() 方法,他们都可以改变 this的指向; 但是它们之间也有重要的区别:bind() 是返回对应函数,便于稍后调用;call() 、apply() 则是立即调用 。

第四条规则:new绑定

function foo(a) {
    this.a = a; 
} 
var bar = new foo(2);
console.log( bar.a ); // 2

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

创建(或者说构造)一个全新的对象。 将构造函数的作用域赋值个新对象(因此this就指向了这个新对象) 执行构造函数中的代码 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

function fn()  
{  
    this.user = '追梦子';  
    return {};  
}
var a = new fn;  
console.log(a.user); //undefined

function fn()  
{  
    this.user = '追梦子';  
    return function(){};
}
var a = new fn;  
console.log(a.user); //undefined

function fn()  
{  
    this.user = '追梦子';  
    return 1;
}
var a = new fn;  
console.log(a.user); //追梦子

function fn()  
{  
    this.user = '追梦子';  
    return undefined;
}
var a = new fn;  
console.log(a.user); //追梦子

也就是说:如果返回值是一个对象,那么 this 指向的就是那个返回的对象,如果返回值不是一个对象那么 this 还是指向函数的实例。

三、四条绑定规则的优先级

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

1、 函数是否在 new 中调用(new绑定)?如果是的话this绑定的是新创建的对象。

var bar = new foo()

2、 函数是否通过 call、apply、bind(显式绑定)调用?如果是的话,this 绑定的是指定的对象。

var bar = foo.call(obj)

3、函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。

var bar = obj.foo()

4、如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到全局对象。

var bar = foo()

四、箭头函数里的this

箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this 。

function foo() {      // 返回一个箭头函数     
    return () => {   //this继承自foo()       
        console.log( this.a );     
    }
} 
var obj1 = {
    a:2 
}; 
var obj2 = {
    a:3 
}; 
var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 2, 不是3!

如果将箭头函数换为普通函数,则打印的是3:

function foo() {   
    return function () {       
        console.log( this.a );     
    }
} 
var obj1 = {
    a:2 
}; 
var obj2 = {
    a:3 
}; 
var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 3

箭头函数与普通函数的区别

箭头函数只能用赋值式写法,不能用声明式写法

const test = (name) => {
    console.log(name)
}
test('Jerry')

如果参数只有一个,可以不加括号,如果没有参数或者参数多于一个就需要加括号

const test = name => {
    console.log(name)
}
test('Jerry')

const test2 = (name1, name2) => {
    console.log(name1 + ' and ' + name2)
}
test2('Tom', 'Jerry')

如果函数体只有一句话,可以不加花括号

const test = name => console.log(name) 

如果函数体没有括号,可以不写return,箭头函数会帮你return

const add = (p1, p2) => p1 + p2
add(10, 25)

记住:函数体的花括号与return关键字同在。

从以上的例子我们可以看出,箭头函数对常规函数的圆括号和花括号都进行了简化。除了这些简化,箭头函数对于常规函数最大的优化之处在于this。

this是使用call方法调用函数时传递的第一个参数,它可以在函数调用时修改,在函数没有调用的时候,this的值是无法确定。

默认绑定外层this

this的值是可以用call方法修改的,而且只有在调用的时候我们才能确定this的值。而当我们使用箭头函数的时候,箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的。

const obj = {
	a: function() { console.log(this) }    
}
obj.a()  //打出的是obj对象

const obj = {
    a: () => {
        console.log(this)
    }
}
obj.a()  //打出来的是window

在使用箭头函数的例子里,因为箭头函数默认不会使用自己的this,而是会和外层的this保持一致,最外层的this就是window对象。

不能用call方法修改里面的this

这个也很好理解,我们之前一直在说,函数的this可以用call方法来手动指定,而为了减少this的复杂性,箭头函数无法用call方法来指定this。

const obj = {
    a: () => {
        console.log(this)
    }
}
obj.a.call('123')  //打出来的结果依然是window对象

因为上文我们说到window.setTimeout()中函数里的this默认是window,我们也可以通过箭头函数使它的this和外层的this保持一致:

const obj = {
    a: function() {
        console.log(this)
        window.setTimeout(() => { 
            console.log(this) 
        }, 1000)
    }
}
obj.a.call(obj)  //第一个this是obj对象,第二个this还是obj对象

想必大家明白了,函数obj.a没有使用箭头函数,因为它的this还是obj,而setTimeout里的函数使用了箭头函数,所以它会和外层的this保持一致,也是obj;如果setTimeout里的函数没有使用箭头函数,那么它打出来的应该是window对象。