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。
有些文章说箭头函数的
this固定不可变,其实是箭头函数所能找到的this不变,比如箭头函数是函数A返回的闭包函数,箭头函数的this就是函数A的this,这个是确定的,但若函数Athis的指向改变了,则箭头函数this指向也会跟随着变化。
最后请思考一个问题,可以通过call()、apply()、bind()这些方法直接改变箭头函数的this指向吗?
三、总结
this对象是JavaScript的比较复杂的知识点,我看过一些文章讨论this对象引用问题分多类阐述或者直接给出公式,混合作用域链、闭包、赋值、回调、传参等多个知识点导致理解起来过于复杂。我认为,this对象设计其实很精妙,重点要把握好函数执行时确定this的本质,再通过研究几个特殊场景下的例子,就可以较好的理解this对象的指向问题。最后你会发现普通函数和箭头函数本质上是一样,唯一的区别在于普通函数有自己的this,而箭头函数没有自己的this。
由于作者水平有限,不正之处敬请指正。谢谢
参考材料:
- JavaScript高级程序设计(第四版)
- 掘金文章:嗨,你真的懂this吗?
- 知乎文章:JavaScript 的 this原理是什么?