让人头疼的this

647 阅读6分钟

1、关于this

this是JavaScript中最复杂的机制之一它是一个很特别的关键字,被自定义在所有函数的作用域中。

function identify(){
    return this.name.toUpperCase();
}

function speak(){
    var greeting = "Hello,I am "+identify.call(this)
    console.log(greeting)
}
var me={
    name:"Good"
}
var you={
    name:"Morning"
}
identify.call(me)
identify.call(you)

speak.call(me)
speak.call(you)

this提供了一种优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。

this既不指向函数自身,也不指向函数的词法作用域,this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

2、绑定规则

2.1默认绑定

最常用的函数调用类型:独立函数调用。其实这条规则是无法应用其他规则时的默认规则。

function foo(){
    console.log(this.a)
}

var a=2;

foo();// 2

首先,声明在全局作用域中的变量就是全局对象的一个同名属性,它们本质上就是同一个东西。当调用foo()时,this.a被解析成了全局变量a。在函数调用时应用了this的默认绑定,因此this指向全局对象。

如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到undefined:

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

当foo()运行在非strict mode 下时,默认绑定才能绑定到全局对象;在严格模式下调用foo()则不影响默认绑定

2.2隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

function foo(){
    console.log(this.a)
}

var obj={
    a:2,
    foo:foo
};
obj.foo();// 2

foo()的声明方式,及之后被当做引用属性添加到obj中都表明这个函数都不属于obj对象。然而调用位置会使用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或者“包含”函数引用。

2.3显式绑定

显示绑定是使用函数的call(..)和apply(..)方法,严格来说,JavaScript的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript提供的绝大多数函数以及你自己创建的所有函数都可以使用这两个方法。它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为可以直接指定this的绑定对象,所以成为显式绑定。

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

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

2.3.1硬绑定

function foo(something){
    console.log(this.a,something);
    return this.a+something;
}
var obj={
    a:2
}
var bar=function(){
    return foo.apply(obj,arguments)
}
var b=bar(3);// 2  3
console.log(b); //5

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

2.3.2软绑定

其实软绑定并不属于显式绑定规则,放在这里只是为了和硬绑定更好的进行比较。

硬绑定把this强制绑定到指定的对象上,防止函数调用应用默认绑定规则。但是硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this。

如果可以给默认绑定指定一个全局对象和undefined以外的值(如果把null或undefined作为this的显示绑定对象,这些值在调用时会被忽略),那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。

可以通过软绑定的方法来实现:

if(!Function.prototype.softBind){
    Function.prototype.softBind=function(obj){
        var fn=this;
        //捕获所有curride 参数
        var curried=[].slice.call(arguments,1);
        var bound = function(){
            return fn.apply(
                (!this || this === (window || global))?obj:this,
                curried.concat.apply(curried,arguments)
            )
        }
        bound.prototype=Object.create(fn.prototype)
        return bound;
    }
}

softBind(..)和bind(..)类似,它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改。

下面这段代码是softBind实现软绑定功能的示例:

function foo(){
    console.log("name:"+this.name);
}
var obj={name:"obj"},
    obj2={name:"obj2"},
    obj3={name:"obj3"};
var fooOBJ=foo.softBind(obj);
fooOBJ();  // name:obj
obj2.foo=foo.softBind(obj);
obj2.foo(); // name:obj2
fooOBJ.call(obj3);//name:obj3
setTimeout(obj2.foo,10);//name:obj 这里应用了软绑定

软绑定的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定规则,则会将this绑定到obj。

2.4new绑定

JavaScript中的new机制和面向类的语言完全不同。

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。

使用new来调用函数时,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行[[Prototype]]链接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
    this.a=a;
}
var bar = new foo(2);
console.log(bar.a)// 2

3、优先级

上面的绑定规则中,默认绑定是优先级最低的绑定规则。而显式绑定的优先级要高于隐式绑定。

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

而new绑定与显式绑定谁的优先级更高呢?调用new绑定时会创建新的对象,这样new绑定会使用新创建的this替换硬绑定的this。

function foo(something){
    this.a=something
}
var obj1={}
var bar=foo.bind(obj1)
bar(2)
console.log(obj1.a)// 2
var baz=new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) //3

我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则,从而来判断this:

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

4、箭头函数的this

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

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

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

如果由不对的地方还请大佬指教,您的建议是我学习途中最宝贵的经验,谢谢指教!!!