this指向原则透析

210 阅读5分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

smalld9fcb449fa428b1cc001b40527b990761622906649.jpg

1.为什么要用this?

  • this提供了一种更优雅的方式来隐式传递 一个对象引用。
function identify() {
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identify.call( this );
    console.log( greeting );
}
var me = {
    name: "Kyle"
};
var you = {
    name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READE

这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。

如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是 KYLE

2. this四条绑定规则

this 关键字, 被自动定义在所有函数的作用域中。

注意:this的指向在函数创建时确定不了,被调用时才能确定。

我们在找this指向时,要先找到调用位置,然后判断符合哪一条绑定规则,同时注意绑定规则的优先级。

2.1 默认绑定

在找不到函数的调用对象时,

  • 非严格模式:函数中this会指向全局对象window。
  • 严格模式下,函数中this会指向 undefined。

注意: 对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式

例1:函数调用前面没有对象

function sayHi(){
    console.log('Hello,', this.name);
}
    var name = 'YvetteLau';
    sayHi();
    
// 'Hello,' YvetteLau

2.2 隐式绑定

通过对象调用函数,那么函数中this会指向最后调用它的对象。

隐式丢失

1.将obj.fn()赋值给某个变量

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

如下示例, o.foothis 隐式绑定在了 o 对象上, 而 bar 引用了 o.foo 函数本身, 所以此时的 bar() 其实是一个不带任何修饰的函数调用, 因此使用了 默认绑定 规则:

var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}
​
var bar = o.foo
​
o.foo() // 1
bar()   // undefined

2.将obj.fn()赋值给函数的形参

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。示例中, bar(o.foo) 实际上采用了隐式赋值: callback = o.foo, 事实上跟上面的例子一样, 都是直接引用了 o.foo 函数本身, 所以造成了 隐式丢失:

function bar(callback) {
    callback()
}
​
var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}
​
bar(o.foo) // undefined

3.setTimeOut函数中的this

例2:setTimeOut中包含的函数,在后面执行时,并没有对象去调用这个函数,所以也是默认绑定。

btn.onclick = function(){
    setTimeout(function(){
        console.log(this);
    },0)
}
btn.onclick();

// window

虽然onclick函数是btn对象调用的,但setTimeout函数中的内容并没有在btn对象调用onclick函数时立即执行,等到setTimeout函数执行时,setTimeout函数找不到调用对象,这时变为默认绑定,非严格模式下,setTimeout函数内的this指向window

(setTimeOut任务是宏任务,会在下一轮事件循环执行。具体参见:juejin.cn/post/693532…)

2.3 显示绑定

我们有时想要强制为某个函数绑定this,可以通过使用 call()apply() 方法来实现;

函数.call(要绑定this的对象, 参数1, 参数2, ...)

函数.apply(要绑定this的对象, [参数1, 参数2, ...])

硬绑定

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

我们创建了函数 bar(),并在它的内部手动调用 了 foo.call(obj) ,因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它 总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

硬绑定ES5API

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

硬绑定时传入null/undefined,则会忽略本次硬绑定,采用默认绑定规则。

2.4 new绑定

当使用 new 操作符调用某个函数时,这个函数被构造调用

new 操作符在调用函数时做的工作:

  • 创建或构造了一个全新的对象
  • 这个新对象会被执行[[原型]]连接
  • 函数中的 this 会指向这个新对象
  • 如果被调用的函数没有返回(return), 则 new 表达式中的函数调用会自动返回这个新对象

3. 绑定规则的优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

判断this:

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

4. 箭头函数

箭头函数无法使用上述规则。根据根据外层(函数或者全局)作用域来决定 this。

箭头函数的this指向它外层作用域调用时的this。

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

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

箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。

参考:

你不知道的JavaScript