JS中this的深入理解

前言

以下的观点来自我对《你不知道的JavaScrpt 》 上册中第二章关于this的理解。

里面的代码和文本大多来自这本书上。我只是总结以下。

下面的所有代码,都是在window浏览器运行环境下的结果。使用Node.js运行有些代码会有不一样的结果。这个问题,我也不懂。请见谅。

首先来说明两个关于对this认识的误解。

误解

误解一:this指向自身

很多新手就像我一样,在没有看这本书之前,会有一种感觉,觉得this这东西还不好理解,this指向的不就是函数自身,多大点事, so easy。

现在我只想拍拍以前的我说一句:年轻人。

不相信,那就看看下面的一段代码。

function foo(num) {
 console.log( "foo: " + num );
 // 记录 foo 被调用的次数
 this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
 if (i > 5) {
 foo( i );
 }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count );   // 0 -- WTF?
复制代码

如果是像以前一样不了解this的我一样,看到这样的输出结果,一定感觉很疑问?

为什么输出的结果会是0,而不是4,this.count++不应该会改变foo.count的值吗?

但是事实上呢,foo函数里面的this绑定的是全局对象window。这段代码在 无意中创建了一个全局变量 count,它的值为 NaN。this.count就是它,值为NaN。

其中this使用的是默认绑定这条规则。你可能会有疑问,不要急,看下面。

误解二:它的作用域

第二种常见的误解是,this 指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。

看看这段代码:

function foo() {
  var a = 2;
  this.bar();
 }
 function bar() {
  console.log( this.a );
 }
 foo(); // ReferenceError: a is not defined
复制代码

这段代码中的错误不止一个。如果是在浏览器中运行,结果就是undefine。但是使用node运行这段代码,程序直接报错:TypeError: this.bar is not a function。

其实在浏览器运行环境下我倒是能理解。this绑定的就是window全局对象。在全局对象上没有a这个变量,所以它的值为undefine.

《你不知道的JavaScrpt》中是这样解释上面的代码

​ 这段代码中的错误不止一个。虽然这段代码看起来好像是我们故意写出来的例子,但是实际上它出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感)地展示了 this 多么容易误导人。

​ 首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。

​ 此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让 bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一 个词法作用域内部的东西。

每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

上面的代码我现在都还有点懵,所以我就不多说了,想要了解,就去看书吧。

this到底是什么

​ this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 ​ 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

在《你不知道的JavaScrpt》书中有关于this调用位置的一点解释。但我理解的也不是很深,估计是错的我也就不多说了。下面来说说这篇文章的重点,this绑定的四条绑定规则。只有理解了这四条规则,你才能看到this的真面目。划重点,划重点,划重点。


绑定规则

第一条:默认绑定

默认绑定就是,函数调用类型:独立函数调用时使用的。可以把这条规则看作是无法应用其他规则时的默认规则。

思考以下下面的代码

function foo() {
  console.log( this.a );
 }

 var a = 2;
 foo(); // 2 
复制代码

看上面的代码,你要注意最下面的两句。在全局对象上创建了一个变量 a =2 ,调用函数的方式 也是 直接使用函数名调用

我就来说说这两句为什么很重要。

  1. 在函数中的this 使用的是默认绑定这条规则,它会绑定到全局对象window对象上,this.a 也就是全局变量 a
  2. 那默认绑定到底是怎么使用的呢?就是运行 foo(); 这句时绑定的。
  3. 在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

如果使用严格模式(strict mode) ,那么情况又会有不一样。具体请自己去看书理解,因为现在我也不懂什么是严格模式,所以解释不清楚。

function foo() {
  console.log(this.a);     //2
  console.log(a);          //undefine
  console.log(this);       //window
  var a = 3;
  console.log(a);          //3
  function bar() {
    console.log(this.a);   //2
    console.log(a);        //3
    console.log(this);     //window
    var a = 4;
    console.log(a);
    function bat() {
      console.log(this.a); //2
      console.log(a);      //4
      console.log(this);   //window
      var a = 5;
      console.log(a);      //5
    }
    bat();
  }
  bar();
}
console.log(a);            //undefine
var a = 2;
console.log(a);            //2
foo();                     // 2
复制代码

上面的代码写的很不好,大佬理解以下,我只是一个小白,有很多不懂的东西,只能用console.log()输出来,慢慢理解。

我就来说说我对上面代码的理解。

  1. 上面代码中的三个函数调用,都是直接使用自身的函数名调用,调用位置没有任何的上下文对象。使用的都是默认绑定这条规则,所以函数内部的this 所绑定的都是全局window对象。this.a 也就是最外层的变量a,所以值就是2。
  2. 在每个函数内部输出自身的a变量,第一条console.log(a);和第二条console.log(a); 输出的结果不一样。这就涉及了词发作用域的问题还有关于引擎运行的问题

这个问题就更加的麻烦,我也就简单的发表以下我的理解。理解的不是很深,大佬请理解以下,有错也欢迎指正。

  1. 首先呢,不知道是编译器,引擎,作用域,这三者的其中一个会在程序运行之前,把所有的声明都在开始时编译一下,有点像是把var a;写在最上面。所以第一个console.log(a),可以找到a这个变量,但a又没有值,所以输出的结果会是undefine.
  2. 但是第二个a,因为已经有了 a = 2;所以第二个console.log(a)输出的值就是2.

讲的不是很明白,因为我理解的也不是很透彻,所以请大佬见谅。

第二条:隐式绑定

隐式绑定在我看来是这四条规则中最复杂的。因为这条规则需要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或包含。这样说有点绕,反正先思考下面的代码:

function foo() {
 console.log( this.a );
}
var obj = {
 a: 2,
 foo: foo
};
obj.foo(); // 2
复制代码

​ 首先要注意的还是foo()的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj 对象。这是书上的原话。总结以下就是,对象obj中的foo属性,只是最外层函数foo的一个引用

​ 然而,调用的时候,使用了obj 上下文来引用函数,因此可以说,函数被调用时obj对象“拥有” 或者 “包含”它。函数中的this绑定的就是obj这个对象,因此this.a和obj.a是一样的。

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

function foo() {
 console.log( this.a );
}
var obj2 = {
 a: 42,
 foo: foo
};
var obj1 = {
 a: 2,
 obj2: obj2
};
obj1.obj2.foo(); // 42
复制代码

上面的代码最后输出的结果是 42,我的理解是foo函数的this被绑定的是obj2这个对象,符合最后一层影响调用位置,因为最顶层影响调用位置我还没有看到这种情况,所以我也不理解就不多说了。

使用隐式调用,有些情况会产生this绑定被丢失的问题,然后this就会使用默认绑定这条规则。

隐式丢失

第一种情况:使用引用赋值时会产生this绑定丢失。

function foo() {
 console.log( this.a );
}
var obj = {
 a: 2,
 foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
复制代码

​ 是不是有一点疑惑?如果按照隐式绑定这条规则来说的话。bar()输出的结果不应该是2吗? 但是事实上却不是这样的。我就说说我的理解。

​ bar 是 obj.foo 的一个引用,但实际上,它是foo()函数的一个 引用。所以bar()其实就和foo()函数一样。使用的是默认绑定规则,this绑定的就是window全局对象,所以输出的结果this.a 值为oops, global。

第二种情况更加的隐蔽:发生在传入参数时

function foo() {
 console.log( this.a );
}
function doFoo(fn) {
 // fn 其实引用的是 foo
 fn(); // <-- 调用位置!
}
var obj = {
 a: 2,
 foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
复制代码

是不是又有点懵。其实这里问题有点深,我说不清楚。只能发表一下自己的看法。

​ doFoo( obj.foo ) 这一句,其实是和 fn = obj.foo是一个道理,如果你能这样理解的话,我想疑问就没有了。很明显吗,fn是foo函数的一个引用,调用时又是fn(),所以和使用foo()没有什么差别,this绑定的还是window全局对象。所以doFoo( obj.foo ) 的值为"oops, global“。

第三条:显示绑定

这条我觉得是最好理解的。就是使用call()或apply()或bind(),强制绑定到某个对象上。

function foo() {
 console.log( this.a );
}
var obj = {
 a:2
};
foo.call( obj ); // 2
复制代码

foo.call( obj )就是强制把this绑定到obj对象上。

但是显示绑定也不能解决绑定丢失的问题。

在《你不知道的JavaScrpt 》书写了两种方法解决这个问题。我也不多说,需要的话就自己看书吧。

第四条:new绑定

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

上面是《你不知道的JavaScrpt 》中对 使用new来调用函数的解释。


下面是《JavaScript高级程序设计》中对 使用new来调用函数的解释。

使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

写了这么多,先看代码吧

function foo(a) {
 this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
复制代码
  1. 使用new时,会先创建一个对象,就假如是obj.然后this就会被绑定到这个对象上.就像obj.foo().
  2. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回obj对象。
  3. obj.foo()就是默认的情况。this也就默认绑定在obj这个对象上。

所以上面的代码,this代表的就是bar对象。

优先级

​ 因为有四条规则,在写代码时,this可能会写的比较复杂。所以这四条规则必须要有优先级。我也不多哔哔,就直接给出结果吧。

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()

好了,这篇文章就写到这里了。其实在《你不知道的JavaScrpt 》书上还有关于this的一些特殊情况的解释。我理解的不是很透彻,可能说出来是错的。我也就不写了,想要了解的话就自己去书上看吧。

这篇文章写的也不是很好,这只是我自己的总结,如果有错,请大佬见谅。理解理解我只是一个小白,还有很多要学的东西。谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值