call、apply和bind实现
# call、apply和bind实现
call、apply和bind在Javascript中都是函数对象的方法,作用是用于改变函数的this指向。在语法和特点上,它们有以下区别:
call和apply都是立即执行,并且第一个参数是this的指向,后面剩下的参数是参数列表。bind是返回一个绑定函数,不会立即执行目标函数,并且第一个参数是this的指向,后面剩下的参数是参数列表。call和apply的区别是参数列表不同,call是参数列表,apply是参数数组。bind可以预先传递参数,返回的新绑定函数在调用时接收剩余参数。
# call
# 1. call方法的语法
func.call(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
在调用 func 时要使用的 this 值。如果函数不在严格模式下,null和undefined将被替换为全局对象,并且原始值将被转换为对象。
arg1, …, argN可选
函数的参数,参数列表与普通函数的声明方式一致,是按顺序传递的。
返回值
使用指定this值和参数调用函数后的结果
# 2. 使用例子
function greet() {
console.log(this.name, this.age)
}
const person = {
name: "Evan",
age: 18
}
greet.call(person) // Evan 18
2
3
4
5
6
7
8
9
10
# 3. call方法规范
规范:ECMAScript® 2025 Language Specification (opens new window)
规范的内容如下:
- 让
func等于this的值 - 如果
func不是可调用的(IsCallable(func),一个IsCallable的对象被认为是有内部的[[Call]]方法的对象),则抛出TypeError的错误 - 执行
PrepareForTailCall(用于准备尾调用优化,在ES6中没有直接规定作为单独的步骤,更多的是一种可能的编译器货运行时行为,以提高函数调用性能) - 使用
thisArg作为this值,以及传入的参数,调用func方法并返回
提示
规范还指出,非严格模式下,thisArg 参数为null或undefined时,会自动替换为指向全局对象,如果是其它原始值则会被Object包装。
# 4. call方法实现
实现call方法的基本思路是在thisArg对象上添加一个属性,其属性值指向当前函数,然后执行函数,最后删除该属性。
:::code-group
Function.prototype.call = function (context, ...args) {
var fn = Symbol() // 使用Symbol作为属性名,避免与已有属性冲突
context = context ? Object(context) : window // 使用Object包装
context[fn] = this // 在context添加一个新的属性,值为当前函数
const result = context[fn](...args) // 执行函数
delete context[fn] // 执行后删除该属性
return result // 返回函数执行结果
}
2
3
4
5
6
7
8
Function.prototype.call = function (context) {
context = context ? Object(context) : window
// 需要保证context原本没有fn属性,解决的办法是随机生成
context.fn = this
// 参数拼接
var args = []
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 执行函数,会隐式调用args.toString()方法
var result = eval('context.fn(' + args + ')')
delete context.fn
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
:::
代码说明
- 往
context添加属性时,需要确保新添加的属性不能与原对象上的属性冲突,在ES6中,可以使用Symbol作为属性名,在ES6以下一般使用随机生成的属性名(生成后仍需确保不在原对象上存在)。 - ES6以下的函数参数不支持扩展操作符,所以需要使用
arguments对象来获取参数列表,然后拼接成字符串,使用eval执行函数。 - 以上(以及下文的
apply)实现的是非严格模式下的方法,严格模式不会对thisArg是undefined和null做转换以及原始类型的包装处理
# apply
# 1. apply方法的语法
func.apply(thisArg[, argsArray])
参数
thisArg
在调用 func 时要使用的 this 值。如果函数不在严格模式下,null和undefined将被替换为全局对象,并且原始值将被转换为对象。
argsArray可选
一个类数组对象,用于指定调用func时的参数,或者如果不需要向函数提供参数,则为null或undefined。
返回值
使用指定this值和参数调用函数后的结果
提示
apply方法与call几乎完全相同,只是函数参数在call中逐个传递,而apply方法接收一个数组作为参数列表。
# 2. 使用例子
const array = ['a', 'b']
const elements = [0, 1, 2]
array.push.apply(array, elements)
console.log(array) // ["a", "b", 0, 1, 2]
2
3
4
# 3. apply方法规范
规范:ECMAScript 2025 Language Specification (opens new window)
规范的内容如下:
- 让
func等于this的值 - 如果
func不是可调用的,则抛出TypeError的错误 - 如果
argsArray是null或undefined,则:- a. 执行
PrepareForTailCall - b. 调用函数并返回结果
- a. 执行
- 否则,从类数组
argsArray中获取参数,创建新数组argsList - 执行
PrepareForTailCall - 调用函数并返回结果
# 4. apply方法实现
apply方法的实现思路与call方法类似,只是apply方法接收一个数组作为参数,而call方法接收多个参数。`
:::code-group
Function.prototype.apply = function (context, arr) {
const fn = Symbol()
context = context ? Object(context) : globalThis
context[fn] = this
if(!arr) {
result = context[fn]()
} else { // array-like
const argList = []
for(let i = 0; i < arr.length; i++) {
argList.push(arr[i])
}
let result = context[fn](...argList)
}
delete context[fn]
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.apply = function (context, arr) {
context = context ? Object(context) : window
context.fn = this
var result
if (!arr) {
result = context.fn()
} else {
var argList = []
for (var i = 0, len = arr.length; i < len; i++) {
argList.push('arr[' + i + ']')
}
result = eval('context.fn(' + argList + ')')
}
return result
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:::
# bind
bind方法创建一个新的函数,当调用该新函数(绑定函数)时,它会调用原始函数(目标函数)并且将其this设置为创建时给定的值。bind还可以预先传入一系列的指定的参数,这些参数在新函数调用时,会被插入到参数列表的前面。
# 1. bind方法语法
func.bind(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
在调用绑定函数时,作为 this 参数传入目标函数 func 的值。如果函数不在严格模式下,null 和 undefined 会被替换为全局对象,并且原始值会被转换为对象。如果使用 new 运算符构造绑定函数,则忽略该值。
arg1, arg2, ... 可选
在调用func时,预先设定的插入到绑定函数参数列表的头部参数。
返回值
使用指定的 this 值和初始参数(如果提供)创建的给定函数的副本。
# 2. 使用例子
:::code-group
var x = 42
const module = {
x: 81,
getX: function() {
return this.x
}
}
const getX = module.getX
const getXBind = getX.bind(module)
console.log(getX()) // 42
console.log(getXBind()) // 81
2
3
4
5
6
7
8
9
10
11
12
function sum (a, b, c, d) {
return a + b + c + d
}
const sumArgs = sum.bind(null, 1, 2)
console.log(sumArgs(3, 4)) // 10
2
3
4
5
6
:::
# 3. bind方法规范
规范:ECMAScript 2025 Language Specification (opens new window) Function.prototype.bind ( thisArg, ...args )
- 令
Target的值为this - 如果
Target不可调用,则抛出TypeError的异常 - 令
F为通过调用BoundFunctionCreate函数创建的新绑定函数,该函数以Target、thisArg和args作为参数 - 令
L为0(表示函数F的长度) - 检查
Target是否拥有名为length的自身属性,并将结果存储在targetHasLength中 - 如果
targetHasLength为真,则执行以下步骤:- 令
targetLen为Target的length属性的值 - 如果
targetLen是一个数字,则执行以下步骤:- 如果
targetLen是正无穷大,则将L设置为正无穷大 - 否则如果
targetLen是负无穷大,则将L设置0 - 否则:
- 令
targetLenAsInt为targetLen转换为整数或无穷大的结果 - 断言:
targetLenAsInt是有限的 - 令
argCount为args数组中元素的数量 - 将
L设置为targetLenAsInt减去argCount和0之间较大的值
- 令
- 如果
- 令
- 执行
SetFunctionLength(F,L)操作,以设置函数F的长度为L - 令
targetName为从Target上获取的name属性值 - 如果
targetName不是一个字符串,则将其设置为空字符串 - 执行
setFunctionName(F,targetName, "bound")操作,以设置函数F的名称为targetName前缀为"bound"的新名称 - 返回
F函数
提示
规范还指出,当使用bind生成的新函数被用作构造函数时,this的值会被忽略,并且new操作符会创建一个新对象,该对象的__proto__属性指向目标函数的prototype属性。
从以上的规范的步骤来看,实现bind方法的步骤是相对复杂的,完整的实现步骤如下:
Function.prototype.bind (thisArg [, arg1 [, arg2, …]]) (opens new window)
对于规范中的实现,有些特性是基于内部属性来完成的,有些内部属性无法通过Javascript来模拟实现(如下文的[[TargetFunction]]),因此,我们自己的bind方法实现通常很难完整的实现整个标准,实现的方式一般如下:
Function.prototype.bind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind must be called on a function')
}
const self = this // 保存目标函数
const slice = Array.prototype.slice
const args = slice.call(arguments, 1) // 剩余参数为预绑定的参数
// 创建一个新的函数
function bound() {
// 1. 参数合并
// 2. 判断是否使用new来调用函数
return self.apply(this instanceof bound ? this : context, args.concat(slice.call(arguments)))
}
// 原型继承,确保作为构造函数时的原型正确
bound.prototype = Object.create(self.prototype)
return bound
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以上代码已经完成了bind方法的基本实现,按照规范,我们还得为绑定函数定义name和length属性,其中name属性是不可直接改变的,length属性是可变的,因此,我们可以考虑通过Object.defineProperty来设置name和length属性。增加如下代码:
// ...
// 设置函数的name和length属性
Object.defineProperty(bound, 'name', {
configurable: true,
enumerable: false,
writable: false,
value: `bound ${self.name}`
})
Object.defineProperty(bound, 'length', {
configurable: true,
enumerable: false,
writable: false,
value: Math.max(0, self.length - args.length)
})
return bound
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
测试如下:
const Hello = function () {}
const HelloBind = Hello.bind(null) // 原生bind
const HelloBind2 = Hello.myBind(null) // 假设我们先的方法名是myBind
const hello = new HelloBind()
const hello2 = new HelloBind2()
console.log(hello instanceof HelloBind) // true
console.log(hello2 instanceof HelloBind2) // true
console.log(hello instanceof Hello) // true
console.log(hello2 instanceof Hello) // true
console.log(HelloBind.name) // bound Hello
console.log(HelloBind2.name) // bound Hello
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,我们实现的bind方法与原生的bind方法表现基本一致。但实际上,对于一些特性是仍然存在表现不一致的情况。如下:
console.log(HelloBind.prototype) // undefined
console.log(HelloBind2.prototype) // Hello()
2
为了实现当使用new操作符来调用绑定函数时,保证创建的实例在原型链上的正确,我们也为bound函数增加了prototype属性,而标准实现中的返回的绑定函数是没有prototype属性的。那如果不增加prototype属性的话,又怎么让instanceof操作符正确判断绑定函数的实例类型呢?
我们都知道,instanceof的基本原理是判断某个构造函数的原型是否出现在指定对象的原型链里,而原生的bind方法在绑定函数的prototype属性为undefined时却仍然可以让instanceof操作符成立,这其实要涉及到原生的instanceof方法的底层机制了。简单来说,在A instanceof B两边的操作数合法的情况下,instanceof运行的机制大致流程如下:
- 从
B中获取[Symbol.hasInstance]的值,如果获取到的值是个函数的话,则调用该函数并转为布尔值来判断是否成立 - 否则,如果
B是一个绑定函数(具备has a [[BoundTargetFunction]] internal slot的函数),则从中读取内部的[[BoundTargetFunction]](在实现中是内部属性名是[[TargetFunction]]),执行A instanceof [[TargetFunction]]的返回值作为结果 - 否则不断地从
A的原型链中读取__proto__属性,如果读取到的值与B的原型一致,则返回true,否则继续读取,直到读取到null,则返回false

从instanceof的执行机制以及上图,我们可以看到,如果我们能够在绑定函数bound上部署一个[Symbo.hasInstance]方法从而拦截instanceof操作符,则可以让instanceof操作符成立的同时,让bound的prototype属性为undefined。另外,在使用new调用绑定函数时,实际应该返回的是目标函数的实例。因此,我们做以下代码修改:
Function.prototype.bind = function (context) {
// ...
// 创建一个新的函数
function bound() { // [!code --]
// 1. 参数合并 // [!code --]
// 2. 判断是否使用new来调用函数 // [!code --]
return self.apply(this instanceof bound ? this : context, args.concat(slice.call(arguments))) // [!code --]
} // [!code --]
bound.prototype = Object.create(self.prototype) // 原型继承,确保作为构造函数时的原型正确 // [!code --]
function bound() { // [!code ++]
if (new.target) { // [!code ++]
return new self(args.concat(slice.call(arguments))) // [!code ++]
} else { // [!code ++]
return self.apply(context, args.concat(slice.call(arguments))) // [!code ++]
} // [!code ++]
} // [!code ++]
// 判断是否使用new来调用函数,同时保证new出来的实例也是bound的一个实例 // [!code ++]
const proto = { // [!code ++]
[Symbol.hasInstance](instance) { // [!code ++]
return Function.prototype[Symbol.hasInstance].call(self, instance) // [!code ++]
} // [!code ++]
} // [!code ++]
proto.__proto__ = bound.__proto__ // 保持原来的原型链 // [!code ++]
bound.__proto__ = proto // 拦截instanceof // [!code ++]
bound.prototype = undefined // [!code ++]
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
改造后,以下代码测试均通过
const Hello = function () {}
const HelloBind = Hello.bind(null) // 原生bind
const HelloBind2 = Hello.myBind(null) // 假设我们先的方法名是myBind
const hello = new HelloBind()
const hello2 = new HelloBind2()
console.log(hello instanceof HelloBind) // true
console.log(hello2 instanceof HelloBind2) // true
console.log(hello instanceof Hello) // true
console.log(hello2 instanceof Hello) // true
console.log(HelloBind.name) // bound Hello
console.log(HelloBind2.name) // bound Hello
console.log(HelloBind.prototype) // undefined
console.log(HelloBind2.prototype) // undefined
console.log(hello.__proto__ === hello2.__proto__) // true
console.log(hello.constructor.name) // Hello
console.log(hello2.constructor.name) // Hello
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.bind最终实现的代码如下:
Function.prototype.bind = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind must be called on a function')
}
const self = this // 保存目标函数
const slice = Array.prototype.slice
function bound() {
if (new.target) {
return new self([...args, ...(slice.call(arguments))])
} else {
return self.apply(context, [...args, ...(slice.call(arguments))])
}
}
// 判断是否使用new来调用函数,同时保证new出来的实例也是bound的一个实例
const proto = {
[Symbol.hasInstance](instance) {
return Function.prototype[Symbol.hasInstance].call(self, instance)
}
}
proto.__proto__ = bound.__proto__ // 保持原来的原型链
bound.__proto__ = proto // 拦截instanceof
bound.prototype = undefined
// 设置函数的name和length属性
Object.defineProperty(bound, 'name', {
configurable: true,
enumerable: false,
writable: false,
value: `bound ${self.name}`
})
Object.defineProperty(bound, 'length', {
configurable: true,
enumerable: false,
writable: false,
value: Math.max(0, self.length - args.length)
})
return bound
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41