阅读 275

一文搞懂JS系列(十)之彻底搞懂this指向

写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞

合集地址:一文搞懂JS系列专题

概览

this 设计的初衷

如果只想搞懂 this 指向的问题,可以跳过直接看下面的指向。

this 设计的初衷,其实和 Javascript 本身的数据存储有着一定的关系。

当然,首先需要你对 栈内存 以及 堆内存 有所了解,如果不懂的,可以先移步到一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝,理解对象的数据存储方式。

对象的存储

先来看简单的一行定义语句

var obj = { foo:  5 };
复制代码

上面的代码,将 {foo : 5} 赋值给 obj,变量 obj 在栈内存中存储的只是 {foo : 5} 在堆内存中地址的引用。

image.png

也就是当要读取 foo 的时候,必须要先从 obj 中读取对象在堆内存中的地址,然后通过 obj.foo 读取到值为 5。

函数的存储

但是 Function 是比较特殊的,我们来看一下 Function 在内存中的存储的例子。

var obj = { foo: function () {} };
复制代码

Javascript 中,函数的存储是有一些特别的,它是独立保存的,然后再将函数的地址指向 foo 。即函数不存在被指向,都是它指向别人的。它是独立的。

image.png

因为,函数是独立的,所以它可以指向不同的对象,也就是说,它可以在不同的执行上下文(环境中执行)。

var f = function () {};
var obj = { f: f };

// 单独执行,即 window,node.js中为 Global ,严格模式下为undefined
f()

// obj 环境执行
obj.f()
复制代码

环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};
复制代码

上面代码中,函数体里面使用了变量 x 。该变量由运行环境提供。

那么问题就来了,由于函数可以在不同的运行环境中执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(执行上下文)。所以, this 就出现了。它的设计初衷就是在函数体内部,指代函数当前的运行环境。

上述概念参考于 JavaScript 的 this 原理 - 阮一峰

起步,搞懂 this 指向

既然上文中,我们已经理解了, this 的设计初衷,其实主要是为了表示当前的运行环境。所以,简单而言, this 永远指向最后调用它的那个对象 ,因为,最后调用的对象决定了当前的运行环境。

当前的运行环境,即决定了 this 究竟是谁。

但是,当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined

冲锋,实践出真知

明白了原理,不真刀真枪实践一把怎么知道呢,看题:

题1:

var name = "windowName";
function a() {
    var name = "cooldream";
    console.log(this.name);          // windowName
    console.log("inner:" + this);    // inner: Window
}
a();
console.log("outer:" + this)         // outer: Window
复制代码

这道题的答案其实很简单,正如我们上面所说的,当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined

因为, a() 其实等同于 window.a()

再看下面一个例子:

题2:

var name = "windowName";
var a = {
    name: "cooldream",
    fn : function () {
        console.log(this.name);      // cooldream
    }
}
a.fn();
复制代码

由于是 a.fn() ,所以,其实此时 fn() 方法指向的是对象 a ,而不是 window ,即最后调用的对象是 a ,所以,答案也很简单,是 cooldream

那么,如果将最后一句 a.fn() 改为 window.a.fn() ,答案仍然是 cooldream,因为,this 永远指向最后调用它的那个对象,尽管改了写法,最后调用它的对象仍然是 a

此时,再做一个改动,将变量 a 中的 name 给注释掉,代码如下所示:

题3:

var name = "windowName";
var a = {
    // name: "cooldream",
    fn : function () {
        console.log(this.name);      // undefined
    }
}
window.a.fn();
复制代码

可以看到值直接变为了 undefined ,这是由于最后调用的对象是 a ,所以,上面的输出语句相当于 console.log(a.name) ,既然没找到,那就抛出一个 undefined ,由于是明确指定对象 a 中的 name 属性,自然也就不存在变量的向上作用域的查找。

接下来,我们看一个比较复杂的例子,在看这个例子之前,让我们脑海中先明确一下赋值语句和调用语句。

一个是指变量的赋值,它其实并不是决定 this 指向的要素,而调用,才是决定 this 指向的重要因素。

想明白了这一点,那么,接下来,我们来看下这个例子:

题4:

var dog = {
  name: 'wangcai',
  hello: function() {
    console.log(`你好,我是${this.name}`)
  }
}

var cat={
  name:'miaomiao',
  hello:dog.hello    // 赋值语句
}

//调用语句
dog.hello();     // wangcai
cat.hello();     // miaomiao
复制代码

上面的代码,相信第一个 dog.hello() 大家都可以理解,调用对象为 dog ,所以,值为 wangcai 。

而第二个 cat.hello() ,可能会受到 cat={hello:dog.hello} 所误导,认为最后的调用对象是 dog

需要注意的是 hello: dog.hello 只是一个赋值语句,将 cat.hello() 函数指向 dog.hello() ,我们来画一个图方便大家理解一下。

image.png

所以,上面的语句完完全全就是一个赋值语句,所以,在运行过程中,上面的代码等同于:

var dog = {
  name: 'wangcai',
  hello: function() {
    console.log(this.name)
  }
}

var cat={
  name:'miaomiao',
  hello: function() {
    console.log(this.name)
  }
}

//调用语句
dog.hello();     // wangcai
cat.hello();     // miaomiao
复制代码

所以,自然答案就很清晰了, cat.hello() 的输出结果就是 miaomiao 。

归根结底, hello: dog.hello终究只是一个赋值语句,将 dog.hello 的值赋值给 cat.hello,而在实际的函数调用中,最后的调用对象始终是 cat

所以,要时刻牢记,this 永远指向最后调用它的那个对象 ,同时,要学会区分赋值语句和调用语句,不要被赋值语句的障眼法蒙住了双眼。

既然掌握了我们的黄金法则,接下来,小试牛刀:

题5:

var dog = {
  name: 'wangcai',
  hello: function() {
    console.log(this.name)
  }
}

var name = 'windowName'
var hello = dog.hello   //赋值语句

// 调用语句
hello() // windowName
复制代码

依旧是一句熟悉的赋值语句 var hello = dog.hello ,当然,这只是一个赋值操作,并不影响 this 的指向,接下来,主要看我们的调用语句 hello() ,可以看到,当调用方法没有明确对象时,this 就指向全局对象,即 window ,我们可以将上述代码理解为如下:

var dog = {
  name: 'wangcai',
  hello: function() {
    console.log(this.name)
  }
}

var name = 'windowName'
var hello = function(){
  console.log(this.name)
}

// 调用语句
window.hello() // windowName
复制代码

所以,答案也显而易见,是 windowName

接下来,让我们来看一道终极考验

终极考验

废话少说,上代码:

var dog = {
  name: 'wangcai',
  hello: function() {
    console.log(this.name) 
  }
}

var cat = {
  name: 'miaomiao',
  hello: function() {
    var targetFunc = dog.hello   //赋值语句 
    targetFunc()   //调用语句 顺序2
  }
}

var name = 'windowName'

// 调用位置
cat.hello()   //调用语句  顺序1
复制代码

我们已经将代码中的赋值语句调用语句进行了标记,可以看到,这一次,有两个调用语句,不难看出它们的顺序,先执行 cat.hello() ,所以,此时的调用对象应该是 cat ,接下来第二句, targetFunc() ,当调用方法没有明确对象时,this 就指向全局对象,所以,最后输出的结果应该是 windowName

所以,最后再牢记一句话, this 永远指向最后调用它的那个对象 。因为最后调用它的对象,才是最后 this 真正指向的环境(执行上下文)。

新的章程

当然, this 指向还有你所不知道的三种特殊的指向情况,可以移步一文搞懂JS系列(十一)之this的三种特殊指向 进行学习。

参考目录

this、apply、call、bind

JavaScript 的 this 原理 - 阮一峰

系列目录

文章分类
前端
文章标签