当前期刊数: 13

1 引言

logo

javascript 的 this 是个头痛的话题,本期精读的文章更是引出了一个观点,避免使用 this。我们来看看是否有道理。

本期精读的文章是:classes-complexity-and-functional-programming

2 内容概要

javascript 语言的 this 是个复杂的设计,相比纯对象与纯函数,this 带来了如下问题:

1
2
3
4
const person = new Person('Jane Doe')
const getGreeting = person.getGreeting
// later...
getGreeting() // Uncaught TypeError: Cannot read property 'greeting' of undefined at getGreeting

初学者可能突然将 this 弄丢导致程序出错,甚至在 react 中也要使用 bind 的方式,使回调可以访问到 setState 等函数。

this 也不利于测试,如果使用纯函数,可以通过入参出参做测试,而不需要预先初始化环境。

所以我们可以避免使用 this,看如下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function setName(person, strName) {
return Object.assign({}, person, {name: strName})
}

// bonus function!
function setGreeting(person, newGreeting) {
return Object.assign({}, person, {greeting: newGreeting})
}

function getName(person) {
return getPrefixedName('Name', person.name)
}

function getPrefixedName(prefix, name) {
return `${prefix}: ${name}`
}

function getGreetingCallback(person) {
const {greeting, name} = person
return (subject) => `${greeting} ${subject}, I'm ${name}`
}

const person = {greeting: 'Hey there!', name: 'Jane Doe'}
const person2 = setName(person, 'Sarah Doe')
const person3 = setGreeting(person2, 'Hello')
getName(person3) // Name: Sarah Doe
getGreetingCallback(person3)('Jeff') // Hello Jeff, I'm Sarah Doe
demo1

这样 person 实例是个纯对象,没有将方法挂载到原型链上,简单易懂。

或者可以将属性放在上级作用域,避免使用 this,就避免了 this 丢失带来的隐患:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getPerson(initialName) {
let name = initialName
const person = {
setName(strName) {
name = strName
},
greeting: 'Hey there!',
getName() {
return getPrefixedName('Name')
},
getGreetingCallback() {
const {greeting} = person
return (subject) => `${greeting} ${subject}, I'm ${name}`
},
}
function getPrefixedName(prefix) {
return `${prefix}: ${name}`
}
return person
}

以上代码没有用到 this,也不会因为 this 产生的问题所困扰。

3 精读

本文作者认为,class 带来的困惑主要在于 this,这主要因为成员函数会挂到 prototype 下,虽然多个实例共享了引用,但因此带来的隐患就是 this 的不确定性。js 有许多种 this 丢失情况,比如 隐式绑定 别名丢失隐式绑定 回调丢失隐式绑定 显式绑定 new绑定 箭头函数改变this作用范围 等等。

由于在 prototype 中的对象依赖 this,如果 this 丢了,就访问不到原型链,不但会引发报错,在写代码时还需要注意 this 的作用范围是很头疼的事。因此作者有如下解决方案:

1
2
3
4
5
6
7
8
9
function getPerson(initialName) {
let name = initialName
const person = {
setName(strName) {
name = strName
}
}
return person
}

由此生成的 person 对象不但是个简单 object,由于没有调用 this,也不存在 this 丢失的情况。

这个观点我是不认可的。当然做法没有问题,代码逻辑也正确,也解决了 this 存在的原型链访问丢失问题,但这并不妨碍使用 this。我们看以下代码:

1
2
3
4
5
6
7
8
9
10
class Person {
setName = (name) => {
this.name = name
}
}

const person = new Person()
const setName = person.setName
setName("Jane Doe")
console.log(person)

这里用到了 this,也产生了别名丢失隐式绑定,但 this 还能正确访问的原因在于,没有将 setName 的方法放在原型链上,而是放在了每个实例中,因此无论怎么丢失 this,也仅仅丢失了原型链上的方法,但 this 无论如何会首先查找其所在对象的方法,只要方法不放在原型链上,就不用担心丢失的问题。

至于放在原型链上会节约多个实例内存开销问题,函数式也无法避免,如果希望摆脱 this 带来的困扰,class 的方式也可以解决问题。

3.1 this 丢失的情况

3.1.1 默认绑定

在严格模式与非严格模式下,默认绑定有所区别,非严格模式 this 会绑定到上级作用域,而 use strict 时,不会绑定到 window。

1
2
3
4
5
6
7
function foo(){
console.log(this.count) // 1
console.log(foo.count) // 2
}
var count = 1
foo.count = 2
foo()
1
2
3
4
5
6
function foo(){
"use strict"
console.log(this.count) // TypeError: count undefined
}
var count = 1
foo()

3.1.2 隐式绑定

当函数被对象引用起来调用时,this 会绑定到其依附的对象上。

1
2
3
4
5
6
7
8
function foo(){
console.log(this.count) // 2
}
var obj = {
count: 2,
foo: foo
}
obj.foo()

3.1.3 别名丢失隐式绑定

调用函数引用时,this 会根据调用者环境而定。

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.count) // 1
}
var count = 1
var obj = {
count: 2,
foo: foo
}
var bar = obj.foo // 函数别名
bar()

3.1.4 回调丢失隐式绑定

这种情况类似 react 默认的情况,将函数传递给子组件,其调用时,this 会丢失。

1
2
3
4
5
6
7
8
9
function foo(){
console.log(this.count) // 1
}
var count = 1
var obj = {
count: 2,
foo: foo
}
setTimeout(obj.foo)

3.2 this 绑定修复

3.2.1 bind 显式绑定

使用 bind 属于显示绑定。

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.count) // 1
}
var obj = {
count: 1
}
foo.call(obj)

var bar = foo.bind(obj)
bar()

3.2.2 es6 绑定

这种情况类似使用箭头函数创建成员变量,以下方式等于创建了没有挂载到原型链的匿名函数,因此 this 不会丢失。

1
2
3
4
5
6
7
8
9
function foo(){
setTimeout(() => {
console.log(this.count) // 2
})
}
var obj = {
count: 2
}
foo.call(obj)

3.2.3 函数 bind

除此之外,我们还可以指定回调函数的作用域,达到 this 指向正确原型链的效果。

1
2
3
4
5
6
7
8
9
function foo(){
setTimeout(function() {
console.log(this.count) // 2
}.bind(this))
}
var obj = {
count: 2
}
foo.call(obj)

关于块级作用域也是 this 相关的知识点,由于现在大量使用 let const 语法,甚至在 if 块下也存在块级作用域:

1
2
3
4
5
6
7
8
if (true) {
var a = 1
let b = 2
const c = 3
}
console.log(a) // 1
console.log(b) // ReferenceError
console.log(c) // ReferenceError

4 总结

要正视 this 带来的问题,不能因为绑定丢失,引发非预期的报错而避免使用,其根本原因在于 javascript 的原型链机制。这种机制是非常好的,将对象保存在原型链上,可以方便多个实例之间共享,但因此不可避免带来了原型链查找过程,如果对象运行环境发生了变化,其原型链也会发生变化,此时无法享受到共享内存的好处,我们有两种选择:一种是使用 bind 将原型链找到,一种是比较偷懒的将函数放在对象上,而不是原型链上。

自动 bind 的方式 react 之前在框架层面做过,后来由于过于黑盒而取消了。如果为开发者隐藏 this 细节,框架层面自动绑定,看似方便了开发者,但过分提高开发者对 this 的期望,一旦去掉黑魔法,就会有许多开发者不适应 this 带来的困惑,所以不如一开始就将 this 问题透传给开发者,使用自动绑定的装饰器,或者回调处手动 bind(this),或将函数直接放在对象中都可以解决问题。


本站由 钟意 使用 Stellar 1.28.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercel 提供托管服务
湘ICP备2023019799号-1
总访问 次 | 本页访问