this的五种绑定方式

1,092 阅读5分钟

this的指向对于很多javascript开发者来说无疑是很难理解清楚的事。我自己也是在这个问题上理解的不甚清晰,现借助分析this的五种绑定方式来进一步理解this。 要理解this的指向,首先需要了解this实际上是在函数被调用时发生的绑定,this总是指向调用该函数的对象。

默认绑定

如果没有调用该函数的对象,默认绑定到window对象上。

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

foo()没有被任何对象调用,默认绑定到window对象上,代码中foo()中打印的变量a是全局变量a

隐式绑定

当函数引用有上下文对象即调用对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

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

上面的代码中,因为调用foo()this被绑定到obj,因此this.aobj.a 是一样的。对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说

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

显示绑定

可以通过call(..)apply(..)方法显示指定this的绑定对象。

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

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)new Boolean(..)或者new Number(..))这通常被称为“装箱”。

new绑定

在javascript中使用new操作符来生成一个对象时,this会绑定到这个新的对象上。

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

使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。

箭头函数

ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this

foo()内部创建的箭头函数会捕获调用时foo()this。由于foo()this绑定到obj1bar(引用箭头函数)的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!

使用注意点

1、如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,this会绑定到undefined:

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

2、隐式有时会变成默认绑定

function foo(){
    console.log(this.a)
}
var obj={
    a:2,
    foo: foo
};
var bar=obj.foo;//函数别名
var a="oops,global";//a是全局对象的属性
bar();//"oops,global"

虽然barobj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,跟obj已经没有任何关系,因此应用了默认绑定。当然,这时如果使用严格模式的话,this的默认绑定会出现this绑定到undefined的情况。

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

var v="global-hello";
var obj = {
    v: 'hello',
    p: ['a1', 'a2'],
    f: function f() {
        this.p.forEach(function (item) {
            console.log(this.v + ' ' + item);
        },obj);
    }
};

obj.f();//hello a1  //hello a2

上面代码的this,因为上下文obj对象,使得在调用时this会指向obj,这时如果去掉obj这个参数,this没有显示或隐式的绑定对象,就会指向全局对象window

var v="global-hello";
var obj = {
    v: 'hello',
    p: ['a1', 'a2'],
    f: function f() {
        this.p.forEach(function (item) {
            console.log(this.v + ' ' + item);
        });
    }
};

obj.f();//global-hello a1  //global-hello a2

call、apply、bind方法的区别

this为JavaScript创造了巨大的灵活性,但也使得this变的难以理解。有时需要把this固定下来,避免出现意想不到的情况。前面提到过通过foo.call(),我们可以在调用函数时强制把它的this绑定到对应的对象上。

JavaScript 提供了callapplybind三个方法,来改变this的指向,这三个方法都是Function.prototype上的方法。

1、Function.prototype.call()
调用格式
func.call(thisObj, arg1, arg2, ...)
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。如果call方法没有参数,或者参数为nullundefined,则等同于指向全局对象。

var obj = {};
var f = function (arg1,arg2) {
  console.log(arg1,arg2)
  return this;
};
f('a','b') === window // true
f.call(obj,'a','b') === obj // true

上面的代码通过调用call方法将this的指向改为obj。

2、Function.prototype.apply()
调用格式
func.apply(thisObj, [arg1, arg2, ...])
apply方法的第一个参数也是this所要指向的那个对象,如果设为nullundefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

function f(x, y){
  console.log(x + y);
}

f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2

callapply的区别仅仅只在于参数形式不一样。

3、Function.prototype.bind()
调用格式
func.bind(thisObj, arg1, arg2, ...)

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 ); //

看上面的代码,我们创建了函数 bar(),并在它的内部手动调用了 foo.call(obj),因此强制把foothis绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种方式会经常用到,所以在ES5中提供了内置的方法Function.prototype.bind,它的用法如下:

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

bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文(即绑定对象)并调用原始函数。
总结一下这三者的区别就是
1)call()apply()bind()都是用来改变this的绑定;
2)call()apply()唯一区别是参数不一样;
3)bind()是返回一个新函数,供以后调用,而apply()call()是立即调用。

参考资源

《你不知道的JavaScript(上卷)》 Kyle Simpson