this
的动态切换为 JavaScript 创造了巨大的灵活性,有时,需要把 this
固定下来,避免出现意想不到的情况。JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定 this
的指向。
Function.prototype.call()
Function
实例的 call()
方法使用给定的 this
值和单独提供的参数列表调用此函数,并返回调用函数的结果,若该方法没有返回值则返回 undefined
。
call(thisArg, arg1, /* …, */ argN)
thisArg
在 function
函数运行时使用的 this
值。请注意,this
可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null
或 undefined
时会自动替换为指向全局对象,原始值会被包装。
通常,当调用函数时,函数内部 this
的值就是该函数被访问的对象。使用 call()
,在调用现有函数时将任意值分配为 this
,而无需首先将该函数作为属性附加到对象,允许将一个对象的方法用作通用实用程序函数。
call()
允许为不同的对象分配和调用属于一个对象的函数/方法。call()
提供新的 this
值给当前调用的函数/方法。可以使用 call
来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
function greet() {
console.log(this, `${this.name} say hello ${this.age}`)
}
const o = {
name: 'kobe',
age: 38
}
greet() // Window, 'undefined say hello undefined'
greet.call(o) // {name: 'kobe', age: 38}, 'kobe say hello 38'
根据 call
方法描述造个轮子,模拟 call
方法。
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') {
// 调用者非函数直接抛出错误
throw new TypeError(`${this} is not a function`)
}
if (context === null || context === void 0) {
// null 和 undefined 替换为全局对象
context = globalThis
} else if (typeof context !== 'object') {
// 原始值替换为包装对象
context = Object(context)
}
// 防止 key 重复以及被外部调用
const thisArg = Symbol()
// 将 this 挂载到 context 的原型链上
// 防止 thisArg 属性被遍历
Reflect.defineProperty(context.__proto__, thisArg, {
value: this,
enumerable: false,
configurable: true
})
// 通过 context 调用重新绑定 this
const result = context[thisArg](...args)
// 移除 context 的原型链上的 this
Reflect.deleteProperty(context.__proto__, thisArg)
return result
}
greet.myCall(o) // {name: 'kobe', age: 38} 'kobe say hello 38'
Function.prototype.apply()
Function
实例的 apply()
方法使用给定的 this
值调用此函数,并且 arguments
作为数组(或类数组)。
apply(thisArg, argsArray)
在调用一个存在的函数时,可以为其指定一个 this
对象。this
指当前对象,也就是正在调用这个函数的对象。使用 apply
,可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。
apply()
与 call()
非常相似,不同之处在于提供参数的方式。apply
使用参数数组而不是一组参数列表(可以使用数组字面量)。也可以使用 arguments
对象作为 argsArray
参数。
arguments
是一个函数的局部变量。它可以被用作被调用对象的所有未指定的参数。这样,在使用 apply
函数的时候就不需要知道被调用对象的所有参数。可以使用 arguments
来把所有的参数传递给被调用对象。被调用对象接下来就负责处理这些参数。
利用 apply
,我们可以做一些有趣的应用:
- 找出数组最大元素
const a = [10, 2, 4, 15, 9]
Math.max.apply(null, a) // 15
- 将数组的空元素变为
undefined
空元素与 undefined
的差别在于,数组的 forEach
方法会跳过空元素,但是不会跳过 undefined
。因此,遍历内部元素的时候,会得到不同的结果。
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
- 转换类似数组的对象
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
// => Array.from({0: 1, length: 1})
- 绑定回调函数的对象
前面“注意回调函数中的 this
”的例子中可以改写为:
const o = new Object()
o.f = function () {
console.log(this) // $('#button')
}
const f = function () {
o.f.aplly(o, Array.from(arguments))
// 或者
// o.f.call(o, ...Array.from(arguments)
}
$('#button').on('click', f)
根据 apply
方法描述造个轮子,模拟 apply
方法,修改以下 myCall
的参数和调用部分。
Function.prototype.myApply = function (context, argsArray) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not a function`)
}
if (context === null || context === void 0) {
context = globalThis
} else if (typeof context !== 'object') {
context = Object(context)
}
const thisArg = Symbol()
Reflect.defineProperty(context.__proto__, thisArg, {
value: this,
enumerable: false,
configurable: true
})
// 判断数组或类数组,转换类数组
const args = []
if (argsArray && (typeof arrayLike === 'object') && ('length' in argsArray)) {
for (let i = 0; i < argsArray.length; i++) {
args[i] = argsArray[i]
}
} else {
throw new TypeError('CreateListFromArrayLike called on non-object')
}
const result = context[thisArg](...args)
Reflect.deleteProperty(context.__proto__, thisArg)
return result
}
Array.prototype.slice.myApply({ 0: 1, length: 1 }) // [1]
Array.myApply(null, ['a', , 'b']) // ['a', undefined, 'b']
Function.prototype.bind()
Function
实例的 bind()
方法创建一个新函数,该函数在被调用时调用此函数,并将其 this
关键字设置为提供的值和给定序列调用新函数时提供的任何参数之前的参数。
bind(thisArg, arg1, /* …, */ argN)
thisArg
调用绑定函数时作为 this
参数传递给目标函数的值。如果使用 new
运算符构造绑定函数,则忽略该值。当使用 bind
在 setTimeout
中创建一个函数(作为回调提供)时,作为 thisArg
传递的任何原始值都将转换为 object
。如果 bind
函数的参数列表为空,或者 thisArg
是 null
或 undefined
,执行作用域的 this
将被视为新函数的 thisArg
。
bind()
和 call()
方法类似,但是 bind()
方法不会立即调用,而是创建一个新的绑定函数(bound function,BF)。绑定函数是一个怪异函数对象(exotic function object),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。绑定函数具有以下内部属性:
[[BoundTargetFunction]]
- 包装的函数对象。[[BoundThis]]
- 在调用包装函数时始终作为this
值传递的值。[[BoundArguments]]
- 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。[[Call]]
- 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this
值和一个包含通过调用表达式传递给函数的参数的列表。
当调用绑定函数时,它调用 [[BoundTargetFunction]]
上的内部方法 [[Call]]
,就像这样 Call(boundThis, args)
。其中,boundThis
是 [[BoundThis]]
,args
是 [[BoundArguments]]
加上通过函数调用传入的参数列表。
绑定函数也可以使用 new
运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this
值会被忽略,但前置参数仍会提供给模拟函数。
const d = new Date()
const p = d.getTime
console.log(p()) // TypeError: this is not a Date object.
使用 bind()
方法修改一下。
const p = d.getTime.bind(d)
console.log(p()) // 16952222222222
bind()方法有一些使用注意点。
- 每一次返回一个新函数:
bind()
方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样:
element.addEventListener('click', o.m.bind(o))
click
事件绑定 bind()
方法生成的一个匿名函数,这样会导致无法取消绑定。
- 结合回调函数使用:回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含
this
的方法直接当作回调函数。
const counter = {
count: 0,
inc: function () {
'use strict'
this.count++
}
}
counter.inc.bind(counter)()
console.log(counter.count) // 1
还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。
const o = {
name: 'jay',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name)
}.bind(this))
}
}
o.print()
- 结合
call()
方法使用
利用 bind()
方法,可以改写一些 JavaScript 原生方法的使用形式。
console.log([1, 2, 3].slice(0, 1)) // [1]
// 等同于
console.log(Array.prototype.slice.call([1, 2, 3], 0, 1)) // [1]
call()
方法实质上是调用 Function.prototype.call()
方法,因此上面的表达式可以用bind()
方法改写。
const slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
根据 bind
方法描述造个轮子,模拟 bind
方法。
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== 'function') {
// 调用者非函数直接抛出错误
throw new TypeError(`${this} is not a function`)
}
if (context === null || context === void 0) {
// null 和 undefined 替换为全局对象
context = globalThis
} else if (typeof context !== 'object') {
// 原始值替换为包装对象
context = Object(context)
}
const fn = this
// 创建一个新的绑定函数
function Bound() {
// 修改 this 指向,new 绑定函数忽略第一参数
return fn.myApply(
new.target ? this : context,
args.concat(Array.from(arguments))
)
}
// 修改绑定函数的原型链
Bound.prototype = this.prototype
return Bound
}
// 测试数组方法
const slice = Function.prototype.call.myBind(Array.prototype.slice)
console.log(slice([1, 2, 3], 0, 1)) // [1]
// 测试 new 绑定函数
function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.toString = function () {
return this.x + ',' + this.y
}
var YAxisPoint = Point.bind(null, 0)
var axisPoint = new YAxisPoint(5)
console.log(axisPoint) // {x: 0, y: 5}
console.log(axisPoint instanceof Point) // true
console.log(axisPoint instanceof YAxisPoint) // true
console.log(new Point(17, 42) instanceof YAxisPoint) // true
bind()
方法主要有以下几个作用:
- 改变函数的
this
:可以将函数的this
绑定到指定的对象上,这对于需要在函数内部使用特定对象的方法或属性时非常有用,可以确保函数在执行时始终具有正确的上下文。 - 创建偏函数:偏函数是指固定函数的一些参数,然后返回一个新函数,新函数可以接收剩余的参数并执行原函数。使用
bind()
可以轻松地创建一个新函数,并为其传递一部分参数,这样当调用新函数时,仍然可以接收剩余的参数。 - 实现函数柯里化:柯里化是指将原来接受多个参数的函数转换为一系列只接受单个参数的函数。使用
bind()
可以方便地实现函数柯里化,即将多参数函数转换为单参数函数的链式调用。 - 延迟执行函数:可以将某个函数的执行延迟到稍后的时间点。通过绑定函数内部的
this
和一些参数,我们可以在需要执行该函数时,再进行调用。这对于事件处理程序、定时器等场景非常有用。 - 实现函数复用:通过预设一些参数,并使用
bind()
函数生成一个新函数,可以实现函数的复用。这样,我们可以创建多个功能类似但部分参数不同的函数,提高代码复用性。
总结
三者都具有改变函数 this
的作用。
call()
和bind()
都是可变长参数,而apply()
的参数是数组。call()
和apply()
立即执行函数,而bind()
返回一个新函数,可以延迟执行。call()
和apply()
传入参数是按顺序传入的,而bind()
传入参数是预设的,并且可以传入任意多的参数。call()
和apply()
仅能改变函数的this
,而bind()
还可以预设一些参数,返回一个新函数,新函数可以接收剩余的参数并执行原函数。