变化莫测的"this"

370 阅读5分钟

this到底是什么

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

this是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

this的绑定四大规则

一、默认绑定:独立函数调用

  • 默认绑定:无法应用其他规则时使用默认绑定。非严格模式下默认绑定全局对象(windows),严格模式下绑定undefined

  • 独立函数调用:指在调用函数时,前面不带任何修饰符

<!--独立函数调用-->
function foo(){}
foo()   //独立函数调用
<!--非独立函数调用-->
var obj = {
    foo:foo
}
obj.foo()  //前面带了修饰符,非独立函数调用
function foo(){
    console.log(this.a)   //2
}
var a = 2
foo()
//严格模式下
function foo(){
    "use strict"
    console.log(this.a)   //TypeError
}
var a = 2
foo()   //调用位置 windows

二、隐式绑定

必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上,也是通常所说的上下文对象

function foo(){
    console.log(this.a)
}
var obj1 = {
    a:42,
    <!--直接定义foo-->
    foo:function foo(){
        console.log(this.a)
    }
}

var obj2 = {
    a:2,
    <!--foo被当作引用属性添加-->
    foo:foo,
    obj1:obj1
}
var a = "global"
<!--隐式绑定-->
obj1.foo()  // 42=>this=>obj1  foo调用位置 obj1
obj2.foo()   // 2=>this=>obj2  foo调用位置 obj2
var bar = obj1.foo
var bar2 = obj2.foo
<!--不带任何修饰的函数调用,因此应用默认绑定-->
bar()  //global=>this=>windows  foo调用位置windows
bar2() //global=>this=>window  foo调用位置windows

obj2.obj1.foo()  // 42=>this=>obj1   foo调用位置obj1

1.无论foo()的声明方式是被当作引用属性添加到obj中,还是直接在obj中定义的,严格来说foo()函数都不属于obj对象 2.对象属性引用链中只有最顶层或者说最后一层会影响调用位置

思考题1:在上述的例子中,是否对于执行bar(),foo的this指向windows存在疑问,也就是我们常说的this丢失绑定问题,我们又该如何让this指向obj1呢?请继续往下看

三、显式绑定

所有函数都可以使用call(...)和apply(...)方法,他们的第一个参数是一个对象,他们会把这个对象绑定到this,接着在调用时指定这个this。因为我们可以直接指定this的绑定对象,因此称之为显式绑定

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

显式绑定的变化方法

1.硬绑定

就是为了解决之前提出的丢失绑定问题

function foo(){
    console.log(this.a)
}
var obj = {
    a:2
}
// 硬绑定
var bar = function(){
    foo.call(obj)  //显式的将foo的this绑定为 obj
}
var a = "global"
bar() //2
setTimeout(bar,100) //2
//硬绑定的bar不可能再修改它的this
bar.call(windows)  //2

2.API调用“上下文”

第三方库的许多函数,以及Javascript语言和宿主环境中许多内置函数,都提供可选的参数,通常被称为“上下文(context),其作用和bind(...)一样,确保回调函数使用指定的this”

function foo(el){
    console.log(el,this.id)
}
var obj = {
    id:'test'
}
[1,2,3].forEach(foo,obj)
//1 test 2 test 3 test

四、new绑定

使用new时,我们会构造一个新对象并把他绑定到构造函数调用中的this上

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

四种绑定的优先级

new 绑定>通过call、apply(显式绑定)>隐式绑定>默认绑定

1.比较隐式绑定与显式绑定

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

2.比较显示绑定与new绑定

function foo(a){
    this.a = a
}
var obj1 = {}
<!--bar中this指向obj1-->
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)  //2
<!--bar 中this指向baz-->
var baz = new bar(3)
console.log(obj1.a)  //2
console.log(baz.a)  //3

new操作修改了硬绑定(到obj1的)调用bar()中的this

判断this

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

5.例外情况

  • 将null或者undefined作为this的绑定对象传入call、apply或者bind,应用默认绑定规则
function foo(){
    console.log(this.a)
}
var a = 2
foo.call(null)  //2 this=>windows
  • 函数的“间接引用”,调用这个函数应用默认绑定规则
function foo(){
    console.log(this.a)
}
var a = 2
var o = {
    a:3,
    foo:foo
}
var p = {
    a:4
}
o.foo()   // 3
(p.foo = o.foo)()  //2

特殊情况

ES6中的箭头函数无法使用上述的四种规则,而是根据外层(函数或者全局)作用域来决定this

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


<!--由于 foo() 的 this 绑定到 obj1,bar(引用箭头函数)的 this 也会绑定到obj1,箭头函数的绑定无法被修改。(new 也不行!)-->

敲重点

运行中函数的this到底指向哪里?找到这个函数的直接调用位置

  • 由new调用?指向新创建的对象。
  • 由call或者apply(或者bind)调用?指向绑定的对象
  • 由上下文对象调用?指向那个上下文对象
  • 默认(独立函数调用)?在严格模式下指向undefined,非严格模式下指向全局对象
  • 箭头函数:指向外层函数调用this的绑定

参考文献

我只是书本的搬运工,以上内容来自《你不知道的Javascript(上卷)》

感谢大家的阅读!!!