理解JS中的this

172 阅读4分钟

前言

this是学习JS时需要面对的三座大山之一,只有将它理解之后学习框架才不会产生疑惑,本文是我对this的一些理解和总结

假设没有this

不用this,想要通过变量获取name,应该怎么做?

let person = {
  name: 'jack',
  sayHi(){
    console.log(`你好,我叫` + person.name)
  }
}

可以用直接保存了对象地址的变量获取'name' 我们把这种办法简称为引用

但这个方法存在两个潜在的问题:

  1. 这个函数和变量绑定在了一起,如下
let sayHi = function(){
  console.log(`你好,我叫` + ???.name)
}
let person = {
  name: 'frank',
  'sayHi': sayHi
}

如果是先声明了函数,再声明变量的话,我们在写这个函数的时候,怎么能确定后面的变量叫person呢?
而且这么定义的话,

  • 如果变量中的person改名了,函数中的person.name也得跟着一起改名
  • 如果函数和变量不在一个文件里面,就无法执行 所以我们不希望sayHi函数里面出现person引用
  1. 如果用类的话,问题就更大了
class Person{
  constructor(name){
    this.name = name 
    // 这里的 this 是 new 强制指定的
  }
  sayHi(){
    console.log(???)
  }
}

我们在声明类的时候,还没有生成任何一个对象,故不可能获取对象的引用,所以???是什么根本写不了

所以现在的问题是,需要一种办法拿到未来的对象,这样才能获取对象的name属性

有一种办法,用参数
可以参考Python的解决方法

class Person:
  def __init__(self, name): # 构造函数
    self.name = name

  def sayHi(self):
    print('Hi, I am ' + self.name)
person = Person('jack')
person.sayHi()

它的特点:

  • 每个函数都接收一个额外的self
  • 这个self就是未来会传进来的对象
  • 在执行person.sayHi()时,其实就等价于执行person.sayHi(person)
  • 在执行之后,person就会被传给了self

把这种思路转移到JS中,就是

对象:
let person = {
  name: 'jack',
  sayHi(p){
    console.log(`你好,我叫` + p.name)
  }
}
person.sayHi(person)
类:
class Person{
  constructor(name){ this.name = name }
  sayHi(p){
    console.log(`你好,我叫` + p.name)
  }
}

这里的p就相当于Python中的self 但这样的方法并不美观,于是JS并没有模仿Python的办法,而是走了另一条路——this

JS在每一个函数里加了this

let person = {
  name: 'jack',
  sayHi(){
    console.log(`你好,我叫` + this.name)
  }
}
person.sayHi()

我们可以用python的方式理解:
JS在函数sayHi()的括号里面加了一个隐藏的this,用来接受未来会被传入的值,
当执行person.sayHi()时,相当于执行person.sayHi(person),
这个括号里的person(是一个地址)就被传入sayHi()里,一个隐藏的this接受了它,这样sayHi就可以通过this引用person

进一步地,可以更直接理解为:
person.sayHi()隐式地把person作为this传给函数sayHi(),即person.sayHi()括号里本来就是一个隐藏的this,只不过JS在执行的时候,会自动把"this=person"了之后,再进行传参

通过this,再看上述的问题1

image.png
即使先声明了函数,再声明变量,最终也能将完美执行
问题2亦是如此,

image.png

但有个问题,现在我们已经习惯用这种隐式传递的调用方式来执行函数了,而根据上述,这样的调用方式并不能方便我们清晰直接地理解this

两种调用

  • person.sayHi() 普通写法,隐式传递
    • 会自动把person传到函数里,作为this
  • person.sayHi.call(person) call写法,显示传递
    • 需要自己手动把person传到函数里,作为this
    • 用这种方法括号里面传什么,this就是什么
    • image.png

尝试使用call调用:

例1

不用this的情况

function add(x,y){
  return x+y
}
add.call(undefined, 1,2)

会得到3
为什么用call的时候,要多写一个undefined?

  • 因为在call中,第一个参数要作为this
  • 但代码中没有this
  • 所以需要用undefined来占位

例2

Array.prototype.forEach2 = function(fn){
  for(let i=0;i<this.length;i++){
    fn(this[i], i, this)
  }
}

这其实就是forEach的原代码,
我们定义一个数组let array = [1,2,3],
用call写法array.forEach2.call(array,(item)=>console.log(item))
和普通写法array.forEach2((item)=>console.log(item))调用的结果都是一样的

image.png

只不过call写法显示地指定了array是this,而普通写法隐式地指定了array是this,
且call里的this也是传什么就是什么,

image.png

所以,我们大部分情况下可以这样理解,this根本就只是一个可以任意指定的隐藏的参数而已,只不过在普通写法里我们不能指定它的值

绑定this

使用.bind可以让this不被改变

function f1(p1, p2){
  console.log(this, p1, p2)
}
let f2 = f1.bind({name:'frank'})
// 那么 f2 就是 f1 绑定了 this 之后的新函数
f2() // 等价于 f1.call({name:'frank'})

.bind还以绑定其他参数

let f3 = f1.bind({name:'frank'}, 'hi')
f3() // 等价于 f1.call({name:'frank'}, hi)