this 绑定机制

this,有 4 种绑定机制,分别对应函数的 4 种调用方式:

  • 默认绑定 — 函数调用模式,this 指向全局对象;               // 被嵌套的函数,独立调用时,默认指向全局对象
  • 隐式绑定 — 方法调用模式,this 指向调用者;
  • new 绑定 — 构造函数调用,this 指向被构造的对象;
  • 显式绑定 — 间接调用模式,this 指向第一个参数;

函数调用 — 默认绑定

[1] 全局环境中,this 默认绑定到window。

console.log(this === window);    // true

[2] 函数独立调用时,this 默认绑定到 window。

function foo(){
    console.log(this === window);
}
foo();    // true

[3] 被嵌套的函数,独立调用时,this 默认绑定到 window。

//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
    a : 2,
    foo:function(){
            function test(){
                console.log(this.a);
            }
            test();
    }
}
obj.foo();//0

[4] 立即执行函数(IIFE),实际上是声明后直接调用执行。

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

//等价于上例

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

// 类似的,test() 函数是独立调用,而不是方法调用,所以 this 默认绑定到 window 。

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

// 代码示例:由于闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,所以常常在嵌套函数中使用var that = this,然后,在闭包中使用that替代this,使用作用域查找的方法来找到嵌套函数的this值 。

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

方法调用 — 隐式绑定

一般地,被直接对象所包含的函数调用时,也称为方法调用,this 隐式绑定到该直接对象。

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

// foo()函数的直接对象是obj1,this隐式绑定到obj1
   obj1.foo();  // 1

// foo()函数的直接对象是obj2,this隐式绑定到obj2
   obj1.obj2.foo();  // 2

[ 隐式丢失 ] 指的是,被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这种情况容易出错,却又常见。

[1] 函数别名

var a = 0;
function foo(){
    console.log(this.a);
};
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();  // 0

// 等价于
var a = 0;
var bar = function foo(){
    console.log(this.a);
}
bar();  // 0

[2] 参数传递

var a = 0;
function foo(){
    console.log(this.a);
};
function bar(fn){
    fn();
}
var obj = {
    a : 2,
    foo:foo
}
//把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo);//0

//等价于
var a = 0;
function bar(fn){
    fn();
}
bar(function foo(){
    console.log(this.a);
});

[3] 内置函数

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

// 等价于
var a = 0;
setTimeout(function foo(){
    console.log(this.a);
},100);  // 0

[4] 间接引用

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3

//将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
(p.foo = o.foo)(); // 2

function foo() {
    console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3

//将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
p.foo = o.foo;
p.foo();//4

[5] 其他情况:在JS引擎内部,obj和obj.foo储存在两个内存地址,简称为M1和M2。

只有obj.foo()这样调用时,是从M1调用M2,因此this指向obj。但是,下面三种情况,都是直接取出M2进行运算,然后就在全局环境执行运算结果(还是M2),因此this指向全局环境。

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

(obj.foo = obj.foo)();//0

(false || obj.foo)();//0

(1, obj.foo)();//0

构造函数调用 — new绑定

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。

[1] 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。

function fn(){
    this.a = 2;
}
var test = new fn();     // 构造函数调用表达式的计算结果就是这个新对象的值。
console.log(test);//{a:2}

[2] 若 return 语句没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。

function fn(){
    this.a = 2;
    return;
}
var test = new fn();
console.log(test);//{a:2}

[4] 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

// 尽管有时候构造函数看起来像一个方法调用,它依然会使用这个新对象作为this。

var o = {
    m: function(){
        return this;
    }
}
var obj = new o.m();      // 在表达式new o.m()中,this并不是o
console.log(obj,obj === o);//{} false
console.log(obj.constructor === o.m);//true

间接调用 — 显式绑定

通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用。

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

普通的显式绑定无法解决隐式丢失问题

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

硬绑定是显式绑定的一个变种,使this不能再被修改

var a = 0;
function foo(){
    console.log(this.a);
}
var obj = {
    a:2
};
var bar= function(){
    foo.call(obj);
}
//在bar函数内部手动调用foo.call(obj)。因此,无论之后如何调用函数bar,它总会手动在obj上调用foo
bar();//2
setTimeout(bar,100);//2
bar.call(window);//2

JS中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()

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

this 绑定的优先级

如果在函数的调用位置上同时存在两种以上的绑定规则应该怎么办呢?

  • 是否是new绑定?如果是,this绑定的是新创建的对象
var bar = new foo();
  • 是否是显式绑定?如果是,this绑定的是指定的对象
var bar = foo.call(obj2);
  • 是否是隐式绑定?如果是,this绑定的是属于的对象
var bar = obj1.foo();
  • 如果都不是,则使用默认绑定
var bar = foo();

// 代码示例(了解):

显式绑定 pk 隐式绑定:显式绑定胜出

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
//在该语句中,显式绑定call(obj2)和隐式绑定obj1.foo同时出现,最终结果为3,说明被绑定到了obj2中
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

new绑定 pk 隐式绑定:new绑定胜出

function foo(something) {
    this.a = something;
}
var obj1 = {foo: foo};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call(obj2,3);
console.log( obj2.a ); // 3
//在下列代码中,隐式绑定obj1.foo和new绑定同时出现。最终obj1.a结果是2,而bar.a结果是4,说明this被绑定在bar上
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

new绑定 pk 显式绑定:new绑定胜出

function foo(something) {
    this.a = something;
}
var obj1 = {};
//先将obj1绑定到foo函数中,此时this值为obj1
var bar = foo.bind( obj1 );
bar( 2 );
console.log(obj1.a); // 2
//通过new绑定,此时this值为baz
var baz = new bar( 3 );
console.log( obj1.a ); // 2
//说明使用new绑定时,在bar函数内,无论this指向obj1有没有生效,最终this都指向新创建的对象baz
console.log( baz.a ); // 3