前言
本文简单讲解函数中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
被设置为undefined
或null
,this
会被替换为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
一定为一个对象- 如果为
undefined
或null
,则会被替换为globalThis
- 如果为其它原始值,则会被替换为这个原始值的包装对象。
- 如果为
- 严格模式下,
主动修改this
指向,call
apply
与 bind
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
apply
与 bind
来指定函数内部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]
}