彻底搞懂 JS 中 this 机制
this 是什么
了解this之前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法作使用域。假如仅通过this的英文解释,太容易产生误导了。它实际是在函数被调使用时才发生的绑定,也就是说this具体指向什么,取决于你是怎样调使用的函数。
this 的四种绑定规则
this的4种绑定规则分别是:默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高。
默认绑定
什么叫默认绑定,即没有其余绑定规则存在时的默认规则。这也是函数调使用中最常使用的规则。
来看这段代码:
pre class=”brush:js;toolbar:false”>function foo() {} console.log( this.a );var a = 2;foo(); //打印的是什么?
foo()
打印的结果是2。
由于foo()是直接调使用的(独立函数调使用),没有应使用其余的绑定规则,这里进行了默认绑定,将全局对象绑定this上,所以this.a 就解析成了全局变量中的a,即2。
注意:在严格模式下(strict mode),全局对象将无法用默认绑定,即执行会报undefined的错误
pre class=”brush:js;toolbar:false”>function foo() { “use strict”; console.log( this.a );}var a = 2;foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
隐式绑定
除了直接对函数进行调使用外,有些情况是,函数的调使用是在某个对象上触发的,即调使用位置上存在上下文对象。
pre class=”brush:js;toolbar:false”>function foo() { console.log( this.a );}var a = 2;var obj = { a: 3, foo: foo};obj.foo(); // ?
obj.foo()
打印的结果是3。
这里foo函数被当做引使用属性,被增加到obj对象上。这里的调使用过程是这样的:
获取obj.foo属性 -> 根据引使用关系找到foo函数,执行调使用
所以这里对foo的调使用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,所以this.a被解析成了obj.a,即3。
多层调使用链
pre class=”brush:js;toolbar:false”>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。
同样,我们看下函数的调使用过程:
先获取obj1.obj2 -> 通过引使用获取到obj2对象,再访问 obj2.foo -> 最后执行foo函数调使用
这里调使用链不只一层,存在obj1、obj2两个对象,那么隐式绑定具体会绑哪个对象。这里准则是获取最后一层调使用的上下文对象,即obj2,所以结果显然是4(obj2.a)。
隐式丢失(函数别名)
注意:这里存在一个圈套,大家在分析调使用过程时,要特别小心
先看个代码:
pre class=”brush:js;toolbar:false”>function foo() { console.log( this.a );}var a = 2;var obj = { a: 3, foo: foo};var bar = obj.foo;bar(); //?
bar() 打印的结果是2。
为什么会这样,obj.foo 赋值给bar,那调使用
bar()
为什么没有触发隐式绑定,用的是默认绑定呢。
这里有个概念要了解清楚,obj.foo 是引使用属性,赋值给bar的实际上就是foo函数(即:bar指向foo本身)。
那么,实际的调使用关系是:通过bar找到foo函数,进行调使用。整个调使用过程并没有obj的参数,所以是默认绑定,全局属性a。
隐式丢失(回调函数)
pre class=”brush:js;toolbar:false”>function foo() { console.log( this.a );}var a = 2;var obj = { a: 3, foo: foo};setTimeout( obj.foo, 100 ); // ?
打印的结果是2。
同样的道理,尽管参传是
obj.foo
,由于是引使用关系,所以传参实际上传的就是foo对象本身的引使用。对于
setTimeout
的调使用,还是 setTimeout -> 获取参数中foo的引使用参数 -> 执行 foo 函数,中间没有obj的参加。这里仍旧进行的是默认绑定。
显示绑定
相对隐式绑定,this值在调使用过程中会动态变化,可是我们就想绑定指定的对象,这时就使用到了显示绑定。
显示绑定主要是通过改变对象的prototype关联对象,这里不开展讲。具体用上,能通过这两个方法call(…)或者apply(…)来实现(大多数函数及自己创立的函数默认都提供这两个方法)。
call与apply是同样的作使用,区别只是其余参数的设置上
pre class=”brush:js;toolbar:false”>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。
硬绑定
pre class=”brush:js;toolbar:false”>function foo() { console.log( this.a );}var a = 2;var obj1 = { a: 3,};var obj2 = { a: 4,};var bar = function(){ foo.call( obj1 );}bar(); // 3setTimeout( bar, 100 ); // 3bar.call( obj2 ); // 这是多少
前面两个(函数别名、回调函数)打印3,由于显示绑定了,没什么问题。
最后一个打印是3。
这里需要注意下,尽管bar被显示绑定到obj2上,对于bar,function(){…} 中的this的确被绑定到了obj2,而foo由于通过
foo.call( obj1 )
已经显示绑定了obj1,所以在foo函数内,this指向的是obj1,不会由于bar函数内指向obj2而改变自身。所以打印的是obj1.a(即3)。
new 绑定
js中的new操作符,和其余语言中(如JAVA)的new机制是不一样的。js中,它就是一个普通函数调使用,只是被new修饰了而已。
用new来调使用函数,会自动执行如下操作:
假如函数没有返回其余对象,那么new表达式中的函数调使用会自动返回这个新对象。
从第三点能看出,this指向的就是对象本身。
看个代码:
pre class=”brush:js;toolbar:false”>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上,所以答案显而易见。
绑定规则优先级
上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:
pre class=”brush:js;toolbar:false”>数能否在new中调使用(new绑定)?假如是的话this绑定的是新创立的对象。数能否通过call、apply(显式绑定)或者者硬绑定调使用?假如是的话,this绑定的是 指定的对象。数能否在某个上下文对象中调使用(隐式绑定)?假如是的话,this绑定的是那个上下文对象。果都不是的话,用默认绑定。假如在严格模式下,就绑定到undefined,否则绑定到 全局对象。 var bar = foo()
规则例外
在显示绑定中,对于null和undefined的绑定将不会生效。
代码如下:
pre class=”brush:js;toolbar:false”>function foo() { console.log( this.a );}foo.call( null ); // 2foo.call( undefined ); // 2
这种情况主要是使用在不关心this的具体绑定对象(使用来忽略this),而传入null实际上会进行默认绑定,导致函数中可可以会用到全局变量,与预期不符。
所以对于要忽略this的情况,能传入一个空对象?,该对象通过
Object.create(null)
创立。这里不使用{}的起因是,?是真正意义上的空对象,它不创立Object.prototype委托,{}和普通对象一样,有原型链委托关系。
1. 这里传null的一种具体用场景是函数柯里化的用
扩展:箭头函数
最后,详情一下ES6中的箭头函数。通过“=>”而不是function创立的函数,叫做箭头函数。它的this绑定取决于外层(函数或者全局)作使用域。
case 1 (正常调使用)
普通函数
pre class=”brush:js;toolbar:false”>function foo(){ console.log( this.a );}var a = 2;var obj = { a: 3, foo: foo};obj.foo(); //3
箭头函数
pre class=”brush:js;toolbar:false”>var foo = () => { console.log( this.a );}var a = 2;var obj = { a: 3, foo: foo};obj.foo(); //2foo.call(obj); //2 ,箭头函数中显示绑定不会生效
case 2 (函数回调)
普通函数
pre class=”brush:js;toolbar:false”>function foo(){ return function(){ console.log( this.a ); } }var a = 2;var obj = { a: 3, foo: foo};var bar = obj.foo();bar(); //2
箭头函数
pre class=”brush:js;toolbar:false”>function foo(){ return () => { console.log( this.a ); } }var a = 2;var obj = { a: 3, foo: foo};var bar = obj.foo();bar(); //3
通过上面两个列子,我们看到箭头函数的this绑定只取决于外层(函数或者全局)的作使用域,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,处理之前this绑定过程各种规则带来的复杂性。
注意:对于ES6之前,箭头函数的替换版本是这样的
pre class=”brush:js;toolbar:false”>// es6function foo(){ return () => { console.log( this.a ); } }var a = 2;var obj = { a: 3, foo: foo};var bar = obj.foo();bar(); //3
通过上面两个列子,我们看到箭头函数的this绑定只取决于外层(函数或者全局)的作使用域,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,处理之前this绑定过程各种规则带来的复杂性。
注意:对于ES6之前,箭头函数的替换版本是这样的
pre class=”brush:js;toolbar:false”>// es6function foo(){ return () => { console.log( this.a ); } }// es6之前的替代方法function foo(){ var self = this; return () => { console.log( self.a ); } }
总结
我们在用js的过程中,对于this的了解往往觉得比较困难,再调试过程中有时也会出现少量不符合预期的现象。很多时候,我们都是通过少量变通的方式(如:用具体对象替换this)来规避的问题。可问题一直存在那儿,我们没有真正的去了解和处理它。
本文主要参考了《你不知道的JavaScript(上卷)》,对this究竟是什么,具体怎样绑定的,有什么例外情况以及ES6中的一个优化方向,来彻底搞清楚我们一直用的this究竟是怎样玩的。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 彻底搞懂 JS 中 this 机制