”温故“ 之 一次性弄懂this

206 阅读9分钟

在后边的学习中经常会遇到this,相信很多人和我一样,到现在为止,对this的具体指向还是很茫然

谁调用,this就指向谁

这就像一本保真的降龙十八掌内功心经一样,流传甚广

但是没有招法,该从何下手呢?

江湖中各种方法各有优势,先分享一下我觉得最好用的方式(the best),并找了其他两个有代表性的,一同分享给大家。

1、I Don‘t Know JS

在看完了诸多前辈的博客之后,可以说是一个跟斗跳到了云里雾里(玩笑玩笑)。

回过头来想,啥玩意是this(此为自己对You-Dont-Know-JS的理解和搬运,也可以去看原文,在下边有链接)

通俗来说,Javascript 是一个文本作用域的语言, 一个变量的作用域,在写这个变量的时候确定,是静态的。 而this 关键字就是为了在 JS 中加入动态作用域(就是在自己地盘上不够得劲了,我要出去转转)而做的努力。用大家 难懂的话来说就是:

this 不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this 绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间 引用 this

这句话什么意思呢?就是说,this不是我们在实例化一个window对象,声明一个函数.......等时侯绑定的,他是为了实现动态,在这些东西运行时绑定的

划重点,强烈建议记住这句话,是在运行时绑定。。。。。。

我们在声明一个函数后马上执行,像这样

var a = 1
//声明时,this没有绑定,假装那里就是个马赛克
function print(){
    console.log(this.a)
}
​
//在此处调用了,我们很明显知道这是全局环境下,此时的call-tack就是全局环境,那么this就是全局对象
print()

有人说这个太简单了,当然好解释,那么看个复杂的:

//此时,this没被绑定
var point = {
  x: 0,
  moveTo: function(x, y) {
    console.log(this)
  }
}
​
//调用绑定this
point.moveTo()

使用point.moveTo()对函数进行了调用,通俗讲就是point对象通过它的key去访问它的value,所以很容易理解,函数是在point对象处(call-site)被调用的,因此,this指的是point。

所以根据书中的内容梳理总结了一个更好记的方法

具体来说有四种, 优先级有低到高分别如下:

  1. 默认的 this 绑定, 就是说 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格模式下, this 就是全局变量 (Node 环境中的 global, 浏览器环境中的 window)

  2. 隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.

    • this 绑定最常让人沮丧的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据 strict mode 的状态,其结果不是全局对象就是 undefined

      const foo = {
          bar: 10,
          fn: function() {
             console.log(this)
             console.log(this.bar)
          }
      }
      ​
      var fn1 = foo.fn
      fn1()
      
  1. 强制绑定: foo.call(obj, ...), foo.apply(obj,[...]), foo.bind(obj,...),this就是第一个参数 obj
  2. 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象

那么就有以下的流程(按顺序判断)

  1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。

    var bar = new foo()

  2. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。有call之类的

    var bar = foo.call( obj2 )

  3. 函数是通过对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个对象。被调用时,前边有东西

    var bar = obj1.foo()

  4. 否则,使用默认的 this默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。被调用时前边没东西

    var bar = foo()

课后习题

var point = {
  x: 0,
  y: 0,
  moveTo: function(x, y) {
      //定义时没有绑定
    function moveX(x) {
      this.x = x
    }
      //调用时。按顺序判断是4,即默认绑定,所以输出undefined
    console.log(moveX(x))
  }
}
​
point.moveTo()
const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o3 = {
    text: 'o3',
    fn: function() {
        //如果在这个地方有this,那就是O3这个obj
        var fn = o1.fn
        //在这调用,此时绑定this,前边没东西,所以指的是全局对象,因此是undefined
        return fn()
    }
}
​
console.log(o1.fn())  //o1,这个太简单,没什么要解释的
//对于这个,还是看调用位置,虽然fn在这调用了,但是fn里没有this啊
console.log(o3.fn())
​
​

画个示意图(巧记而已):

2、看阮一峰的博客后有感

他把this的使用分为以下几个部分,这也是网上大多数的方法

情况一:纯粹函数的使用(为了跟后边的构造函数区分开,这里应该是被当作方法使用时?)

//这里的this就代表全局对象,所以运行结果是1
var x = 1
function print(){
    console.log(this.x)
}
​
print()  //1//所以在函数里直接使用的话,this指向的都是全局对象吗
function print2(){
    let x = 1
    console.log(this.x)
}
​
print() // undefined
​
​
//let 在全局环境下的声明和var也不一样吗?见下边的分析
let y = 1
function print2(){
    console.log(this.y)
}
​
print2() // undefined

情况二:作为对象方法的调用

function print(){
    console.log(this.x)
}
​
obj = {}
obj.x = 1
obj.y = print
​
obj.y()  //1

此时this指向的是上级对象

有一种情况

function print(){
    console.log(this.x)
}
​
obj1 = {x:2}
obj2 = {x:5}
obj1.y = print
​
obj2.a = obj1.y
obj2.a()  //5

虽然这里是赋值操作,但最终调用的是obj2,谁调用this就是谁

情况三:作为构造函数调用

function Test{
    this.x = 1
    console.log(this.x)
}
​
let newObj = new Test() //1

此时this指向的是我们new的对象

这个的过程可以去看一下构造函数的相关知识arr的reverse方法是在哪里定义? - 知乎 (zhihu.com)

情况四:apply调用

apply()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数。

var x = 0;
function test() {
 console.log(this.x);
}
​
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply() // 0

apply()的参数为空时,默认调用全局对象。因此,这时的运行结果为0,证明this指的是全局对象。

如果把最后一行代码修改为

obj.m.apply(obj); //1

此时,this就代表apply的第一个参数,也就是obj,对于apply、call和bind其实都是去改变传入函数的this

比如:很多时候,我们想让this指向自己,为了代码的方便,比如在迭代中时

function foo(num) {
    console.log( "foo: " + num )
​
    // 追踪 `foo` 被调用了多少次
    this.count++
}
​
foo.count = 0
​
for (var i=0; i<10; i++) {
    if (i > 5) {
        foo(i)
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9// `foo` 被调用了多少次?
console.log( foo.count )  // 0 -- 这他妈怎么回事……?

但最后发现是0,学了上述的this指向(不管是方方的,还是网络上常用的),我们可以理解到,在for循环里,foo()每次调用时的this指向的是全局对象(在web环境下指代的是window,node环境下是global),所以this.count相当于给全局对象创建了个属性,那么怎么更改他呢?

function foo(num) {
    console.log( "foo: " + num )
​
    // 追踪 `foo` 被调用了多少次
    this.count++
}
​
foo.count = 0for (var i=0; i<10; i++) {
    if (i > 5) {
        //利用call去更改
        foo.call(foo,i)
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9// `foo` 被调用了多少次?
console.log( foo.count )  //4

注:

作为补充,有些知识点需要知道

一、执行上下文

执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object) , 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。

全局上下文是最外层的上下文,在浏览器中,全局上下文的对象就是我们 常说的window对象,因此所有通过 var 定 义的全局变量和函数都会成为 window 对象的属性和方法

二、区分一下let、var、const

首先这三个关键字都是声明变量的方式

  1. var关键字

    • 使用方法
    //声明一个变量,并赋值
    var message = 'a'
    ​
    //可以被这么重写,但是不推荐
    message = 'b'
    
    • 声明作用域

      使用 var 操作符定义的变量会成为包含它的函数的局部变量,在函数内部定义变量,在函数退出时会被立即销毁,这在ES5之前常用,比如避免一些变量被全局使用

    function sum (a){
        var b = 1
        return a + b
    }
    ​
    console.log(sum(1))    //2
    console.log(b)        //b is not defined
    

    不过他可以这样转换为全局变量

    //相当于前边有个var b   b的值是undefined
    //var b
    function sum (a){
         b = 1
        return a + b
    }
    ​
    console.log(sum(1))    //2
    console.log(b)        //1
    

    但这样的方法不推荐,因为很多时候你并不知道你有没有在外边使用过这个变量,如果贸然使用此种方法就会把它重写,带来不方便

    //在代码中使用过
    var b = 8
    //....
    function sum (a){
         b = 1
        return a + b
    }
    ​
    console.log(sum(1))    //2
    console.log(b)        //1
    

    从以上我们可以看出来,var关键字声明的范围是函数作用域和全局作用域

    • 声明提升

      见函数内容相关博客

    var的这些特性带来很多不方便

    • 声明提升带来的变量覆盖

      var a = 'hello'function sayHello(){
          console.log(a)
          if(false){
              var a = 'no'
          }
      }
      ​
      sayHello() //undefined
      
    • 计数的变量成为全局变量

      //在全局作用域下的if 或 for 或 {} 中var声明的变量都是全局变量,for 循环定义的迭代变量会渗透到循环体外部
      for(var i = 1; i < 5 ; i++){
          console.log(i)
      }
      ​
      console.log(i) //5
      
  2. let 关键字

    • 声明范围

      这是它和var的最大区别,let 的声明范围是块级作用域(在ES6 出现)

      何为块级作用域,简单理解,他只在它被使用的那个上下文是可用的,在孩子或者父亲那里都不可以

      if (true){
          var name = 'BlueMiao'
          console.log(name)
      }
      ​
      console.log(name)  //BlueMiao
      ​
      ​
      if (true){
          let name1 = 'BlueMiao1'
          console.log(name1)
      }
      ​
      console.log(name1)  // is not defined
      

      由于它的作用域范围,因此它支持嵌套重复定义

    • let 声明的变量不会在作用域中被提升。

    • 使用 let 在全局作用域中声明的变量不会成为 window 对象的属性

    • 在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合 并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同 时也就不可能在没有声明的情况下声明它。

      所以有下边的情况

      var name 
      var name 
      var name = 'Blue'
      console.log(name)   //Bluelet name
      let name  //报错
      
  3. const 关键字

    与let基本相同 ,只是必须声明时要立即初始化

    const a = 1
    const a   //报错const a = 2
    a = 1    //报错//但是当变量是对象时,改变不会报错
    const a = []
    a.push('a')
    

《javascript高级程序语言设计》中这么说

  1. 不使用 var
  2. const 优先,let 次之

3、读方应杭的博客有感

此时再读方方老师的博客,感觉这更应该是种好的记忆方法

一个转换公式,记就完事了,没有为什么

func(p1, p2) //等价于
func.call(undefined, p1, p2)
​
obj.child.method(p1, p2)// 等价于
obj.child.method.call(obj.child, p1, p2)

对于一些特殊的情况

比如new、箭头函数(跟外边环境一样)、事件(事件发生的主体,比如按钮点击的按钮)再进行单独记忆


4、引用

this - JavaScript | MDN (mozilla.org)

你怎么还没搞懂 this? - 知乎 (zhihu.com)

(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上) - 知乎 (zhihu.com)

有道云笔记 (youdao.com)

JavaScript 的 this 原理 - 阮一峰的网络日志 (ruanyifeng.com)

Javascript 的 this 用法 - 阮一峰的网络日志 (ruanyifeng.com)

You-Dont-Know-JS/ch2.md at 1ed-zh-CN · getify/You-Dont-Know-JS (github.com)