摘要:本文属于原创,欢迎转载,转载请保留出处: https://github.com/jasonGeng88/blog
理解this以前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法做用域。若是仅经过this的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说this具体指向什么,取决于你是怎么调用的函数。git
this的4种绑定规则分别是:默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高。es6
什么叫默认绑定,即没有其余绑定规则存在时的默认规则。这也是函数调用中最经常使用的规则。github
来看这段代码:app
function foo() { } console.log( this.a ); var a = 2; foo(); //打印的是什么?
foo()
打印的结果是2。函数
由于foo()是直接调用的(独立函数调用),没有应用其余的绑定规则,这里进行了默认绑定,将全局对象绑定this上,因此this.a 就解析成了全局变量中的a,即2。优化
注意:在严格模式下(strict mode),全局对象将没法使用默认绑定,即执行会报undefined的错误this
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
除了直接对函数进行调用外,有些状况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。prototype
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); // ?
obj.foo()
打印的结果是3。调试
这里foo函数被当作引用属性,被添加到obj对象上。这里的调用过程是这样的:code
获取obj.foo属性 -> 根据引用关系找到foo函数,执行调用
因此这里对foo的调用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,因此this.a被解析成了obj.a,即3。
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 4, foo: foo }; var obj2 = { a: 3, obj1: obj1 }; obj2.obj1.foo(); //?
obj2.obj1.foo()
打印的结果是4。
一样,咱们看下函数的调用过程:
先获取obj2.obj1 -> 经过引用获取到obj1对象,再访问 obj1.foo -> 最后执行foo函数调用
这里调用链不仅一层,存在obj一、obj2两个对象,那么隐式绑定具体会绑哪一个对象。这里原则是获取最后一层调用的上下文对象,即obj1,因此结果显然是4(obj1.a)。
注意:这里存在一个陷阱,你们在分析调用过程时,要特别当心
先看个代码:
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo; bar(); //?
<font color="red">bar()
打印的结果是2。</font>
为何会这样,obj.foo 赋值给bar,那调用bar()
为何没有触发隐式绑定,使用的是默认绑定呢。
这里有个概念要理解清楚,obj.foo 是引用属性,赋值给bar的实际上就是foo函数(即:bar指向foo自己)。
那么,实际的调用关系是:经过bar找到foo函数,进行调用。整个调用过程并无obj的参数,因此是默认绑定,全局属性a。
function foo() { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; setTimeout( obj.foo, 100 ); // ?
<font color="red">打印的结果是2。</font>
一样的道理,虽然参传是obj.foo
,由于是引用关系,因此传参实际上传的就是foo对象自己的引用。对于setTimeout
的调用,仍是 setTimeout -> 获取参数中foo的引用参数 -> 执行 foo 函数,中间没有obj的参与。这里依旧进行的是默认绑定。
相对隐式绑定,this值在调用过程当中会动态变化,但是咱们就想绑定指定的对象,这时就用到了显示绑定。
显示绑定主要是经过改变对象的prototype关联对象,这里不展开讲。具体使用上,能够经过这两个方法call(...)或apply(...)来实现(大多数函数及本身建立的函数默认都提供这两个方法)。
call与apply是一样的做用,区别只是其余参数的设置上
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 3, }; var obj2 = { a: 4, }; foo.call( obj1 ); // ? foo.call( obj2 ); // ?
打印的结果是3, 4。
这里由于显示的申明了要绑定的对象,因此this就被绑定到了obj上,打印的结果天然就是obj1.a 和obj2.a。
function foo() { console.log( this.a ); } var a = 2; var obj1 = { a: 3, }; var obj2 = { a: 4, }; var bar = function(){ foo.call( obj1 ); } bar(); // 3 setTimeout( bar, 100 ); // 3 bar.call( obj2 ); // 这是多少
前面两个(函数别名、回调函数)打印3,由于显示绑定了,没什么问题。
最后一个打印是3。
这里须要注意下,虽然bar被显示绑定到obj2上,对于bar,function(){...} 中的this确实被绑定到了obj2,而foo由于经过foo.call( obj1 )
已经显示绑定了obj1,因此在foo函数内,this指向的是obj1,不会由于bar函数内指向obj2而改变自身。因此打印的是obj1.a(即3)。
js中的new操做符,和其余语言中(如JAVA)的new机制是不同的。js中,它就是一个普通函数调用,只是被new修饰了而已。
使用new来调用函数,会自动执行以下操做:
从第三点能够看出,this指向的就是对象自己。
看个代码:
function foo(a) { this.a = a; } var a = 2; var bar1 = new foo(3); console.log(bar1.a); // ? var bar2 = new foo(4); console.log(bar2.a); // ?
最后一个打印是3, 4。
由于每次调用生成的是全新的对象,该对象又会自动绑定到this上,因此答案显而易见。
上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:
数是否在new中调用(new绑定)?若是是的话this绑定的是新建立的对象。 数是否经过call、apply(显式绑定)或者硬绑定调用?若是是的话,this绑定的是 指定的对象。 数是否在某个上下文对象中调用(隐式绑定)?若是是的话,this绑定的是那个上下文对象。 果都不是的话,使用默认绑定。若是在严格模式下,就绑定到undefined,不然绑定到 全局对象。 var bar = foo()
在显示绑定中,对于null和undefined的绑定将不会生效。
代码以下:
function foo() { console.log( this.a ); } foo.call( null ); // 2 foo.call( undefined ); // 2
这种状况主要是用在不关心this的具体绑定对象(用来忽略this),而传入null实际上会进行默认绑定,致使函数中可能会使用到全局变量,与预期不符。
因此对于要忽略this的状况,能够传入一个空对象ø,该对象经过Object.create(null)
建立。这里不用{}的缘由是,ø是真正意义上的空对象,它不建立Object.prototype委托,{}和普通对象同样,有原型链委托关系。
1. 这里传null的一种具体使用场景是函数柯里化的使用
最后,介绍一下ES6中的箭头函数。经过“=>”而不是function建立的函数,叫作箭头函数。它的this绑定取决于外层(函数或全局)做用域。
function foo(){ console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); //3
var foo = () => { console.log( this.a ); } var a = 2; var obj = { a: 3, foo: foo }; obj.foo(); //2 foo.call(obj); //2 ,箭头函数中显示绑定不会生效
function foo(){ return function(){ console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //2
function foo(){ return () => { console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //3
经过上面两个列子,咱们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的做用域</font>,对于前面的4种绑定规则是不会生效的。它也是做为this机制的一种替换,解决以前this绑定过程各类规则带来的复杂性。
注意:对于ES6以前,箭头函数的替换版本是这样的
// es6 function foo(){ return () => { console.log( this.a ); } } var a = 2; var obj = { a: 3, foo: foo }; var bar = obj.foo(); bar(); //3
经过上面两个列子,咱们看到箭头函数的this绑定<font color="red">只取决于外层(函数或全局)的做用域</font>,对于前面的4种绑定规则是不会生效的。它也是做为this机制的一种替换,解决以前this绑定过程各类规则带来的复杂性。
注意:对于ES6以前,箭头函数的替换版本是这样的
// es6 function foo(){ return () => { console.log( this.a ); } } // es6以前的替代方法 function foo(){ var self = this; return () => { console.log( self.a ); } }
咱们在使用js的过程当中,对于this的理解每每以为比较困难,再调试过程当中有时也会出现一些不符合预期的现象。不少时候,咱们都是经过一些变通的方式(如:使用具体对象替换this)来规避的问题。可问题一直存在那儿,咱们没有真正的去理解和解决它。
本文主要参考了《你不知道的JavaScript(上卷)》,对this究竟是什么,具体怎么绑定的,有什么例外状况以及ES6中的一个优化方向,来完全搞清楚咱们一直使用的this究竟是怎么玩的。