前言

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

函数内this的指向

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

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


function fnReturnThis() { return this } function fnReturnThisStrict() { "use strict" return this } console.log(this === window) // true

典型函数调用


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都指向当前函数的调用对象(.前面的对象)
  • 部分特殊场景,在非严格模式下存在如下差异化

非严格模式差异化


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 被设置为 undefined 或 null,this 会被替换为 globalThis
    • 如果函数被调用时 this 被设置为一个原始值,this 会被替换为原始值的包装对象。
  • 而严格模式下,this的值自始自终都遵循规范,若未指定值,则指向当前函数的调用对象,否则为指定值。

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


Number.prototype.fnReturnThis = fnReturnThis Number.prototype.fnReturnThisStrict = fnReturnThisStrict const num = 1 console.log(num.fnReturnThis()) // Number {1} console.log(num.fnReturnThisStrict()) // num

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


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一定为一个对象
      • 如果为undefined 或 null,则会被替换为 globalThis
      • 如果为其它原始值,则会被替换为这个原始值的包装对象。

主动修改this指向,call apply 与 bind


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 apply 与 bind来指定函数内部this:


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,其它入参为函数入参。返回函数执行结果。

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


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后的函数执行结果,支持追加函数入参(偏函数)。


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] }

更多推荐