首先,JavaScript的this指向问题并不是传说中的那么难,不难的是机制并不复杂,而被认为很差理解的是逻辑关系和容易混淆的执行上下文。这篇博客也就会基于这两个很差理解的角度来展开,如要要严格的来对this的指向来分类的话,有三类不一样的状况,一种是独立函数执行的指向机制,第二种就是引用指向机制,第三种是new机制下的this指向。而后创建在这三个指向机制的基础上来剖析一些this的常见问题,下面进入正文解析this指向机制:数组
1、独立函数执行的指向机制app
在JavaScript中函数执行能够分为两种状况,一种是函数纯粹的执行,另外一种是被某个对象引用执行。函数纯粹的执行也一般被描述为独立函数执行,这种状况的函数执行内部this指向全局对象,可是在严格模式下独立函数执行this会指向undefined。注意,机制的自己很是简单,可是容易出错的却在函数被调用的机制上,若是再在这个问题上深刻的追溯问题的根源的话,其自己的问题是出在JavaScript对象与对象的属性和方法的归属关系问题。函数
1.1指向全局与指向undefined学习
function foo(){ console.log(this.a); } var a = 2; foo();//2
这个运行结果证实了独立的函数执行的this指向了全局对象,接下来咱们再看看严格模式下的独立函数的this的指向。this
function foo(){ "use strict"; console.log(this.a); } var a = 2; foo();//TypeError: Cannot read property 'a' of undefined
从错误提示能够看出来,严格模式下的this指向是undefined。可是这两个函数的执行能不能就表明所有的纯函数执行就是这个机制呢?spa
1.2this向与赋值机制code
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } var bar = obj.foo; var a = 1; bar();//1
执行结果为何是1?由于bar是一个纯函数执行啊。很难理解这部分机制的大多都是学习后台强属性语言,由于在强属性语言中,属性的归属永远都是一个对象的,可是在JavaScript中全部的函数都是一个独立的个体,它不属于任何对象,又能够是任何对象的方法。当函数在不被任何对象引用执行的时候它就算是一个独立函数,可能有的人会理解为var bar = obj.foo是对象引用啊,记住在JavaScript中,这行代码的函数是赋值,bar得到是foo函数的堆内存地址,不会记录obj的关联性。而在强属性语言中,这种赋值就不仅是把函数赋给一个变量了,同时还会带着这个函数的关联对象的关系一块儿赋给这个变量,因此这就是JavaScript的赋值机制给this带来的问题。对象
关于个赋值机制这段代码可能还不能彻底说服你,由于foo函数被声明在全局,那下面来看看下面这段修改后的代码:blog
var obj = { a:2, foo:function(){ console.log(this.a); } } var bar = obj.foo; var a = 1; bar();//1
引用方法赋值行为,接收方法的变量不会接收引用关系,得到的是一个独立函数的纯粹堆内存引用,这对理解this引用很重要。并且还有值得咱们注意的一个地方就是经过参数的传值行为本质上也只是函数的纯粹赋值而已,不会带着引用关系传递到形参上的。看下面这段代码来理解这种机制:继承
var obj = { a:2, foo:function(){ console.log(this.a); } } function bar(fn){ fn(); } var a = 1; bar(obj.foo);//1
JavaScript传值、赋值本质上都只是将函数的堆内存地址赋给变量而已,而上面这段代码有说明了另外一个问题就是无论在什么地方,函数纯粹的执行它的this指向都是全局(fn嵌套在bar内,可是this仍是指向了全局对象),而后这里又会延伸出一个新的问题,嵌套函数内严格模式,这里有一个细节值得注意。
var obj = { a:2, foo:function(){ console.log(this.a); } } function bar(fn){ "use strict"; fn(); } var a = 1; bar(obj.foo);//1
不是说严格模式下的this指向undefined吗?怎么这里的this仍是指向了全局对象呢?
不错,在严格模式下this指向会被修改成undefined,可是必须是当前做用域被设置了严格模式,看下面这段代码来理解:
var obj = { a:2, foo:function(){ "use strict"; console.log(this.a); } } function bar(fn){ fn(); } var a = 1; bar(obj.foo);// Cannot read property 'a' of undefined
关于纯函数的执行this指向已经所有解析完,接下来咱们继续引用函数执行的this指向机制。
2、引用函数执行的this指向机制
关于引用函数执行的this指向机制比起纯函数执行来讲,要简单的多,惟一存在容易混淆不清的地方就是对象属性与做用域,不少时候咱们都把做用域当作是对象,但实际上不是,他只是在某些状况下有些特性与对象相似而已。
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } var a = 3; obj.foo();//1
其实经过上面的示例咱们能够看到函数内的this指向了引用函数执行的对象。其实从这个例子咱们也能够反推出一个逻辑,那就是纯函数执行其实质并不是是纯粹的函数执行,而是当函数没有被指定的对象引用执行的时候,函数其实质上是被全局对象隐式的引用执行,在严格模式下是被undefined隐式的引用执行。
2.1引用函数执行的this机制与赋值
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } var obj1 = { a:2, obj:obj } var a = 3; obj1.obj.foo();//1
咱们在前面对函数赋值机制作了深刻剖析,再来看看对象赋值。将一个对象赋给另外一个对象,再经过链式调用对象的方法,其本质上this仍是遵循了引用执行机制,this指向直接调用函数的对象,再来看下一个示例就会更清楚了:
function foo(){ console.log(this.a); } var obj = { foo:foo } var obj1 = { a:2, obj:obj } var a = 3; obj1.obj.foo();//undefined
当引用执行函数的对象没有函数执行须要的参数时,这个参数的值会默认为undefined,必定要注意,这种赋值调用,对象与对象之间并非继承关系,仅仅只是一个调用关系。在对象原型链的博客里我会详细剖析引用执行函数的对象this指向及内部原型链的查询机制。
2.2引用函数执行的this指向与call和apply
function foo(){ console.log(this.a); } var obj = { a:1 } foo.call(obj);//1
上面的示例证实函数this指向了传入call方法的实参obj上了,而且会当即执行这个函数,call方法其本质上是一个很是简单的操做,只不过是实现了对象动态添加方法而且当即执行的一个行为而已。因此上面的代码能够显示的用下列代码静态添加方法并引用执行来完成:
function foo(){ console.log(this.a); } var obj = { a:1, foo:foo } obj.foo();//1
上面这两段代码从本质上来将是彻底对等的操做,只不过经过call方法实现了动态添加执行,在obj对象上实质上是找不到foo这个方法的。关于call的this指向解析清楚之后,关于apply就很容易了,这两个方法的核心功能实际上是同样的,都是将方法动态的绑定到指定的对象上而后引用执行这个方法。既然是方法要执行就必然会涉及到参数传递,这两个方法的差别就在于参数的传递有写差别,其余的彻底一致。
function foo(a,b,c){ console.log(this.a + ";参数:" + a + b + c); } var obj = { a:1 } var a = "a", b = "b", c = "c", arr = ["a","b","c"]; foo.call(obj,a,b,c);//1;参数:abc foo.apply(obj,arr);//1;参数:abc
call的参数传递和普通的函数传参一致,都是单个一一传入,apply的参数传递方式是将实参打包成一个数组传入,数组的下标与形参的顺序一一对应,由于call和apply方法的第一个参数须要传入函数执行的引用对象,因此函数执行的参数都是从第二位开始传入。
关于函数执行的this指向解析所有解析完,还记得在博客开头的我有提到关于this指向的执行上下文混淆的问题吗?这个问题主要出在当引用对象是一个回调函数的时候,就会容易混淆this的指向,这是由于咱们常常把执行性上下单纯的当作一个对象来对待,这样的观点致使咱们对this的指向很容易混淆不清,下面咱们来看一段代码:
function foo(){ var a = 1; function bar(){ var a = 2 console.log(this.a); } return bar; } var a = 3; function baz(){ console.log(this.a); } baz.call(foo());//undefined
有点小惊讶吧,不是2,也不是3,而是undefined,这里有几个误区:
1.一般咱们都把call和apply两个方法的第一个参数成为执行上下文,这不彻底正确。
2.由于全局做用域会把变量和函数转成自身属性(即全局对象的属性),可是其余的函数的做用域不能。
3.一般咱们把做用域成为执行上下文当作是一个对象,误认为把做用域内的变量和方法及调用执行方法的对象的属性都归为执行上下文对象的属性,这个误区能够算是上面两个误区的增强版。
这里咱们先要试着理解执行上下文究竟是个什么东西?而后才能弄清楚this指向的究竟是什么?
咱们一般所表达的执行上下文其实质上包含了三个部分:引用方法执行的对象,方法自身执行的做用域,除自身做用域的全部上层做用域。而this指向的是引用方法执行的对象。
上面的代码中,baz.call(foo())能够理解为时bar.baz()这样的引用执行方式,可是由于bar上没有baz这个方法,这样写会报错,而call和apply实质上在调用函数以前给引用执行方法的对象临时的作了一个添加方法的处理,只是这个方法会执行完之后就会被删除,表面上咱们能够这么理解,可是引擎内部是不回作这种损耗效率的事情,call和apply的内部机制应该是一种又有效的动态的添加执行行为,这部分没有深刻研究,有兴趣的朋友能够在评论区一块儿讨论。
其实这里应该还有一个关于bind的柯里化的this指向问题,这部分我写到柯里化那部分博客的时候,再考虑是在这篇博客上添加仍是在柯里化部分的博客上扩展。
3、new机制下的this指向
关于new机制的this指向相对来讲是最简单的,由于这个机制下的this是一个固定指向,只出如今经过function实例化对象的时候,不会出如今程序逻辑中。由于涉及一些对象实例化因此会在这里扩展一点对象实例化的内容,由于是一个交叉知识点,后期还会在对象原型机制的时候再作管理解析。
function Car(name,height,lang,weight,health){ this.name = name; this.height = height; this.lang = lang; this.weight = weight; this.health = health; this.run = function(){ this.health --; } } var BMW = new Car("BMW",1400,4900,1400,100); console.log(BMW);
在new机制下的this,指向新建立的对象。这个新建立的对象的描述是一个很是模糊的说法,好比从字面量的层面来理解,上面的示例中,能够说this是指向BMW,若是对JavaScript的堆栈存储关系有所了解就会知道这个描述能够说是错误的,由于当咱们var一个新的变量,而后将BMW的值赋给这个新的变量,这个新的变量并不会记忆BWM的引用赋值关系,而是直接将BMW指向的堆内存当成本身的,这时候这个新的变量和BMW就同时平等的关联着这个同一个对象的堆内存地址。
从上面的思考能够看出,这个新建立的对象从严格意义上来说并不能说是指向了某个变量,还有一种状况就是只经过new关键字示例化了对象,但并无给某个变量赋值操做:
new Car("BMW",1400,4900,1400,100);
这个不作赋值操做的对象实例化,在实际开发中咱们虽然不会这么作,可是本质上确实是实例化了一个对象,这就进一步说明前面的思考是值得探讨的。
咱们连这个新对象是谁都不知道,就说this指向了新建立的对象,这有点太不负责任了吧!!!
因此这里,重点须要探讨的是这个新的对象究竟是谁?下面我经过一个流程图来描述function的new机制实例化对象的全过程来讲明这个问题:
经过上图基本上就能够彻底理解function构造对象实例化的内部机制了,基本上就是一下几个步骤:
1.函数执行的前一刻建立变量对象后,在变量对象上隐式的生成一个属性this,并赋值{}。
2.变量提高参数统一,函数声明提高完成后开始执行,经过this.xxx的方式给this对象添加属性,而后执行赋值;(而且还会隐式的添加原型指向:__proto__指向Object,在原型上的constructor的值赋为构造函数)。
3.函数执行完的前一刻隐式的执行return this操做。
以上就是this指向规则的所有剖析内容,ES6的胖箭头this词法不在这里分析,后期会有关于ES6的详细内容博客。