【你不知道的JavaScript】this全面解析

464 阅读6分钟

前言

本系列为学习笔记,我将在此记录我从【你不知道的JavaScript】中获取到的知识,如果你也有兴趣,可以跟我一起学习。

动态作用域

在上一章中我们了解到词法作用域就是在书写的时候定义的静态作用域,书写的范围就是词法作用域的范围,那么什么是动态作用域?思考以下代码

function fn(){console.log(a)}
function fn2(){
	var a=1;
    fn()
}
fn2() //??

上面这段代码会输出什么?

JS只有词法作用域,词法作用域会将fn函数中的a通过RHS引用到函数中去,所以上面的代码的结果是RefrenceError

而动态作用域则是不关心书写的定义,而是考虑代码在哪运行,就在哪生成动态作用域,如果JS拥有动态作用域,那么上面的代码应该是打印出1

动态作用域就是代码执行时产生fn的作用域,当没有找到a时,引擎会往上查找,找到fn2函数中的a,并打印出来。

很可惜,虽然JS并不支持动态作用域,但我们可以使用简单明了的词法作用域。

为什么要提到动态作用域呢?因为这跟this的机制有很大的关系

跟this的关系

现在,我们来区分一下词法作用域和动态作用域的差别

  • 词法作用域更关心的是书写,代码写在哪其实就定义好了
  • 动态作用域不关心书写,而是你在哪调用,哪就产生作用域

this是动态作用域的表亲,同样this关心的是在哪调用,而不是在哪书写(箭头函数除外),箭头函数依然是词法作用域的范畴

this全面解析

学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,抛开以前错误的假设和理解。

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个问题:这个this到底引用的是什么?

要找调用位置,不单单是找到调用位置的代码那么简单,因为有时候我们可能难以通过单纯的代码来找位置。

我们需要通过调用栈来找到调用位置。

什么是当前调用栈?当前调用栈就是代码执行时候所处在哪个当前函数,而调用位置就是当前执行函数之前调用的位置

function fn(){
	//调用fn时的当前调用栈
    //此时fn调用位置是全局作用域
	...
    fn1() // fn1的调用位置
}
function fn1(){
	//当前调用栈从fn => fn1
	...
    fn2() // fn2的调用位置
}
function fn2(){
	//当前调用栈从fn1 => fn2
	...
}
fn()

this绑定规则

当找到调用位置后,我们需要分析this的绑定规则

默认绑定

在非strict模式下,this在函数中默认绑定给window对象

function fn(){console.log(this.a)}
var a=2
fn() // 2

独立函数调用时,这里的this默认指向挂在window下的a

隐式绑定

在函数调用时,要看一下调用时的上下文。

var obj={name:"qiu",fn:fn}
function fn(){console.log(this.name)}
obj.fn() //?

当我在使用obj.fn调用函数时,发现打出来的是obj.name,说明这时候this变成了obj。因此你可以说函数被调用时obj的上下文继承给了fn函数

隐式丢失

var obj={name:"qiu",fn:fn}
function fn(){console.log(this.name)}
var baz=obj.fn
baz() // 打出空白

通过代码,我们可以得知,在obj.fn赋值给baz后,this丢失了,不再是原来的obj,说明运行环境的变化导致this会丢失。

我们应该透过运行环境来分析其中的代码,避免被困扰。

var obj={name:"qiu",fn:fn}
function fn(){console.log(this.name)}
var name ='global a'
function fn2(f){f()}
fn2(fn) //??
fn2(obj.fn) //??

运行一下,你会发现答案是一样的,这时候不要被迷惑,原因是随着运行环境的变化,this不断丢失,最后都指向了默认绑定的window。

上面的代码中,传入的参数obj.fn就是一种赋值,它类似与bar=obj.fn,只是这时候它赋值给了函数fn2的参数f,从这时候开始,this就已经丢失了。

这时候就需要提一下setTimeout了,setTimeout是属于window对象的,认为这时候调用函数就类似于隐式绑定

var name="global"
window.setTimeout(()=>{console.log(this.name)},1000) //"global"

如果我把代码带成这样呢?

var obj={name:"yanxi",fn:fn}
function fn(){
	var name="qiu"
	console.log(this.name)
}
var name="global"
window.setTimeout(obj.fn,1000)

现在你应该也是知道答案了,obj.fn赋值给了setTimeout的参数时,this丢失了,所以打出来的结果是global

显式绑定

使用call、apply、bind来显式绑定,这几个函数的第一个参数是给this准备的,第一个参数传什么,this就是什么

思考下面代码

var obj={name:"yanxi",fn:fn}
function fn(){
	var name="qiu"
	console.log(this.name)
}
var name="global"
window.setTimeout(obj.fn.bind(obj),1000)

这时候就按照我们想要的来打印了对吗?

new 绑定

对于new之后发生了什么,可以看我的这篇博客当我们new一个构造函数时,发生了什么? 简单概括一下:

  • 创造一个对象
  • 把新创造的对象放到this上
  • 将对象的原型链与自身的prototype相连
  • 把构造函数的属性方法赋值给对象
  • 返回这个对象
function foo(a){
	this.a=a
}
var obj=new foo('qiu')

想象一下,使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上

//变成了
function foo(a){
	var newObj={} //创造了新对象
    newObj.a=a //把this改成新对象
    return newObj //返回出去
}
var obj=foo('qiu')

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”

this词法

箭头函数就是词法作用域的this,而并非类似动态作用域的this.

function fn(){
	return ()=>{
    	console.log(this.a)//继承外层函数fn中的this
    }
}
var a=1
var obj={a:2,fn:fn}
var baz=obj.fn()
baz() //2

上面的代码读取时,会取外层的fn中的this,且像bind一样牢牢绑定。

const fn=()=>{console.log(this.a)}
var obj={a:2,fn:fn}
var a=3
obj.fn() //3
var fn2=obj.fn
fn2() //3
obj.fn.call(obj) //3 不是2!
fn.call(obj) //依然是3

上面的代码中,箭头函数直接在词法阶段捕获外层的this---window,所以不管怎样,this只会绑定外层的window,哪怕连call也改变不了

总结

我们知道this在JS中有以下规则

  • 默认绑定到全局对象
  • 隐式绑定 obj.fn
  • 隐式丢失 fn2=obj.fn
  • 显式绑定 bind、call、apply
  • new 绑定
  • this忽略上述规则,会往外层继承绑定 熟悉上面的规则后,在书面规范方面我们应该遵守统一书写风格,要么完全避免this使用,必要时采用self=this,来保存this指针。要么就拥抱this,一直使用bind书写显式来告诉所有人,你这里的this是指xxx,因为并非所有人都能搞懂this的怪异乱象。

参考

你不知道的JavaScript