JavaScript中this绑定详解

JavaScript中this绑定详解_第1张图片


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

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

默认绑定

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

 
   
  1. function sayLocation() { 
  2.     console.log(this.atWhere) 
  3.  
  4. var atWhere = "I am in global" 
  5.  
  6. sayLocation() // 默认绑定,this绑定在全局对象,输出 “I am in global”  

再看一个例子

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

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

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

默认绑定里的严格模式

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

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

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

隐式绑定

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

 
   
  1. function say() { 
  2.     console.log(this.name
  3. var obj1 = { 
  4.     name"zxt"
  5.     say: say 
  6.  
  7. var obj2 = { 
  8.     name"zxt1"
  9.     say: say 
  10. obj1.say() // zxt 
  11. obj2.say() // zxt1  

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

隐式绑定时丢失上下文

 
   
  1. function say() { 
  2.     console.log(this.name
  3. var name = "global" 
  4. var obj = { 
  5.     name"inside"
  6.     say: say 
  7. var alias = obj.say // 设置一个简写   (1)  
  8. 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函数执行后,返回的是一个新函数。下面以代码说明。

 
   
  1. // 不带参数 
  2. function speak() { 
  3.     console.log(this.name
  4.  
  5. var name = "global" 
  6. var obj1 = { 
  7.     name'obj1' 
  8. var obj2 = { 
  9.     name'obj2' 
  10.  
  11. speak() // global 等价于speak.call(window) 
  12. speak.call(window) 
  13.  
  14. speak.call(obj1) // obj1 
  15. speak.call(obj2) // obj2  

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

 
   
  1. // 带参数 
  2. function count(num1, num2) { 
  3.     console.log(this.a * num1 + num2) 
  4.  
  5. var obj1 = { 
  6.     a: 2 
  7. var obj2 = { 
  8.     a: 3 
  9.  
  10. count.call(obj1, 1, 2) // 4 
  11. count.apply(obj1, [1, 2]) // 4 
  12.  
  13. count.call(obj2, 1, 2) // 5 
  14. count.apply(obj2, [1, 2]) // 5  

上面这个例子则说明了 apply 和 call 用法上的差异。

而 bind 函数,则返回一个绑定了指定的执行上下文的新函数。还是以上面这段代码为例

 
   
  1. // 带参数 
  2. function count(num1, num2) { 
  3.     console.log(this.a * num1 + num2) 
  4.  
  5. var obj1 = { 
  6.     a: 2 
  7.  
  8. var bound1 = count.bind(obj1) // 未指定参数 
  9. bound1(1, 2) // 4 
  10.  
  11. var bound2 = count.bind(obj1, 1) // 指定了一个参数 
  12. bound2(2) // 4  
  13.  
  14. var bound3 = count.bind(obj1, 1, 2) // 指定了两个参数 
  15. bound3() //4 
  16.  
  17. var bound4 = count.bind(obj1, 1, 2, 3) // 指定了多余的参数,多余的参数会被忽略 
  18. bound4() // 4 

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

new 绑定

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

 
   
  1. function Person(name,age) { 
  2.     this.name = name 
  3.     this.age = age 
  4.     console.log("我也只不过是个普通函数"
  5. Person("zxt",22) // "我也只不过是个普通函数" 
  6. console.log(name) // "zxt" 
  7. console.log(age) // 22 
  8.  
  9. var zxt = new Person("zxt",22) // "我也只不过是个普通函数" 
  10. console.log(zxt.name) // "zxt" 
  11. 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绑定规则的情况下,才会调用默认绑定。那隐式绑定和显式绑定呢?还是上代码吧,代码可从来不会说谎。

 
   
  1. function speak() { 
  2.     console.log(this.name
  3.  
  4. var obj1 = { 
  5.     name'obj1'
  6.     speak: speak 
  7. var obj2 = { 
  8.     name'obj2' 
  9.  
  10. obj1.speak() // obj1 (1) 
  11. 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 绑定和显式绑定。

 
   
  1. function foo(something) { 
  2.     this.a = something 
  3.  
  4. var obj1 = {} 
  5. var bar = foo.bind(obj1)  // 返回一个新函数bar,这个新函数内的this指向了obj1  (1) 
  6. bar(2) // this绑定在了Obj1上,所以obj1.a === 2 
  7. console.log(obj1.a) 
  8.  
  9. var baz = new bar(3)  // 调用new 操作符后,bar函数的this指向了返回的新实例baz  (2) 
  10.  
  11. console.log(obj1.a) 
  12. console.log(baz.a)   

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

至此,四种绑定规则的优先级排序就已经得出了,分别是

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

箭头函数中的this绑定

箭头函数是ES6里一个重要的特性。

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

 
   
  1. var a = 1 
  2. var foo = () => { 
  3.     console.log(this.a) // 定义在全局对象中,因此this绑定在全局作用域 
  4.  
  5. var obj = { 
  6.     a: 2 
  7. foo() // 1 ,在全局对象中调用 
  8. foo.call(obj) // 1,显示绑定,由obj对象来调用,但根本不影响结果  

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

 
   
  1. // 定义一个构造函数 
  2. function Person(name,age) { 
  3.     this.name = name 
  4.     this.age = age  
  5.     this.speak = function (){ 
  6.         console.log(this.name
  7.         // 普通函数(非箭头函数),this绑定在调用时的作用域 
  8.     } 
  9.     this.bornYear = () => { 
  10.         // 本文写于2016年,因此new Date().getFullYear()得到的是2016 
  11.         // 箭头函数,this绑定在实例内部 
  12.         console.log(new Date().getFullYear() - this.age) 
  13.         } 
  14.     } 
  15.  
  16. var zxt = new Person("zxt",22) 
  17.  
  18. zxt.speak() // "zxt" 
  19. zxt.bornYear() // 1994 
  20.  
  21. // 到这里应该大家应该都没什么问题 
  22.  
  23. var xiaoMing = { 
  24.     name"xiaoming"
  25.     age: 18  // 小明永远18岁 
  26.  
  27. zxt.speak.call(xiaoMing) 
  28. // "xiaoming" this绑定的是xiaoMing这个对象 
  29. zxt.bornYear.call(xiaoMing) 
  30. // 1994 而不是 1998,这是因为this永远绑定的是zxt这个实例  

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

小结

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

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


作者:499311496

来源:51CTO

你可能感兴趣的:(javascript)