写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!
合集地址:一文搞懂JS系列专题
概览
食用时间: 15-20分钟
难度: 中等,别跑,看完再走
食用价值: 彻底搞懂
this
指向的问题。-
铺垫知识
一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝,理解对象的数据存储方式。
this
设计的初衷
如果只想搞懂 this
指向的问题,可以跳过直接看下面的指向。
this
设计的初衷,其实和 Javascript
本身的数据存储有着一定的关系。
当然,首先需要你对 栈内存 以及 堆内存 有所了解,如果不懂的,可以先移步到一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝,理解对象的数据存储方式。
对象的存储
先来看简单的一行定义语句
var obj = { foo: 5 };
上面的代码,将 {foo : 5}
赋值给 obj
,变量 obj
在栈内存中存储的只是 {foo : 5}
在堆内存中地址的引用。
也就是当要读取 foo
的时候,必须要先从 obj
中读取对象在堆内存中的地址,然后通过 obj.foo
读取到值为 5。
函数的存储
但是 Function
是比较特殊的,我们来看一下 Function
在内存中的存储的例子。
var obj = { foo: function () {} };
在 Javascript
中,函数的存储是有一些特别的,它是独立保存的,然后再将函数的地址指向 foo
。即函数不存在被指向,都是它指向别人的。它是独立的。
因为,函数是独立的,所以它可以指向不同的对象,也就是说,它可以在不同的执行上下文(环境中执行)。
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()
,答案仍然是 window
,因为,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()
,我们来画一个图方便大家理解一下。
所以,上面的语句完完全全就是一个赋值语句,所以,在运行过程中,上面的代码等同于:
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 原理 - 阮一峰
系列目录
-
一文搞懂JS系列(一)之编译原理,作用域,作用域链,变量提升,暂时性死区
-
一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝
-
一文搞懂JS系列(三)之垃圾回收机制,内存泄漏,闭包
-
一文搞懂JS系列(四)之闭包应用-柯里化,偏函数
-
一文搞懂JS系列(五)之闭包应用-防抖,节流
-
一文搞懂JS系列(六)之微任务与宏任务,Event Loop
-
一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类
-
一文搞懂JS系列(八)之轻松搞懂Promise
-
一文搞懂JS系列(九)之更优雅的异步解决方案
-
一文搞懂JS系列(十)之彻底搞懂this指向