原文阅读

  js中的this是很容易让人觉得困惑的地方,这篇文章打算说一下this绑定的几种情况,相信可以解决大部分关于this的疑惑。

this是在运行的时候绑定的,不是在编写的时候绑定的,函数调用的方式不同,就可能使this所绑定的对象不同。


1.几种绑定规则

  函数调用的位置对this的指向有着很大的影响,但却不是完全取决于它。下面是几种this的绑定规则:

1.1.默认绑定

  默认规则的意思就是在一般情况下,如果没有别的规则出现,就将this绑定到全局对象上,我们看如下代码:

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

  这段代码中,this是被默认绑定到了全局对象上,所以this.a得到的是2。我们如何判断这里应用了默认绑定呢?foo在调用的时候直接使用不带任何修饰的函数引用,只能使用默认绑定。有人会误认为结果是3this常有的几种错误理解之一就是认为this指向当前函数的词法作用域,this与词法作用域以及作用域对象是完全不同的东西,作用域对象是在引擎内部的,js代码是无法访问的。还有本文我们不讨论严格模式下的情况,严格模式这里的this会绑定到undefined

1.2.隐式绑定

  如果在调用位置有上下文对象,说简单点就是这个函数调用时是用一个对象.出来的。就像下边这样,它就遵循隐式绑定:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var a = "opps, gloabl";                //全局对象的属性
obj.foo();                    //2
var bar = obj.foo;
bar();                        //"opps, gloabl"

  第9行代码,就是函数在调用的时候,是用前边的对象加上.操作符调用出来的,此时就用到了隐式绑定规则,隐式绑定规则会将函数调用中的this绑定到这个上下文对象,此时的this.aobj.a是一样的。
  而隐式绑定会出现一个问题,就是隐式丢失,上边的第10行代码,是新建一个foo函数的引用,即bar,在最后一行调用的时候,这个函数不具有上下文对象,此时采用默认绑定规则,得到的结果自然是opps, gloabl;
  绑定丢失也会发生在函数作为参数传递的情况下,即传入回调函数时,因为参数传递就是一种隐式赋值,看如下代码:

function foo() {
    console.log( this.a );
}
function doFoo(fn) {
    fn();            //在此处调用,参数传递是隐式赋值,丢失this绑定
}
var obj = {
    a: 2,
    foo: foo
};
var a = "opps, global";
doFoo( obj.foo );        //看似是隐式绑定,输出opps, global

  javascript环境中内置的函数,在具有接受一个函数作为参数的功能的时候,也会发生像上边这种状况。例如setTimeout函数的实现就类似于下边的伪代码:

function setTimeout(fn, delay) {
    //等待delay毫秒
    fn();//在此处调用
}

  所以回调函数丢失this绑定是非常常见的,后边我们再看如何通过固定this来修复这个问题。

1.3.显式绑定

  在此之前,相信你已经用过很多次applycall函数了,使用这两个函数可以直接为你要执行的函数指定this,所以这种方式称为显式绑定。

function foo() {        //显式绑定this,这种方式依然无法解决绑定丢失的问题
    console.log( this.a );
}
var obj = {
    a: 2
};
foo.call( obj );        //2

  通过像上边这样调用,我们可以将foothis强制绑定到obj上。如果给call传入的是一个基本类型数据,这个基本类型数据将会被转换成对应的基本包装类型。不过这种方式依然无法解决上边的丢失绑定问题。

1.3.1.硬绑定

  为了解决这个问题,我们可以写像下边这样的代码:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2
};
var bar = function() {         //创建一个包裹函数,以确保obj的绑定
    foo.call( obj );
};
bar();                       //2
setTimeout( bar, 100 );           //2
bar.call( window );           //2

  上边这样的函数确实解决了绑定丢失的问题,每次调用bar就可以确保obj的绑定,不过还不能为函数传参,而且这种方法复用率低,所以又出现了这样的辅助绑定函数:

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
function bind(fn, obj) {        //辅助绑定函数
    return function() {
        return fn.apply( obj, arguments );
    };
}
var obj = {
    a: 2
};
var bar = bind( foo, obj );
var b = bar(3);
console.log(b);

  因为这种模式很常用,所以ES5内置了这个方法,就是bindbind(...)返回一个硬编码的新函数,将你指定的对象绑定到调用它的函数的this上。

function foo(something) {
    console.log( this.a, something );
    return this.a + something;
}
var obj = {
    a: 2
};
var bar = foo.bind( obj );    //bind返回一个绑定到obj上的新函数
var b = bar(3);
console.log(b);
var a = "window's a";
foo("!");                       //对原来的函数不产生影响

1.3.2.API调用参数指定this

  许多第三方库里的函数,以及许多语言内置的函数,都提供了一个可选的参数用来指定函数执行的this

function foo(el) {
    console.log( el, this.id );
}
var obj = {
    id: "awesome"
};
[1, 2, 3].forEach( foo, obj );   //forEach的第二个参数就是用来设置this

1.4.new绑定

js中的所谓的构造函数,其实和一般的普通函数没有什么区别,并不具有特殊性,它们只是被new操作符调用的普通函数而已。实际上并不存在什么构造函数,只存在对于函数的构造调用

发生构造函数的调用时,会自动执行下边的操作:

  1. 创建一个全新的对象。

  2. 这个对象会被执行[[Prototype]]连接。

  3. 这个新对象会绑定到函数调用的this

  4. 执行这个函数里的代码。

  5. 如果函数没有返回其他对象,则自动返回这个新对象。

这个在执行new操作的时候对this的绑定就叫做new绑定。

function fun() {
  this.a = 1;
  this.b = 2;
}
var instance = new fun();
console.log(instance.a);

1.5.箭头函数的this

  ES6中的箭头函数是无法使用以上几种规则的,它是根据外层的作用域来决定this,即取决于外层的函数作用域或全局作用域,而且箭头函数的绑定无法修改,即使是new绑定也不可以。

function foo() {
    return (a) => {
        console.log( this.a );
    }
}
var obj1 = {
    a: 2
};
var obj2 = {
    a: 3
};
var bar = foo.apply(obj1);
bar.apply(obj2);        //2

2.绑定规则的优先级

  前边我们已经说了this的几种绑定规则,当函数调用的位置可以使用多条绑定规则的时候,我们就需要确定这几种规则的优先级。

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.foo.call( obj1 );    //2

  从上边的代码可以看出来,显式绑定的优先级要高于隐式绑定,下边再看看显式绑定和new绑定的优先级:

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

  仔细看这段代码,barfoo绑定到obj1上返回的一个函数,对这个函数进行new操作,并传入新的a值,发现改变的是新对象baz的属性,和obj1已经脱离关系。说明new绑定的优先级高于硬绑定。

  综上所述,我们在遇到this时,如果不是箭头函数,就可以以这种顺序判断它的指向:

  1. 如果函数在new中调用,绑定到新建的对象。

  2. 函数通过callapply或者硬绑定调用,this绑定到指定的对象上。

  3. 函数在某个上下文对象中调用,绑定到这个上下文对象上。

  4. 采用默认绑定规则。


  以上就是这篇博客的所有内容了,如果有什么理解的不对的地方欢迎讨论,这里是我的博客以及github,欢迎来访。

参考书籍:《你不知道的JavaScript(上卷)》


kongcheng
369 声望9 粉丝

学习前端ing