前端温故知新之 this

92 阅读7分钟

TjDet1.png

1、概念

this提供了一种隐形的传递一个对象的引用,是执行上下文创建时确定的一个在执行过程中不可更改的变量

2、绑定规则

this在函数调用阶段确定,也就是执行上下文创建的阶段进行赋值,保存在变量对象中,即当函数在不同的调用方式下都可能会导致this的值不同,this的最终指向是那个调用它的对象

2.1 默认绑定(函数直接调用)

独立函数运行默认指向全局对象window(浏览器环境)

var name = 'hello'
function test() {
  console.log(this.name) // 'hello',this 等于 window
}
test()

使用let/const定义的变量是不会被绑定到window对象上的,而var声明的变量会挂载在window对象上当作对象属性

var a = 'a'
let b = 'b'
const c = 'c'
function test() {
  console.log(this.a) // 'a',相当于window.a
  console.log(this.b) // undefined
  console.log(this.c) // undefined
}
test()

2.2 隐式绑定(对象方法调用)

函数的调用是在某个对象上触发的,对象属性链调用则指向最后一层

function sayHi() {
  console.log(this.name) // person2,对象属性链调用 this 指向最后一层的 person 对象
}
let person2 = {
  name: 'person2',
  sayHi: sayHi
}
let person1 = {
  name: 'person1',
  person: person2
}
person1.person.sayHi()

2.2.1 隐式绑定丢失

由于某些原因丢失了绑定对象,则会应用默认绑定

引用赋值

把一个隐式绑定的函数赋给了另一个变量或把函数当作参数传入到另一个函数

var name = 'windowName'
function callbackFn(fn) {
  fn()
}
let obj = {
  name: 'hello',
  getName: function () {
    console.info(this.name)
  }
}
var newName = obj.getName
newName() // 'windowName',调用 newName 函数为直接调用,与 obj 对象无关了
callbackFn(obj.getName) // 'windowName',调用 callbackFn 中的 fn 函数为直接调用

内置函数

内置函数中如setTimeoutsetInterval等定时器,它俩的第一个参数是回调函数,这个回调函数中this指向window

var name = 'windowName'
setTimeout(function () {
  console.log(this.name) // 'windowName'
}, 1000)

2.3 显式绑定(call/apply/bind 指定 this 对象)

call/apply/bind可以改变函数执行时的this指向,显式的指定this所指向的对象

// 语法
fn.call(obj, param1, param2, ...)
fn.apply(obj, [param1,param2,...])
fn.bind(obj, param1, param2, ...)
  • fnthis指向obj对象(objnullundefined则出现绑定丢失,使用默认绑定)

  • call/apply返回fn函数执行的结果,bind返回fn函数的拷贝,拥有指定的this,需要手动触发执行该函数

  • callapply传参不同,call是使用逗号相隔依次传递,而apply则是以数组形式传递

为了借助已实现的方法,改变方法中数据的this指向,减少重复代码,节省内存

const arr = [1, 527, 102, 3, 15]
Math.max.apply(null, arr) // 527

call/apply/bind 模拟实现

// call/apply 只是所传参数不同,只需要对入参进行调整即可
Function.prototype.myCall = function (context, ...args) {
  context = context || window // 传入的为 null 和 undefined,则应用默认绑定规则 this 指向 window 对象,原始值则自动包装对象
  let fn = Symbol() // 创造唯一的 key 值
  context[fn] = this // 设置属性值为所传函数,为后面调用,隐式绑定 this 做准备
  let result = context[fn](...args) // 执行函数返回结果,隐式绑定,所以 this 指向 context
  delete context[fn] // 删除新增属性
  return result
}
function getName(a, b) {
  console.info(this.name + a + b)
}
let obj = {
  name: 'haha'
}
getName.myCall(obj, 1, 2) // 'haha12'
Function.prototype.myBind = function (objThis, ...params) {
  const thisFn = this
  let fToBind = function (...secondParams) {
    const isNew = this instanceof fToBind // this 是否是 fToBind 的实例 也就是返回的 fToBind 是否通过 new 调用
    const context = isNew ? this : objThis // new 调用就绑定到 this 上,否则就绑定到传入的 objThis 对象上
    return thisFn.call(context, ...params, ...secondParams) // 用 call 调用函数绑定 this 的指向并传递参数,返回执行结果,即上面 myCall 实现
  }
  if (thisFn.prototype) {
    // 复制源函数的 prototype 给 fToBind 一些情况下函数没有 prototype,比如箭头函数
    fToBind.prototype = Object.create(thisFn.prototype)
  }
  return fToBind // 返回拷贝的函数
}

function getNames(a, b) {
  console.info(this.name + ',' + a + ',' + b)
}
function GetName(a, b) {
  console.info(this.name + '-' + a + '-' + b)
}
GetName.prototype.name = 'name'
GetName.prototype.say = function () {
  console.log('123')
}
let obj = {
  name: 'haha'
}

// 普通函数调用
let bindFun = getNames.myBind(obj, 'params')
bindFun('secondParams') // 'haha,params,secondParams'

// 构造函数调用
let bindFuns = GetName.myBind(obj, 'params')
let fun = new bindFuns('secondParams') // 'name-params-secondParams'
fun.say() // '123'

2.4 new 绑定(构造函数调用)

new关键字用来创建一个类(模拟类)的实例对象,实例化对象之后,也就继承了类的属性和方法

function objectFactory() {
  // shift 返回第一个参数构造函数,并改变 arguments 参数原数组
  let constructor = [].shift.call(arguments)
  // 判断 constructor 是否是一个函数
  if (typeof constructor !== 'function') {
    console.error('type error')
    return
  }
  // 创建一个空的对象并链接到构造函数的原型,使它能访问原型中的属性
  let newObject = Object.create(constructor.prototype)
  // 将 this 指向新建对象,并执行函数
  let result = constructor.apply(newObject, arguments)
  // 判断返回对象类型,如果是引用类型,则返回该引用类型的对象,否则返回新建的对象
  let flag = result && (typeof result === 'object' || typeof result === 'function')
  // 判断返回结果
  return flag ? result : newObject
}
function SayName(name) {
  this.name = name
}
let obj = objectFactory(SayName, 'haha')
console.info(obj.name) // 'haha'

2.5 特殊绑定(箭头函数)

  • 箭头函数没有this,绑定的是最近一层非箭头函数作用域的this

  • 箭头函数的指向永远是最近一层非箭头函数作用域的this指向(显式绑定无法改变this指向,但可以通过修改外层的this来达成间接修改),并且没有argumentsprototype等对象,也不可以当作构造函数使用

var name = 'window'
var obj1 = {
  name: 'obj1',
  foo1: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  foo2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.foo1.call(obj2)() // 'obj2' 'obj2'
obj1.foo1().call(obj2) // 'obj1' 'obj1'
obj1.foo2.call(obj2)() // 'window' 'window'
obj1.foo2().call(obj2) // 'window' 'obj2'
  • obj1.foo1.call(obj2)()obj1.foo1通过call显示绑定obj2,所以第一个name返回obj2,第二层为箭头函数,所以它的this继承上一层的obj1.foo1this同样返回obj2

  • obj1.foo1().call(obj2)obj1.foo1()通过隐式绑定obj1,所以第一个name返回obj1,第二层为箭头函数显式绑定无效,还是返回obj1

  • obj1.foo2.call(obj2)(),第一层为箭头函数显式绑定无效,继承外层的作用域为window,返回window,第二层函数最后调用为window,所以也返回window

  • obj1.foo2().call(obj2),第一层为箭头函数,继承外层的作用域为window,返回window,第二层函数显示绑定obj2,所以返回obj2

3、绑定优先级

new绑定 > 显式绑定(bind) > 隐式绑定 > 默认绑定

4、综合经典题

4.1 题目一

function Foo() {
  getName = function () {
    console.log(1)
  }
  return this
}
Foo.getName = function () {
  console.log(2)
}
Foo.prototype.getName = function () {
  console.log(3)
}
var getName = function () {
  console.log(4)
}
function getName() {
  console.log(5)
}
Foo.getName() // '2'
getName() // '4'
Foo().getName() // '1'
getName() // '1'
new Foo.getName() // '2'
new Foo().getName() // '3'
new new Foo().getName() // '3'
  • Foo.getName(),直接执行FoogetName静态属性,返回 2

  • getName(),变量提升之后,全局变量的getName覆盖getName函数,返回 4

  • Foo().getName(),先执行Foo函数,返回this,这是this指向window,然后getName 方法,覆盖全局变量的getName调用,返回 1

  • getName(),调用全局函数,使用上次更新覆盖的getName函数,返回 1

  • new Foo.getName(),先计算Foo.getName(),再计算new,返回 2

  • new Foo().getName(),先计算new Foo()this指向实例,再计算.getName(),因为实例中没有属性,所以接着原型找,返回 3

  • new new Foo().getName(),先计算new Foo().getName(),然后再new一次,同样查找原型链,返回 3

4.2 题目二

var number = 5
var obj = {
  number: 3,
  fn: (function () {
    var number
    this.number *= 2
    number = number * 2
    number = 3
    return function () {
      var num = this.number
      this.number *= 2
      console.log(num)
      number *= 3
      console.log(number)
    }
  })()
}
var myFun = obj.fn
myFun.call(null) // '10' '9'
obj.fn() // '3' '27'
console.log(window.number) // '20'
  • 首先obj定义的时候,fn自执行函数触发,形成闭包并返回一个匿名函数。自执行函数的this指向window,所以此时window.number为 10,fn函数的局部变量number为 3

  • myFun.call(null),隐式绑定丢失,this指向window,执行fn函数,此时num等于window.number,打印 10,window.number计算为 20,number等于闭包局部变量乘以 3,打印 9

  • obj.fn(),隐式绑定,this指向obj,执行fn函数,此时num等于3,打印 3,obj.number计算为 6,number等于闭包局部变量乘以 3,打印 27,此时window.number还是 20

参考