前言
成都FCC全栈大会贺老分享“This In JS”主题记录,我们知道,在Javascript中this和其他语言的this并不相同,我们在不同环境,不同语法,不同调用模式下,this可能总会出现各种奇怪的指向。
正文
function f() { this }
function f() { 'use strict'; this }
const f = () => this
我们先看看这样一个很简单例子,我相信这个例子大家初学JS或者面试的时候都用经历过,我们在不同的语法环境下,不同的模式下,this可能都是不同的值。
同时,this
的取值也取决于代码调用方式,如
f()
obj.f()
new f()
是函数调用?对象属性调用?或者构造器的形式调用?同时JS也有独特的方法来改变this的指向,如我们常用的 call apply bind
。
f.call(obj)
f.call(null)
f.call(42)
f.call()
可能大家没想过,如果call中传入了null,传入了一个数字,那么this
又是什么,同时,这些又和上文如是否是严格模式有关系,不同模式下this
也都会发生变化。
我们来看一个例子
<form id=myForm>
<input type="button" name="beijing" value="北京">
<input type="button" name="shanghai" value="上海">
<input type="button" name="chengdu" value="成都">
</form>
<script>
class Greeting {
static from(control) { return new Greeting(control.value) }
constructor(name) { this.name = name }
hello() { console.log(`Hello ${this.name || 'world'}!`) }
}
[...myForm.elements]
.map(e => [e, Greeting.from(e)])
.forEach(([e, {hello}]) => e.addEventListener('click', hello))
</script>
这样一段代码,大家可以想想,当点击成都
的时候,会打印什么话
······
答案是打印Hello chengdu
,当调用click时,this其实指向的是element,也就是说,最后打印的name其实是元素上的name
属性。
这么一看似乎问题不大,因为问题能很快定位,当我们把代码改成这样呢
<form id=myForm>
<input type="button" name="beijing" value="beijing">
<input type="button" name="shanghai" value="shanghai">
<input type="button" name="chengdu" value="chengdu">
</form>
<script>
class Greeting {
static from(control) { return new Greeting(control.value) }
constructor(name) { this.name = name }
hello() { console.log(`Hello ${this.name || 'world'}!`) }
}
[...myForm.elements]
.map(e => [e, Greeting.from(e)])
.forEach(([e, {hello}]) => e.addEventListener('click', hello))
</script>
我们的执行逻辑想打印input
上的value
,打印出来发现,没错,打印了Hello chengdu
,其实这段代码对比上文就是有Bug的,当我们下次维护时,出了bug也能难以去定位问题。
我们在来看看Promise
中的this。
Promise.all(numberPromises)
.then(values => {
const nonNumbers = values.filter(isNaN)
return Promise.all(values.concat(nonNumbers.map(Promise.reject)))
})
.then(nextStep)
.catch(errorLogger)
这是一段写法比较奇怪的代码,讲的是,我们需要筛选出不是数字的值,如果存在非组织,那么就会进入catch中,调用error,当我们在errorLogger打印信息时,我们应该是希望打印不是数字的值,可实际上,我们到catch后,发现打印的是
PromiseReject called on non-object
为什么呢?我们都认为Promise.reject
是一个静态函数,其实在标准Promise定义中他是需要this的,因为Promise定义中,他是可以被子类化的,当我们调用Promise.reject
时,他会看当前this指向的是什么类,如果是Promise子类的话,他便会创建一个Promise子类的实例。我想,对于大多数人而言,应该都不清楚Promise.reject
需要一个this吧
this的问题
即便学会了也可能会出问题,他具体表现在:
- 容易挖坑
- 可能隐藏
- 难以定位
ES6解决方案
在JS中,我们用到this的地方主要有:
- 普通函数
- 回调函数
- 构造器
- 方法
在ES6中,我们提供了class
,来解决构造器this的问题,提供了allow function
来解决,那其实我们对this判断有可能失败的情况,那么就还有普通函数
以及方法
。正常情况下,我们自己的代码还是可以分辨出this
指向,但是对于第三方库以及框架,this
到底指向什么就难以区分
贺老准备在下一次T39会议提出,通过语言的层面解决this指向不清的问题
Outdated draft: gilbert/es-explicit-this
显示this
我们知道,在函数中,this其实是作为一个隐藏的变量来提供给开发者使用,那么我们是否可以把this
显示出来呢,在TS中,我们可以使用这样的代码
Number.prototype.toHex = function (this: number) {
return this.toString(16)
}
显示的告诉this是一个number类型的代码,那么我们可以借鉴显示this的方式,构建出如下的代码。
function getX(this) { // 显示 this
return this.x
}
function getX(this o) { // 别名
return o.x
}
function getX(this {x}) { // 解构
return x
}
这样写this,能带来什么好处呢?
示例1
// original code
class Player {
attack(opponent) {
return Game.calculateResult(
this.input(),
opponent.input(),
)
}
}
// better naming
class Player {
attack(this offense, defense) {
return Game.calculateResult(
offense.input(),
defense.input(),
)
}
}
通过别名,我们可以增强代码的可读性
示例二
// original code
function process (name) {
this.taskName = name;
const that = this
doAsync(function (amount) {
this.x += amount;
that.emit('change', this)
});
};
// better naming
function process (this obj, name) {
obj.taskName = name;
doAsync(function callback (this result, amount) {
result.amount += 2;
obj.emit('change', result)
});
};
我们知道,同名变量后者会覆盖前者,this也不例外,该例子我们可以通过显示this
的方式,保证this
的是你想要的,而不用另外定义变量
示例三
function div(@int32 this numerator, @int32 denominator) {
// if (numerator !== numerator|0) throw new TypeError()
// if (denominator !== denominator|0) throw new TypeError()
// ...
}
使用了显示的this后,我们也可以使用decorator
封装一些公共逻辑处理this。
主要
这一份提案主要解决了this
指向容易混淆的问题。这里,贺老也提出了一份另外的提案,用于提前发现this指向错误的问题。
Outdated draft: hax/proposal-function-this
增加一个具体属性 thisArgumentExpected
,我们可以使用这个属性,提前发现我们this是否出现错误,如,我们定义一个APIon
// safer API:
function on(eventTarget, eventType, listener, options) {
if (listener.thisArgumentExpected) throw new TypeError('listener should not expect this argument')
return eventTarget.addEventListener(eventType, listener, options)
}
当这个api发现thisArgumentExpected
是true的话,就提前抛出错误,而不需要等待点击的时候才发现错误,越早检测到,对代码的稳定性也就越好。同时,针对不同情况thisArgumentExpected
,thisArgumentExpected
也会自动变化
// 箭头函数
let arrow = () => { this }
arrow.thisArgumentExpected // false
// 使用bind的函数
let bound = f1.bind()
bound.thisArgumentExpected // false
// 未使用bind的普通函数
function func() {}
func.thisArgumentExpected // false
// 使用bind的普通函数
function implicitThis() { this }
implicitThis.thisArgumentExpected // true
// 显示this的普通函数
function explicitThis(this) {}
explicitThis.thisArgumentExpected // true
// 对象
class C {
m1() {}
m2() { this }
m3(this) {}
m4() { super.foo }
static m1() {}
static m2() { this }
static m3(this) {}
static m4() { super.foo }
}
C.prototype.m1.thisArgumentExpected // false
C.prototype.m2.thisArgumentExpected // true
C.prototype.m3.thisArgumentExpected // true
C.prototype.m4.thisArgumentExpected // true
C.m1.thisArgumentExpected // false
C.m2.thisArgumentExpected // true
C.m3.thisArgumentExpected // true
C.m4.thisArgumentExpected // true
C.thisArgumentExpected // null
通过这样的形式,我们对对应API增加thisArgumentExpected
检测,便可以提前知道我们的代码是否可能会有this
指向错误的问题
总结
上面的介绍目前只是提案,并没有实际支持,是否得到通过还未可知。不过从介绍看,确实解决了this的一些问题,对于开发第三方库以及框架的同学而言是一份好消息,对于普通开发者,也能间接的提升开发体验(知道该库的this到底是什么)
本文提到的提案地址
Outdated draft: gilbert/es-explicit-this
Outdated draft: hax/proposal-function-this