javascript中的this指向(二)

继续上一节内容

1、判断 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();

2、绑定例外

规则总有例外,这里也一样。在某些场景下 this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

1、被忽略的 this

如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、 apply 、或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

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

var a = 2;
foo.call(null);//2

那么什么情况下你会传入 null 呢?

一种非常常见的做法是使用 apply(..) 来“展开”一个数组,并当作参数传入一个函数。类似的,bind(..) 可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:

function foo(a,b){
    console.log("a:"+ a + ",b:"+b);
}
//把数组“展开”成参数
foo.apply(null,[2,3]);//a:2,b:3

//使用 bind(..) 进行柯里化
var bar = foo.bind(null,2);
bar(3);//a:2,b:3

这两种方法都需要传入一个参数当作 this 的绑定对象。如果函数并不关心 this 的话,你任然需要传入一个占位值,这是 null 可能是一个不错的选择,就是代码所示的那样。

然而,总是使用 null 来忽略 this 绑定可能产生一些副作用。如果某个函数确实使用了 this (比如第三方库中的一个函数) ,那默认绑定规则会把 this 绑定到全局对象(在浏览器中这个对象是 window),这将导致不可预计的后果(比如修改全局对象)。显而易见,这种方式可能会导致许多难以分析和追踪的 bug。

2、更安全的 this

一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对你的程序产生任何副作用。如果我们在忽略 this 绑定时总是传入一个空对象,那就什么都不用担心了,因为任何对于 this 的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

由于这个对象完全是一个空对象,在JavaScript 中创建一个空对象最简单的方法都是 Object.create(null)。Object.create(null) 和 {} 很像,但是并不会创建 Object.prototype 这个委托,所以它比 {} “更空”:

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
//我们的空对象
var $n = Object.create(null);

//把数组展开成参数
foo.apply($n,[2,3);//a:2,b:3

//使用bind(..)进行柯里化
var bar = foo.bind($n,2);
bar(3);//a:2,b:3

2、间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这中情况下,调用这个函数会应用默认绑定规则。

间接引用最容易在赋值时发生:

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

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定。

注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式, this 会绑定到 undefined,否则 this 会被绑定到全局对象。

3、软绑定

之前我们已经看到过,硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。

如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。

可以通过一种被称为软绑定的方法来实现我们想要的效果:

if(!Function.prototype.softBind){
    Function.prototype.softBind = function(obj){
        var fn = this;
        //捕获所有的 curried 参数
        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(..) 的其他原理和 ES5 内置的 bind(..) 类似。它会对指定的函数进行封装,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把指定的默认对象 obj 绑定到 this,否则不会修改 this。此外,这段代码还支持可选的柯里化(详情查看之前的和 bind(..) 相关的介绍)。

下面我们看看 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    <-----look

fooOBJ.call(obj3); // name:obj3   <-----look

setTimeout(obj2.foo,10);
//name:obj   <-----应用了软绑定

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

4、this 词法

我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。

箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不适用 this 的四种标准规则,而是根据外层(函数或者全局)作用于来决定 this。

我们来看看箭头函数的词法作用域:

function foo(){
    //返回一个箭头函数
    return (a)=>{
        //this 继承自foo()
        console.log(this.a);
    };
}

var obj1 = {
    a:2
};
var obj2 = {
    a:3
};

var bar = foo.call(boj1);
bar.call(obj2); // 2 ,不是 3

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

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo(){
    setTimeout(()=>{
        //这里的 this 在词法上继承自 foo()
        console.log(this);
    },1000)
}

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

箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo(){
    var self = this; //lexical capture of this
    setTimeout(function(){
        console.log(self.a);
    },1000)
}

var obj={
    a:2
}

foo.call(obj);//2

虽然 self = this 和箭头函数看起来都可以取代 bind(..) ,但是从本质上来说,它们想替代的是 this 机制。

如果你经常编写 this 风格的代码,但是绝大部分都会使用 self = this 或者箭头函数来否定 this 机制,那你或许应当:

1.只使用词法作用域并完全抛弃错误 this 风格代码;

2.完全采用 this 风格,在必要的时候使用 bind(..),尽量避免使用 self = this 和箭头函数。

当然,包含这两种代码风格的程序可以正常运行,但是在同一个函数或者通同一个程序中混合使用这两种风格通常会使代码更难维护,并且可能也会更难编写。

总结:

this 绑定的四条规则

1.默认绑定

独立函数调用时,this 指向全局对象(window),如果使用严格模式,那么全局对象是无法使用默认绑定,this 绑定至 undefined

2.隐式绑定

函数 this 是指向调用者(隐式指向)

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
 
obj.foo();//2
obj.obj2.foo()//foo 中的 this 与 obj2 绑定

问题:隐式丢失

描述:隐式丢失值得是函数中的 this 丢失绑定对象,即他会应用第 1 条的默认绑定规则,从而将 this 绑定到全局对象上或者 undefined 上,取决于是否在严格模式下运行

以下情况会发生隐式丢失:

  • 1.绑定至上下任对象的函数被赋值给一个新的函数,然后调用这个新的函数时
  • 2.传入回调函数时

3.显式绑定

显式绑定的核心是 JavaScript 内置的 call() 和 apply() 方法,call 和 apply bind 的 this 第一个参数(显式指向)

4.new 绑定

构造函数的 this 是 new 之后的新对象即实例对象(构造器)

猜你喜欢

转载自blog.csdn.net/MFWSCQ/article/details/106272702