javascript基础- 函数中 this 指向、call、apply、bind

前言

本文简单讲解函数中this的指向问题,及涉及主动改变 this 指向的 call apply bind三个方法的实现原理。

函数内this的指向

在函数内部,this 的值取决于函数如何被调用。

我们先定义两个函数,分别使用和不使用严格模式,返回this,以供后续使用。

js 复制代码
function fnReturnThis() {
  return this
}
function fnReturnThisStrict() {
  "use strict"
  return this
}
console.log(this === window) // true

典型函数调用

js 复制代码
console.log(fnReturnThisStrict() === undefined) // true

const obj = { 
  fnReturnThis, 
  fnReturnThisStrict 
}
console.log(obj.fnReturnThis()) // obj
console.log(obj.fnReturnThisStrict()) // obj

const obj2 = {
  fnReturnThis: obj.fnReturnThis,
  fnReturnThisStrict: obj.fnReturnThisStrict,
  obj,
}
console.log(obj2.fnReturnThis()) // obj2
console.log(obj2.fnReturnThisStrict()) // obj2

console.log(obj2.obj.fnReturnThis()) // obj
console.log(obj2.obj.fnReturnThisStrict()) // obj

const fnReturnObj = {
  fnReturnWrapper: function() { 
    return fnReturnThisStrict()
  }
}
console.log(fnReturnObj.fnReturnWrapper()) // undefined
  • 可以看到,通常情况下,函数内的this都指向当前函数的调用对象(.前面的对象)
  • 部分特殊场景,在非严格模式下存在如下差异化

非严格模式差异化

js 复制代码
console.log(fnReturnThis()) // window
console.log(fnReturnThisStrict()) // undefined

console.log(fnReturnThis.call(null)) // window
console.log(fnReturnThisStrict.call(null)) // null

console.log(fnReturnThis.call(undefined)) // window
console.log(fnReturnThisStrict.call(undefined)) // undefined

console.log(fnReturnThis.call(false)) // Boolean {false}
console.log(fnReturnThisStrict.call(false)) // false

console.log(fnReturnThis.call(0)) // Number {0}
console.log(fnReturnThisStrict.call(0)) // 0

console.log(fnReturnThis.call(BigInt(0))) // BigInt {0}
console.log(fnReturnThisStrict.call(BigInt(0))) // 0n

const symbol = Symbol()
console.log(fnReturnThis.call(symbol)) // Symbol {Symbol()}
console.log(fnReturnThisStrict.call(symbol) === symbol) // true

console.log(fnReturnThis.call('0')) // String {'0'}
console.log(fnReturnThisStrict.call('0')) // '0'

const fnReturnObj = {
  fnReturnWrapper: function() { 
    console.log(
      this,
      fnReturnThis(),
      fnReturnThisStrict()
    )
  }
}
fnReturnObj.fnReturnWrapper() // fnReturnObj  window  undefined
  • 非严格模式下,一个特殊的过程称为 this 替换,确保 this 的值总是一个对象
    • 如果一个函数被调用时 this 被设置为 undefinednullthis 会被替换为 globalThis
    • 如果函数被调用时 this 被设置为一个原始值,this 会被替换为原始值的包装对象。
  • 而严格模式下,this的值自始自终都遵循规范,若未指定值,则指向当前函数的调用对象,否则为指定值。

通过修改原型链,也符合上述标准

js 复制代码
Number.prototype.fnReturnThis = fnReturnThis
Number.prototype.fnReturnThisStrict = fnReturnThisStrict
const num = 1
console.log(num.fnReturnThis()) // Number {1}
console.log(num.fnReturnThisStrict()) // num

作为回调函数执行,同样也符合上述标准

js 复制代码
function callbackFn(fnReturnThis) {
  return fnReturnThis()
}
console.log(callbackFn(fnReturnThis)) // window
console.log(callbackFn(fnReturnThisStrict)) // undefined

const objUnStrict = { fnReturnThis }
const objStrict = { fnReturnThis: fnReturnThisStrict }
function callbackFn2(obj) {
  return obj.fnReturnThis()
}
console.log(callbackFn2(objStrict)) // objStrict
console.log(callbackFn2(objUnStrict)) // objUnStrict

console.log(Array.from(new Array(1)).map(fnReturnThis)[0]) // window
console.log(Array.from(new Array(1)).map(fnReturnThisStrict)[0]) // undefined

console.log(Array.from(new Array(1)).map(fnReturnThis, null)[0]) // window
console.log(Array.from(new Array(1)).map(fnReturnThisStrict, null)[0]) // null

console.log(Array.from(new Array(1)).map(fnReturnThis, 0)[0]) // Number {0}
console.log(Array.from(new Array(1)).map(fnReturnThisStrict, 0)[0]) // 0

所以要确定函数中this,只需按以下步骤:

  • 找到调用的这个函数(一定要找准确),调用this就是
    • 严格模式下,this就是调用对象
    • 非严格模式下,this一定为一个对象
      • 如果为undefinednull,则会被替换为 globalThis
      • 如果为其它原始值,则会被替换为这个原始值的包装对象。

主动修改this指向,call applybind

ts 复制代码
apply<T, R>(this: (this: T) => R, thisArg: T): R;
apply<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args: A): R;

call<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R;

bind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
bind<T, A extends any[], B extends any[], R>(this: (this: T, ...args: [...A, ...B]) => R, thisArg: T, ...args: A): (...args: B) => R;

由于函数中的this指向是在函数执行前确定的,所以同一个函数以不同方式执行会导致函数内部this指向不可预测(只看函数,你永远不会知道下一次这个函数是被谁调用),所以,在必要的时候,需要用call applybind来指定函数内部this

ts 复制代码
function PlusN(n = 1) { 
  this.a = this.a + n
}
const obj = {
  a: 1,
  plusN: PlusN,
}
obj.plusN(1)
console.log(obj.a) // 2

const plus = obj.plusN

plus.call(obj, 1)
console.log(obj.a) // 3

plus.apply(obj, [1])
console.log(obj.a) // 4

const plusBind = PlusN.bind(obj)
plusBind(1)
console.log(obj.a) // 5

plus(1) // TypeError Cannot read properties of undefined (reading 'a')

// 作为构建函数
console.log(new PlusN()) // PlusN {a: 1}
const PlusNBind = PlusN.bind(obj)
console.log(new PlusNBind()) // PlusN {a: 1}
console.log(obj.a) // 5 不会影响 obj

从原理实现 call apply

call apply都是接受第一个参数作为函数内的this,其它入参为函数入参。返回函数执行结果。

也就是需要用第一个入参来调用函数,后续入参作为函数入参。并返回函数执行结果。

js 复制代码
Function.prototype.myCall = function() {
  // 获取传入的数组参数
  let [context, ...args] = arguments

  // if myApply
  // args = args[0] 

  // 这里我们只做原理解析,注入原始值的情况不做考虑
  if (!(context instanceof Object)) throw new TypeError('context must be an object')
  
  // 将函数挂载到传入的context对象上
  let fnSymbolKey = Symbol()
  context[fnSymbolKey] = this

  // 记录返回值
  let res = args.length === 0
    ? context[fnSymbolKey]()
    : context[fnSymbolKey](...args)
  
  // 从上下文中删除函数引用
  delete context[fnSymbolKey]

  // 返回返回值
  return res
}

从原理实现 bind

从目标函数上调用,接受第一个参数作为函数内的this,其它入参为函数入参,返回一个函数,执行函数返回绑定this后的函数执行结果,支持追加函数入参(偏函数)。

ts 复制代码
Function.prototype.myBind = function(...params) {
  if (typeof this !== "function") throw new TypeError("what is trying to be bound is not a function")
  let [oThis, ...args] = params
  
  const Obj = { [this.name]: this }
  let functionToBind = this
  let FunctionBound = function(...boundFunctionParams) {
    return functionToBind.myCall(
      // 作为构建函数使用
      this instanceof FunctionBound ? this : oThis,
      // 偏函数功能
      ...args.concat(...boundFunctionParams)
    )
  }
  // 作为构建函数,将原型对象赋给新的函数
  FunctionBound.prototype = this.prototype
  return Obj[this.name]
}
相关推荐
薛定谔的算法2 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku2 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode2 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu2 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu2 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript
LuckySusu2 小时前
【js篇】深入理解 JavaScript 作用域与作用域链
前端·javascript
LuckySusu2 小时前
【js篇】call() 与 apply()深度对比
前端·javascript
LuckySusu2 小时前
【js篇】addEventListener()方法的参数和使用
前端·javascript
该用户已不存在2 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net