本文详细解析了JavaScript中Function原型链上的bind、call和apply三个方法。通过对比它们的语法与使用场景,深入探讨了它们改变this指向的机制,并提供了从源码层面实现这三个方法的完整代码与逻辑分析,帮助开发者彻底理解其底层原理。
bind、call和apply均位于Function的原型链上,这意味着无论是通过function关键字声明的普通函数,还是箭头函数,都可以直接调用这三个方法。它们的核心功能之一是能够改变this的指向,本文将深入探讨如何手动实现这三个方法。
bind、call和apply的基本用法
bind方法详解
bind()允许函数对象进行调用,其执行结果是返回一个全新的函数。
语法定义
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind()接收的第一个参数将被设定为新函数的this。若调用时未提供参数列表,或者第一个参数为null与undefined,则新函数的this将继承原函数执行时的this。在使用bind()时,需注意以下关键点:
- 该方法返回一个新的函数引用,但不会立即执行该函数。
- 通过传入的参数列表来绑定
this的指向;若未显式传入thisArg,则需明确指定this的目标。 - 对于箭头函数,由于其内部this是词法绑定的,因此无法通过bind改变其this指向,仅能绑定参数。这一点在讨论箭头函数特性时已提及,故不建议在此场景下使用箭头函数。
具体示例如下:
常规使用场景
function fn(a) {
console.log(this, a)
}
const fn1 = fn.bind({x: 100}); // fn1是一个函数,但是并没有立即执行
fn1(); // {x:100} 100
console.log(fn === fn1); // false,bind返回的是一个新的函数
箭头函数场景
const fn = (a) => {
console.log(this, a);
}
const fn1 = fn.bind({x: 100}, 100); // 返回一个新的函数fn1,不会执行
fn1(); // window,100 箭头函数通过bind返回的函数无法修改其this指向
未绑定this,或绑定至null、undefined的情况
const fn = (a) => {
console.log(this, a);
}
const fn1 = fn.bind(); // 未绑定
const fn2 = fn.bind(null); // 绑定null
const fn3 = fn.bind(undefined); // 绑定undefined
fn1(); // 绑定到执行作用域,默认为window
fn2(); // 绑定到执行作用域,默认为window
fn3(); // 绑定到执行作用域,默认为window
call与apply方法
与bind不同,call和apply的主要作用是立即执行函数,同时解决执行过程中this指向的问题。
语法格式
function.call(thisArg, arg1, arg2, ...)
function.apply(thisArg, argsArray)
call的参数列表是可选的,若传入的thisArg为null或undefined,则自动替换为全局对象;若传入原始值,则替换为对应的包装类型。apply的用法与call类似,区别在于其额外传入的参数为数组或类数组对象,而call的额外参数为不确定数量的参数。
示例代码:
function fn(a, b) {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // {x: 100} 10 20
fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call和apply均无法修改箭头函数的this指向:
const fn = (a, b) => {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // Window 10 20
fn.apply({x: 100}, [10, 20]); // Window 10 20
简要回顾了bind、call、apply的基本用法后,接下来将深入探讨它们的底层实现原理。
bind方法的实现思路
基于前文对bind()的使用分析,在实现该功能时需考虑以下要素:
- 最终需返回一个新的函数,该函数可通过
function关键字进行声明。 - 必须能够绑定新函数的this指向。
- 需要处理运行时的参数,可通过调用apply或call来实现参数的传递。
参考实现代码:
// 通过原型链注册方法
// context:传递的上下文this;bindArgs表示需要绑定的额外参数
Function.prototype.newBind = function (context, ...bindArgs) {
const self = this; // 当前调用bind的函数对象
// 返回的函数本身也是可以再传入参数的
return function (...args) {
// 拼接参数
const newArgs = bindArgs.concat(args);
return self.apply(context, newArgs)
}
}
function fn(a,b) {
console.log(this, a, b);
}
const fn1 = fn.newBind({x: 100}, 10);
fn1(20); // {x: 100} 10 20
bind()返回的是一个新函数,执行该新函数等效于通过call或apply调用原函数,并传入指定的this和参数列表。
call与apply方法的实现
在实现bind时,我们使用了apply来完成this的绑定。那么,要实现apply时,应该采用何种方式绑定this呢?可能会有开发者发现,在apply中使用call,或在call中使用apply也能完成this的绑定。但这会导致方法嵌套,并非我们期望的最终实现方案。
我们先来
call和apply的应用场景:
- bind返回一个新的函数,并不会立即执行;而call和apply会立即执行函数。
- 核心功能之一是绑定this。
- 需要传入执行所需的参数。
示例代码:
function fn(a, b) {
console.log(this, a, b);
}
fn.call({x: 100}, 10, 20); // {x: 100} 10 20
fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call和apply的实现效果一致,都是立即执行函数,不同点在于call需要传入单个或者多个参数,apply可以传入一个参数数组。
如何在函数执行时绑定this:
- const obj = {x: 100, fn() {this.x}}
- 执行obj.fn(),此时fn()内部的this指向的就是obj
- 可以借此实现函数绑定this
使用过Vue的朋友都知道,Vue实例其实就是一个对象,其里面的方法在调用时,this就会指向当前对象。举个栗子:
let obj = {
key: 'key',
getKey: () => {
return this.key;
},
getKey2() {
return this.key;
}
};
obj.getKey(); // this指向window,返回值取决于window中是否有对应的属性
obj.getKey2(); // this指向obj,返回 'key'
这个例子在这些情况下不建议你使用箭头函数也是有提及的,感兴趣的朋友可以去看看。根据此原理,我们就可以来尝试给函数绑定this了:某函数调用apply,那么我们就将这个函数添加到传入的this对象中(如果未传入则this为全局对象,如果传入的是原始值,则使用其包装类型),然后使用()来执行函数,这个时候函数的this指向的就是我们传入的this了。
参考实现代码:
Function.prototype.newCall = function(context, ...args) {
if (context == null) context = globalThis; // 如果传入的上下文是null或者undefined,则使用全局globalThis,一般指向的就是window
if (typeof context !== 'object') context = new Object(context); // 如果是原始类型(数字、字符串、布尔值等),则使用其包装类型
const fnKey = Symbol(); // 使用Symbol可确保key值不会重复,避免属性覆盖
context[fnKey] = this; // this指向的是当前调用newCall的函数
console.log(context[fnKey]); // 打印当前函数以及上下文this
console.log(context);
const res = context[fnKey](...args); // 执行函数,函数的this指向为context
delete context[fnKey]; // 删除fn,防止污染
return res; // 返回结果
}
fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20
function fn(a,b) {
console.log(this, a, b);
}
这样我们就实现了call,那么apply实现类似,只不过传入的额外参数要变成数组或类数组的方式
Function.prototype.newCall = function(context, args) {
if (context == null) context = globalThis; // 如果传入的上下文是null或者undefined,则使用全局globalThis,一般指向的就是window
if (typeof context !== 'object') context = new Object(context); // 如果是原始类型(数字、字符串、布尔值等),则使用其包装类型
const fnKey = Symbol(); // 使用Symbol可确保key值不会重复,避免属性覆盖
context[fnKey] = this; // this指向的是当前调用newCall的函数
console.log(context[fnKey]); // 打印当前函数以及上下文this
console.log(context);
const res = context[fnKey](...args); // 执行函数,函数的this指向为context
delete context[fnKey]; // 删除fn,防止污染
return res; // 返回结果
}
fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20
function fn(a,b) {
console.log(this, a, b);
}
注意打印的当前函数以及上下文:

实现call和apply与bind有很大的不同就是如何来处理this绑定。
总结
掌握了如何实现bind、call和apply,对于理解如何使用,以及如何避免潜在的错误有很大的帮助。特别是call和apply,我们在实现的时候借助于对象内部的非箭头函数,其this指向对象自身这一基础知识,实现了this绑定。如果还未搞清楚的朋友,可以将代码运行起来看看,也许能帮助你更好的理解。
以上就是一文搞懂JavaScript中bind,apply,call的实现的详细内容,更多关于JavaScript bind apply call的资料请关注本站其它相关文章!