首先,JavaScript的this指向问题并非传说中的那么难,不难的是机制并不复杂,而被认为不好理解的是逻辑关系和容易混淆的执行上下文。这篇博客也就会基于这两个不好理解的角度来展开,如要要严格的来对this的指向来分类的话,有三类不同的情况,一种是独立函数执行的指向机制,第二种就是引用指向机制,第三种是new机制下的this指向。然后建立在这三个指向机制的基础上来剖析一些this的常见问题,下面进入正文解析this指向机制:

一、独立函数执行的指向机制

在JavaScript中函数执行可以分为两种情况,一种是函数纯粹的执行,另一种是被某个对象引用执行。函数纯粹的执行也通常被描述为独立函数执行,这种情况的函数执行内部this指向全局对象,但是在严格模式下独立函数执行this会指向undefined。注意,机制的本身非常简单,但是容易出错的却在函数被调用的机制上,如果再在这个问题上深入的追溯问题的根源的话,其本身的问题是出在JavaScript对象与对象的属性和方法的归属关系问题。

1.1指向全局与指向undefined

function foo(){
    console.log(this.a);
}
var a = 2;
foo();//2

这个运行结果证明了独立的函数执行的this指向了全局对象,接下来我们再看看严格模式下的独立函数的this的指向。

function foo(){
    "use strict";
    console.log(this.a);
}
var a = 2;
foo();//TypeError: Cannot read property 'a' of undefined

从错误提示可以看出来,严格模式下的this指向是undefined。但是这两个函数的执行能不能就代表全部的纯函数执行就是这个机制呢?

1.2this向与赋值机制

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函数被声明在全局,那下面来看看下面这段修改后的代码:

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指向机制。

二、引用函数执行的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指向问题,这部分我写到柯里化那部分博客的时候,再考虑是在这篇博客上添加还是在柯里化部分的博客上扩展。

三、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机制实例化对象的全过程来说明这个问题:

JavaScrpt中的this指向规则-LMLPHP

通过上图基本上就可以完全理解function构造对象实例化的内部机制了,基本上就是一下几个步骤:

1.函数执行的前一刻创建变量对象后,在变量对象上隐式的生成一个属性this,并赋值{}。

2.变量提升参数统一,函数声明提升完成后开始执行,通过this.xxx的方式给this对象添加属性,然后执行赋值;(并且还会隐式的添加原型指向:__proto__指向Object,在原型上的constructor的值赋为构造函数)。

3.函数执行完的前一刻隐式的执行return this操作。

以上就是this指向规则的全部剖析内容,ES6的胖箭头this词法不在这里分析,后期会有关于ES6的详细内容博客。

01-07 19:48