2022-09-21前沿技术00
请注意,本文编写于 554 天前,最后修改于 554 天前,其中某些信息可能已经过时。

1 引言

logo

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

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

2 内容概要

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

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,看如下的例子:

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 丢失带来的隐患:

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 的作用范围是很头疼的事。因此作者有如下解决方案:

function getPerson(initialName) {
  let name = initialName
  const person = {
    setName(strName) {
      name = strName
    }
  }
  return person
}

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

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

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。

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

3.1.2 隐式绑定

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

function foo(){
  console.log(this.count) // 2
}
var obj = {
  count: 2,
  foo: foo
}
obj.foo()

3.1.3 别名丢失隐式绑定

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

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 会丢失。

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 属于显示绑定。

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 不会丢失。

function foo(){
  setTimeout(() => {
    console.log(this.count) // 2
  })
}
var obj = {
  count: 2
}
foo.call(obj)

3.2.3 函数 bind

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

function foo(){
  setTimeout(function() {
    console.log(this.count) // 2
  }.bind(this))
}
var obj = {
  count: 2
}
foo.call(obj)

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

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),或将函数直接放在对象中都可以解决问题。

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!