this
关键字是JavaScript函数内部的一个对象,this
是一个指针,指向调用函数的对象。看似简单的定义但却由于在解析this
引用过程中可能涉及到执行上下文、作用域链、闭包等复杂的机制,导致this
的指向问题变得异常复杂。首先必须明白一点,任何复杂的机制都不可能轻而易举的学懂弄通,因此,本文将与大家一起耐心回顾this
对象,归纳总结this
的引用机制,希望对你有所帮助。
一、函数到底执行了没?
要向弄懂this
对象,必须先搞懂函数是什么时候执行?
先看看简单的一个例子(例1):
function fn(){
console.log('你好');
}
fn;
fn();
let f = fn;
f();
上面的例子一共输出 2 次 你好
。fn()
、f()
表达式中函数被调用执行,fn
和let f=fn
表达式中函数并未被调用执行。
函数执行主要看是否存在函数名()
,使用不带括号的函数名会访问函数指针,并非调用该函数
再看看一个加入闭包机制的例子(例2):
function fn(){
let hi = '你好'
return function gn(){
console.log(hi);
}
}
fn;
fn();
let f = fn;
f();
let g = fn();
g();
上面的例子似乎较为复杂了,那一共输出多少次 你好
?
- 输出
你好
必须是函数gn
被调用执行,因此关键在于函数gn
什么时候被调用? - 根据前一个例子,表达式
fn
、f=fn
没有调用函数fn
,则更不会调用函数gn
。 - 根据前一个例子,表达式
fn()
、f()
是相同的含义,均调用了函数fn
。在闭包中,调用fn
返回返回一个函数gn
的函数指针,但最终并没有通过该函数指针调用gn
,因此在表达式fn()
、f()
、g=fn()
并没有执行函数gn
。 表达式
g=fn()
,可以将函数gn
赋值给g
,最后通过g()
完成对函数gn
的调用执行。类似于:let hi = ‘你好’; let g = function (){ console.log(hi); } g();//函数执行
因此最终该例子仅输出一次
你好
。
最后看看一个对象内部的函数调用例子(例3):
let o = {
hi:'好难呀',
fn: function () {
let hi = '你好'
return function gn() {
console.log(hi);
}
}
}
o.fn;
o.fn();
let f = o.fn;
f();
let g = o.fn();
g();
这个例子中,一共输出多少次 你好
?
其实无论函数放到对象内部定义还是外部定义,均可以采用前一个例子的分析步骤解析函数被调用执行的过程,因此,本例子中也仅输出一次 你好
。
全局环境中定义的function
,则该函数自动成为window
对象的方法,即全局环境下的fn()
调用等价于window.fn()
调用。
二、神奇的this
what,上面讲了一大堆的都还没有讲到this
?
别急,理解this
的引用机制,我认为最关键的是理解函数 执行 的上下文。倘若连函数什么时候执行都傻傻搞不清,那理解this
对象更无从谈起,来,我们开始继续探索。
红宝书第四版将
this
对象阐述为:1.在标准函数中,this
引用的是把函数当成方法调用的上下文对象
2.在箭头函数中,this
引用的是定义箭头函数的上下文
(一)标准函数的this
对象
普通函数(除箭头函数)内部中均有一个this
对象。this
在函数执行时确定所指对象。这里有两个关键点:执行时、对象。
- 普通函数在执行时才能确定
this
。那在定义时能确定码?不行!记住:函数执行时确定!函数执行时确定!函数执行时确定! - 普通函数的
this
指向的是调用该函数的对象。那可以指向其他函数吗?可以指向原始数据类型吗?统统不行!记住:指向调用该函数的对象、指向调用该函数的对象、指向调用该函数的对象
虽然很多文章对this
的引用分了情况讨论,但我依旧认为理解上述两个关键点是最重要的。来,我们通过例子进一步分析,以下将按照掘金文章:嗨,你真的懂this吗?的分类标准进行讨论。
默认绑定
默认绑定简单说就是在全局环境中执行函数,即没有任何对象直接调用该函数。这种情况下,函数的this
将指向window
(非严格模式)或为undefined
(严格模式)
//非严格模式下,this指向window
function fn1(){
console.log(this);
}
//严格模式下,this为undefined
function fn2(){
'use strict'
console.log(this);
}
简单吧,可是你能准确的判断出函数是在全局环境中执行的么?请看下面例子(例4):
var hi = 'window'
let o = {
hi: '对象',
gn: function (){
let hi = '函数';
console.log(this.hi);
},
fn: function () {
let hi = '函数'
return function (){
let hi = '闭包函数';
console.log(this.hi);
};
}
}
o.gn();
let f = o.fn();
f();
一旦涉及对象内部方法、闭包等机制,就会导致问题变得复杂许多。你能看出一共输出了几次?有多少次输出是在全局环境中执行的呢?
按照前一章节分析,可以知道一共有两次输出(若是不理解可以回看第一节),分别为表达式o.gn()
和f()
输出对象
,window
。分析如下:
- 表达式
o.gn()
显然是通过对象o
对函数gn
进行调用,因此gn
执行时的this
所指向的就是o
; - 表达式
let f = o.fn();
将执行fn
函数并将闭包函数的函数指针赋值给f
变量,此时执行的函数是fn
而并非是闭包函数,因此此刻fn
的this
指向对象o
,但是闭包函数的this
现在还没有确定; - 通过调用表达式
f();
,让闭包函数执行,此刻闭包函数并不是某个对象调用执行,因此是运行在全局环境中,所以闭包函数的this
将指向window
(非严格模式)
因此,不管函数如何赋值,只要该函数并未执行,this
指针就不会确定所指对象。第一个关键点就是理解函数是什么时候执行的!第二个关键点就是找到函数是如何被调用的!
隐式绑定
隐式绑定是指通过某个对象调用函数时,函数的this
就指向该对象。简单说就是谁调用函数,函数就指谁。
我们看看下面这个例子,该例子出自知乎文章:JavaScript 的 this原理是什么?(例5)
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())
一看感觉很复杂,但本质上还是找到哪个对象调用函数进行执行,分析一波:
- 执行表达式
console.log(o1.fn())
时,对象o1
调用执行函数fn
,因此,函数fn
的this
指向对象o1
,所以输出o1
; - 执行表达式
console.log(o2.fn())
时,对象o2
调用执行函数fn
,因此,o2
内部函数fn
的this
指向对象o2
,但且慢,这里有个表达式return o1.fn()
,不难看出这又通过o1
调用了o1
内部函数fn
(该函数this
指向o1
对象),并将执行结果返回。因此绕个弯还是回到执行o1
内部函数fn
,输出o1
; - 执行表达式
console.log(o3.fn())
时,对象o3
调用执行函数fn
,因此,o3
内部函数fn
的this
指向对象o3
,但o3
内部函数fn
并没有直接用this
,而是通过赋值操作获取了o1
内部的fn
函数,并执行fn
函数。注意,这里有个坑,最后执行fn
函数是没有对象调用的,因此fn
函数的this
指向window
,这个跟例4类似,若不理解可以回头看例4。
一定要分清默认绑定和隐式绑定的场景,关键点还是在于判断出函数执行的时间,然后找出哪个对象调用了该函数。
结合回调函数再看一个例子(例6):
var hi = 'window'
let o = {
hi: '对象',
fn: function () {
let hi = '函数';
setInterval(function(){
console.log(this.hi);
},1000);
}
}
o.fn();
你觉得应该输出什么呢?我们先分析一波:
- 很明显,表达式
o.fn();
执行过程中,函数fn
的this
铁定是指向对象o
; - 再看
fn
函数里面,执行了setInterval
函数,特别是还传入了匿名函数作为回调函数,匿名函数在每一秒执行过程中并没有任何对象调用它,因此匿名函数的this
指向window
,最终输出window
。
结合传参再看一个例子(例7):
var hi = 'window'
let o = {
hi: '对象',
fn: function () {
let hi = '函数';
console.log(this);
}
}
function gn(fn){
fn();
}
gn(o.fn)
你觉得这回输出什么呢?不断的分析:
- 首先明确一点:参数的传入等价于赋值。因此
gn(o.fn)
等价于f = o.fn; gn(f)
;好家伙,又是赋值,没有执行函数的都是骗子! - 函数
gn
内部执行传入的函数fn
,并没有发生对象调用,因此此刻执行的环境就是全局环境,输出window
。
赋值、回调、闭包都是this
的头号大敌,一定等确定函数真的执行了,再去找关联的对象。
显示绑定
显示绑定是指通过call、apply、bind
对函数的this
进行重定向,直接指定函数this
所指的对象。
var hi = 'window'
let o = {
hi: '对象',
}
function fn() {
let hi = '函数';
console.log(this.hi);
}
fn.call(o);
通过fn
函数的call
方法,可以将全局环境中执行的fn
函数内部this
强行指向对象o
,因此输出:对象
。
让我们思考一下,关于方法call、apply、bind
之间有什么不同呢?
红宝书第四版解释如下:
call
和apply
作用是一样的,只是传入参数的形式不同,call
向函数传入参数需要一个个列出来,而apply
需要使用参数数组进行传入参数bind
方法会创建一个新的函数实例,其this
值会绑定到传给bind
的对象。
值得注意的是,call
和apply
方法在调用后会直接执行函数,bind
方法则不会,但是bind
方法将会一直绑定固定的this
给新创建的实例。bind
的具体用法如下:
var hi = 'window'
let o = {
hi: '对象',
}
function fn() {
let hi = '函数';
console.log(this.hi);
}
let f = fn.bind(o);
f();//无论f如何调用,f内部的this始终指向对象o,输出:对象
fn();//this 依旧按照正常绑定规则进行绑定,输出:window
new
绑定
new关键字会出现在使用构造函数创建特定类型对象中,且看一下红宝书对于new
关键字的操作解释:
红宝书第四版将new操作步骤解释为:
- 在内存中创建一个新对象
- 这个新对象内部[[Prototype]]特性被赋值为构造函数的prototype属性
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
上述操作流程已经很清楚了,看看下面的例子(例8):
function fn() {
this.name = "Tony";
this.showName = function (){
console.log(this.name);
}
}
let newObj = new fn();
newObj.showName();
结合红宝书的解释,我们可以知道在使用new
关键字时有如下步骤:
- 生成一个新匿名对象
- 该匿名对象的[[Prototype]]特性被赋值为构造函数的prototype属性(这块涉及原型链知识)
- 构造函数
fn
的内部this
指向该匿名函数 - 执行
fn
内部代码,给匿名函数添加属性name
和方法showName
- 返回匿名函数,并赋给newObj
(二)箭头函数的this
对象
相比于普通函数内部有一个this
对象,箭头函数内部是没有this
对象。你没有听错,箭头函数内部是没有this
对象!
那该如何确定箭头函数的this
引用呢?回顾JavaScript关于作用域链机制,当一个函数作用域中没有某个变量时,则将会在作用域链中的逐级往后寻找,直到找到某个变量或因找不到而报错。因此,箭头函数内部没有this
对象,则在使用this
对象时,必须要找到外层函数的this
对象或者window
的this
对象,而箭头函数对应的外层this
关系是在箭头函数定义时确定的,因此无论箭头函数是在哪里调用,箭头函数所能找到的this
已经在定义时就确定了。
我们通过例子来找箭头函数的this
(例9):
var hi = 'window'
let o = {
hi:'对象',
gn:()=>{
let hi = '箭头函数';
console.log(this.hi);
},
fn:function () {
let hi = '函数'
return ()=>{
let hi = '箭头函数';
console.log(this.hi);
};
}
}
o.gn();
let f = o.fn();
f();
f = o.fn.call(window);
f();
先寻找箭头函数的this
:
- 函数
gn
是箭头函数,而且外层没有其他的函数包裹,因此根据变量解析的作用域链规则,箭头函数的的this
就是window
的this
。 - 函数
fn
是一个返回箭头函数的匿名函数,根据作用域链规则,在查找箭头函数this
过程中,找到外层函数fn
的this
当做箭头函数的this
。
最后我们得出:
gn
箭头函数的this
就是window
的this
;fn
返回的箭头函数的this
就是函数fn
的this
。
运行上述例子,可以获得浏览器以下输出
简单分析一下:
- 第一个输出由表达式
o.gn()
产生,由于gn
箭头函数的this
就是window
的this
,因此hi
变量就是window
; - 第二个输出由表达式
let f = o.fn(); f();
产生,由于fn
返回的箭头函数的this
就是函数fn
的this
,通过表达式o.fn()
将函数fn
的this
指向对象o
,导致箭头函数的this
也是o
,最终输出对象
; - 第三个输出由表达式
f = o.fn.call(window); f();
产生,由于fn
返回的箭头函数的this
就是函数fn
的this
,通过表达式f = o.fn.call(window)
将函数fn
的this
指向window
,导致箭头函数的this
也是window
,最终输出window
。
最后请思考一个问题,可以通过call()、apply()、bind()
这些方法直接改变箭头函数的this
指向吗?
三、总结
this
对象是JavaScript的比较复杂的知识点,我看过一些文章讨论this
对象引用问题分多类阐述或者直接给出公式,混合作用域链、闭包、赋值、回调、传参等多个知识点导致理解起来过于复杂。我认为,this
对象设计其实很精妙,重点要把握好函数执行时确定this
的本质,再通过研究几个特殊场景下的例子,就可以较好的理解this
对象的指向问题。最后你会发现普通函数和箭头函数本质上是一样,唯一的区别在于普通函数有自己的this
,而箭头函数没有自己的this
。
由于作者水平有限,不正之处敬请指正。谢谢
参考材料:
- JavaScript高级程序设计(第四版)
- 掘金文章:嗨,你真的懂this吗?
- 知乎文章:JavaScript 的 this原理是什么?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。