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