1. 首页

带你了解This 带来的困惑

1 引言

带你了解This 带来的困惑

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
/*Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */

初学者可能突然将 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

带你了解This 带来的困惑

这样 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 // 函数别名
/*Js中文网 - 全球前端挚爱的技术成长平台 https://www.javascriptc.com/ */
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),或将函数直接放在对象中都可以解决问题。

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:带你了解This 带来的困惑

链接:https://www.javascriptc.com/4897.html

« 深度对比Apache CarbonData、Hudi和Open Delta三大开源数据湖方案
自己封装Icon选择器组件(基于vue-element-admin框架)»
Flutter 中文教程资源

相关推荐

QR code