详细讲解JavaScript中的this绑定,你不知道的JS上

在 javascript 中,这个记录会包含函数在哪里被调用

this 可以说是 javascript
中最耐人寻味的一个特性,就像高中英语里各种时态,比如被动时态,过去时,现在时,过去进行时一样,无论弄错过多少次,下一次依然可能弄错。本文启发于《你不知道的JavaScript上卷》,对
javasript 中的 this 进行一个总结。

关于this

 

  与静态词法作用域不用,this的指向动态绑定,在函数执行期间才能确定。感觉有点像C++的多态?

    var a = 1;
    var obj = {
        a: 2,
        fn: function() {
            console.log(this.a);
        }
    };
    obj2 = {
        a: 3,
        fn: obj.fn
    };
    //通过对象调用 this指向obj
    obj.fn(); //2
    //通过函数调用 this指向window
    setTimeout(obj.fn, 0); //1
    obj2.fn(); //3

  这个例子很好理解,谁调用的函数,this就指向谁。

  当一个函数被调用时,会创建一个活动记录(上下文)。这个记录会包含函数在哪里被调用,函数的调用方法,传入的参数等信息。this就是记录的其中一个信息。

 

  开始详解this了!

  理解this绑定,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。

  分析位置最重要的是分析调用栈,我们关心的调用位置就在当前正在执行的函数的前一个调用中。

    //调用栈是f1 即全局作用域
    function f1() {
        console.log('f1');
        f2(); //f2调用位置
    }
    //调用栈是f1 => f2
    function f2() {
        console.log('f2');
        f3(); //f3调用位置
    }
    //调用栈是f1 => f2 => f3
    function f3() {
        console.log('f3');
    }
    f1(); //f1调用位置

   

  通过找到调用位置,判断需要应用哪一条规则。this绑定的四条规则:

 

默认绑定

  最常用的函数调用类型:独立函数调用。

    var a = 1;
    // 全局函数
    function fn() {
        // 'use strict' error
        console.log(this.a); //1
    }
    //默认this绑定到window
    fn();

  在这里,fn是直接进行调用,没有任何修饰,相当于window.fn(),只能适用默认绑定,指向了window。

 

隐式绑定

  这种绑定需要考虑调用位置是否有上下文对象。

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

  当fn被调用时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用fn时this被绑定到obj,因此this.a和obj.a是一样的。

  另外,链式调用时,只有最近的调用对象会影响this。

    var a = 1;
    var obj = {
        a: 2,
        fn: function() {
            console.log(this.a);
        }
    };
    obj2 = {
        a: 3,
        fn: obj
    };
    //this.a相当于obj.a
    obj2.fn.fn(); //2

隐式丢失

  一个最常见的this绑定问题是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或undefined上。

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

  被作为函数调用时,会被默认绑定到window对象。

  另外一种情况是传入回调函数时:

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

    function fn2(fn) {
        fn();
    }
    var a = 1;
    var obj = {
        a: 2,
        fn: fn
    }
    //还是作为函数调用
    fn2(obj.fn);

 

显式绑定

  就是用apply和call方法强制绑定this。一个例子说明一切:

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

 

硬绑定

  直接用vue源码来举例子吧。

    function bind(fn, ctx) {
        return function(a) {
            var len = arguments.length;
            return len ? len > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
        }
    }

  简单暴力,都会背了。

 

new绑定

  JS中new的机制实际上和面向类的语言完全不同。

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

  Number作为构造函数时,ES5.1中这样描述:当Number在new表达式中被调用时,它是一个构造函数,会初始化新创建的对象。

  因为,包括内置对象函数在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上不存在所谓的构造函数,只有对于函数的构造调用。

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

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

  2、这个新对象会被执行【原型】连接。

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

  4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

    function fn(a, b) {
        this.a = a;
        this.b = b;
    }
    var f = new fn();
    console.log(f); //{a:undefined,b:undefined}

 

 

学习 this 的第一步就是明白 this
既不是指向函数自身也不指向函数的作用域。this
实际上是在函数被调用时发生的绑定,它指向什么地方完全取决于函数在哪里被调用。

优先级

 

  如果某个调用位置应用多条规则,需要为这些规则设定优先级。(默认绑定、隐式绑定、显示绑定、new绑定)

  首先默认绑定优先级最低。

  隐式 VS 显示?

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

  显式比较厉害。

 

  再来比较一下new和显示绑定优先级:

    function fn(a) {
        this.a = a;
    }
    var obj = {};
    //this强制绑定到obj对象上
    var f1 = fn.bind(obj);
    f1(2);
    //现在obj有了一个a属性
    console.log(obj.a); //2
    //new一个对象将this.a改成3
    var f2 = new f1(3);
    //这里依然没有变
    console.log(obj.a); //2
    //将this指向了new出来的a
    console.log(f2.a); //3

  可以看出,通过new的绑定,覆盖了bind强制绑定的a,所以new绑定是优先于显示绑定的。

 

  顺便贴一下MDN提供的bind()实现:

    if (!Function.prototype.bind) {
        Function.prototype.bind = function(oThis) {
            if (typeof this !== 'function') {
                throw new TypeError('Function.prototype.bind - what is trying ' +
                    'to be bound is not callable');
            }
            var aArgs = Array.prototype.slice.call(arguments, 1),
                fToBind = this,
                fNOP = function() {},
                fBound = function() {
                    return fToBind.apply(
                        this instanceof fNoP && oThis ? this : oThis,
                        aArgs.concat(Array.prototype.slice.call(arguments))
                    );
                };
            fNOP.prototype = this.prototype;
            fBound.prototype = new fNOP();
            return fBound;
        };
    }

  

默认绑定

总结

优先级为:

  1、如果使用了new,this绑定新创建的对象。

  2、通过call、apply显式绑定。

  3、是否使用隐式绑定?即对象调用

  4、其余情况默认绑定在window上

 

在 javascript 中
,最常用的函数调用类型就是独立函数调用,因此可以把这条规则看作是无法应用其他规则时的默认规则。如果在调用函数的时候,函数不带任何修饰,也就是“光秃秃”的调用,那就会应用默认绑定规则,
默认绑定的指向的是全局作用域。

例外情况

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

  如果用apply展开数组可能用得上,比如fn.apply(null,arr),其中arr为数组,里面的值作为参数传给fn。

  但是这个参数十分不安全,可能会不经意的修改全局变量,可以通过纯空对象来创建一个安全的this。

    var a = 1,
        b = 1;
    console.log(a); //1

    function fn(a) {
        this.a = a;
    }

    function fn2(b) {
        this.b = b;
    }
    //原本可能只想利用apply
    fn.apply(null, [2]);
    //不小心改了全局变量!
    console.log(a);
    //创建一个纯空对象
    var obj = Object.create(null);
    console.log(b); //1
    //安全的this
    fn2.call(obj, 3);
    // 现在安全了!
    console.log(b); //1

   

  

  但是引入了箭头函数,问题就有点奇怪了。

    var a = 1;
    var obj = {
        a: 2,
        fn: () => {
            console.log(this.a);
        }
    };
    obj2 = {
        a: 3,
        fn: obj.fn
    };
    obj.fn(); //1
    setTimeout(obj.fn, 0); //1
    obj2.fn(); //1

  全是1,这是怎么回事?换arguments的例子来看:

    function fn() {
        setTimeout(function() {
            console.log(arguments);
        }, 0)
    }
    fn(1, 2); //[] 确实未传入参数
    function fn2() {
        setTimeout(() => {
            console.log(arguments);
        }, 0)
    }
    fn2(1, 2); //[1,2] 把外部的参数吸进来了!

  简单来讲,箭头函数使得this查询回到了词法作用域规则,它本身不具有this、arguments等属性,根据外层作用域来决定this。

    function fn() {
        return (a) => console.log(this.a);
    }
    var obj = {
        a: 1
    };
    var obj2 = {
        a: 2
    }
    var f = fn.call(obj);
    f(); //1
    f.call(obj2); //1

  当箭头函数外部的fn被绑定到obj时,无论怎么调用和绑定,this指向永远是上一层作用域(window),所以会输出1。

 

function sayLocation() {
 console.log(this.atWhere)
}

var atWhere = "I am in global"

sayLocation() // 默认绑定,this绑定在全局对象,输出 “I am in global”

真·总结

  如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面4条规则来判断this的绑定对象。

  1、由new调用?绑定到新创建的对象。

  2、由call、apply或bind调用?绑定到指定对象。

  3、由上下文调用?绑定到那个上下文调用。

  4、默认:严格模式绑定到undefined,否则绑定到全局对象。

 

再看一个例子

var name = "global"
function person() {
 console.log(this.name) // (1) "global"
  person.name = 'inside'
 function sayName() {
  console.log(this.name) // (2) "global" 不是 "inside"
 }
 sayName() // 在person函数内部执行sayName函数,this指向的同样是全局的对象
}
person()

在这个例子中,person 函数在全局作用域中被调用,因此第(1)句中的 this
就绑定在了全局对象上(在浏览器中是是window,在node中就是global),因此第(1)句自然输出的是一个全局对象的
name
属性,当然就是”global”了。sayName函数在person函数内调用,即使这样第(2)句中的this指代的仍然是全局对象,即使
person 函数设置了 name 属性。

这就是默认绑定规则,它是 javascript 中最常见的一种函数调用模式,this
的绑定规则也是四种绑定规则中最简单的一种,就是绑定在全局作用域上。

默认绑定里的严格模式

在 javascript 中,如果使用了严格模式,则 this
不能绑定到全局对象。还是以第一个例子,只不过这次加上了严格模式声明

'use strict'
function sayLocation() {
 console.log(this.atWhere)
}
var atWhere = "I am in global"
sayLocation()
// Uncaught TypeError: Cannot read property 'atWhere' of undefined

可以看出,在严格模式下,把 this 绑定到全局对象上时,实际上绑定的是
undefined ,因此上面这段代码会报错。

隐式绑定

当函数在调用时,如果函数有所谓的“落脚点”,即有上下文对象时,隐式绑定规则会把函数中的
this
绑定到这个上下文对象。如果觉得上面这段话不够直白的话,还是来看代码。

function say() {
 console.log(this.name)
}
var obj1 = {
 name: "zxt",
 say: say
}

var obj2 = {
 name: "zxt1",
 say: say
}
obj1.say() // zxt
obj2.say() // zxt1

很简单是不是。在上面这段代码中,obj1 , obj2 就是所谓的 say
函数的落脚点,专业一点的说法就是上下文对象,当给函数指定了这个上下文对象时,函数内部的this
自然指向了这个上下文对象。这也是很常见的一种函数调用模式。

隐式绑定时丢失上下文

function say() {
 console.log(this.name)
}
var name = "global"
var obj = {
 name: "inside",
 say: say
}
var alias = obj.say // 设置一个简写 (1) 
alias() // 函数调用 输出"global" (2)

可以看到这里输出的是 ”global“ ,为什么就和上例中不一样,我们明明只是给
obj.say 换了个名字而已?
首先我们来看上面第(1)句代码,由于在 javascript
中,函数是对象,对象之间是引用传递,而不是值传递。因此,第(1)句代码只是
alias = obj.say = say ,也就是 alias = say ,obj.say
只是起了一个桥梁的作用,alias 最终引用的是 say 函数的地址,而与 obj
这个对象无关了。这就是所谓的”丢失上下文“。最终执行 alias
函数,只不过简单的执行了say函数,输出”global”。

显式绑定

显式绑定,顾名思义,显示地将this绑定到一个上下文,javascript中,提供了三种显式绑定的方法,apply,call,bind。apply和call的用法基本相似,它们之间的区别是:

apply(obj,[arg1,arg2,arg3,…] 被调用函数的参数以数组的形式给出
call(obj,arg1,arg2,arg3,…) 被调用函数的参数依次给出
而bind函数执行后,返回的是一个新函数。下面以代码说明。

// 不带参数
function speak() {
  console.log(this.name)
}

var name = "global"
var obj1 = {
  name: 'obj1'
}
var obj2 = {
  name: 'obj2'
}

speak() // global 等价于speak.call(window)
speak.call(window)

speak.call(obj1) // obj1
speak.call(obj2) // obj2

因此可以看出,apply, call
的作用就是给函数绑定一个执行上下文,且是显式绑定的。因此,函数内的this自然而然的绑定在了
call 或者 apply 所调用的对象上面。

// 带参数
function count(num1, num2) {
  console.log(this.a * num1 + num2)
}

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

count.call(obj1, 1, 2) // 4
count.apply(obj1, [1, 2]) // 4

count.call(obj2, 1, 2) // 5
count.apply(obj2, [1, 2]) // 5

上面这个例子则说明了 apply 和 call 用法上的差异。
而 bind
函数,则返回一个绑定了指定的执行上下文的新函数。还是以上面这段代码为例

// 带参数
function count(num1, num2) {
  console.log(this.a * num1 + num2)
}

var obj1 = {
  a: 2
}

var bound1 = count.bind(obj1) // 未指定参数
bound1(1, 2) // 4

var bound2 = count.bind(obj1, 1) // 指定了一个参数
bound2(2) // 4

var bound3 = count.bind(obj1, 1, 2) // 指定了两个参数
bound3() //4

var bound4 = count.bind(obj1, 1, 2, 3) // 指定了多余的参数,多余的参数会被忽略
bound4() // 4

所以,bind
方法只是返回了一个新的函数,这个函数内的this指定了执行上下文,而返回这个新函数可以接受参数。

new 绑定

最后要讲的一种 this 绑定规则,是指通过 new 操作符调用构造函数时发生的
this 绑定。首先要明确一点的是,在 javascript
中并没有其他语言那样的类的概念。构造函数也仅仅是普通的函数而已,只不过构造函数的函数名以大写字母开头,也只不过它可以通过
new 操作符调用而已.

function Person(name,age) {
  this.name = name
  this.age = age
  console.log("我也只不过是个普通函数")
}
Person("zxt",22) // "我也只不过是个普通函数"
console.log(name) // "zxt"
console.log(age) // 22

var zxt = new Person("zxt",22) // "我也只不过是个普通函数"
console.log(zxt.name) // "zxt"
console.log(zxt.age) // 22

上面这个例子中,首先定义了一个 Person
函数,既可以普通调用,也可以以构造函数的形式的调用。当普通调用时,则按照正常的函数执行,输出一个字符串。
如果是通过一个new操作符,则构造了一个新的对象。那么,接下来我们再看看两种调用方式,
this
分别绑定在了何处首先普通调用时,前面已经介绍过,此时应用默认绑定规则,this绑定在了全局对象上,此时全局对象上会分别增加
name 和 age
两个属性。当通过new操作符调用时,函数会返回一个对象,从输出结果上来看
this 对象绑定在了这个返回的对象上。
因此,所谓的new绑定是指通过new操作符来调用函数时,会产生一个新对象,并且会把构造函数内的this绑定到这个对象上。
事实上,在javascript中,使用new来调用函数,会自动执行下面的操作。

  1. 创建一个全新的对象
  2. 这个新对象会被执行原型连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

四种绑定的优先级

上面讲述了javascript中四种this绑定规则,这四种绑定规则基本上涵盖了所有函数调用情况。但是如果同时应用了这四种规则中的两种甚至更多,又该是怎么样的一个情况,或者说这四种绑定的优先级顺序又是怎么样的。
首先,很容易理解,默认绑定的优先级是最低的。这是因为只有在无法应用其他this绑定规则的情况下,才会调用默认绑定。那隐式绑定和显式绑定呢?还是上代码吧,代码可从来不会说谎。

function speak() {
  console.log(this.name)
}

var obj1 = {
  name: 'obj1',
  speak: speak
}
var obj2 = {
  name: 'obj2'
}

obj1.speak() // obj1 (1)
obj1.speak.call(obj2) // obj2 (2)

所以在上面代码中,执行了obj1.speak(),speak函数内部的this指向了obj1,因此(1)处代码输出的当然就是obj1,但是当显式绑定了speak函数内的this到obj2上,输出结果就变成了obj2,所有从这个结果可以看出显式绑定的优先级是要高于隐式绑定的。事实上我们可以这么理解obj1.speak.call(obj2)这行代码,obj1.speak只是间接获得了speak函数的引用,这就有点像前面所说的隐式绑定丢失了上下文。好,既然显式绑定的优先级要高于隐式绑定,那么接下来再来比较一下new
绑定和显式绑定。

function foo(something) {
  this.a = something
}

var obj1 = {}
var bar = foo.bind(obj1) // 返回一个新函数bar,这个新函数内的this指向了obj1 (1)
bar(2) // this绑定在了Obj1上,所以obj1.a === 2
console.log(obj1.a)

var baz = new bar(3) // 调用new 操作符后,bar函数的this指向了返回的新实例baz (2)

console.log(obj1.a)
console.log(baz.a) 

我们可以看到,在(1)处,bar函数内部的this原本指向的是obj1,但是在(2)处,由于经过了new操作符调用,bar函数内部的this却重新指向了返回的实例,这就可以说明new
绑定的优先级是要高于显式绑定的。
至此,四种绑定规则的优先级排序就已经得出了,分别是

new 绑定 > 显式绑定 > 隐式绑定 >
默认绑定

箭头函数中的this绑定

箭头函数是ES6里一个重要的特性。
箭头函数的this是根据外层的(函数或者全局)作用域来决定的。函数体内的this对象指的是定义时所在的对象,而不是之前介绍的调用时绑定的对象。举一个例子

var a = 1
var foo = () => {
  console.log(this.a) // 定义在全局对象中,因此this绑定在全局作用域
}

var obj = {
  a: 2
}
foo() // 1 ,在全局对象中调用
foo.call(obj) // 1,显示绑定,由obj对象来调用,但根本不影响结果

从上面这个例子看出,箭头函数的 this
强制性的绑定在了箭头函数定义时所在的作用域,而且无法通过显示绑定,如apply,call方法来修改。在来看下面这个例子

// 定义一个构造函数
function Person(name,age) {
  this.name = name
  this.age = age 
  this.speak = function (){
    console.log(this.name)
    // 普通函数(非箭头函数),this绑定在调用时的作用域
  }
  this.bornYear = () => {
    // 本文写于2016年,因此new Date().getFullYear()得到的是2016
    // 箭头函数,this绑定在实例内部
    console.log(new Date().getFullYear() - this.age)
    }
  }
}

var zxt = new Person("zxt",22)

zxt.speak() // "zxt"
zxt.bornYear() // 1994

// 到这里应该大家应该都没什么问题

var xiaoMing = {
  name: "xiaoming",
  age: 18 // 小明永远18岁
}

zxt.speak.call(xiaoMing)
// "xiaoming" this绑定的是xiaoMing这个对象
zxt.bornYear.call(xiaoMing)
// 1994 而不是 1998,这是因为this永远绑定的是zxt这个实例

因此 ES6
的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定
this ,具体来说就是,箭头函数会继承 外层函数调用的this绑定
,而无论外层函数的this绑定到哪里。

小结

以上就是javascript中所有this绑定的情况,在es6之前,前面所说的四种绑定规则可以涵盖任何的函数调用情况,es6标准实施以后,对于函数的扩展新增了箭头函数,与之前不同的是,箭头函数的作用域位于箭头函数定义时所在的作用域。

而对于之前的四种绑定规则来说,掌握每种规则的调用条件就能很好的理解this到底是绑定在了哪个作用域。

您可能感兴趣的文章:

  • js绑定事件this指向发生改变的问题解决方法
  • javascript下动态this与动态绑定实例代码
  • JavaScript call apply使用
    JavaScript对象的方法绑定到DOM事件后this指向问题
  • Javascript中的this绑定介绍
  • 简单谈谈javascript中this的隐式绑定
  • JavaScript中this的四个绑定规则总结
  • javascript this用法小结
  • javascript中onclick(this)用法介绍
  • js中的this关键字详解
  • Javascript中this绑定的3种方法与比较