一、什么是call、bind、apply
call、bind 和 apply 是函数对象上的方法,用于在调用函数时控制函数的执行上下文(即 this 值)和参数。
如果对this的指向有疑问的,可以去看JavaScript中的this解密!这篇文章。
二、call、bind、apply的使用和区别
我们可以使用apply()、call()和bind()等方法显式地绑定this关键字的值。这样我们可以手动控制this的值,强行改变函数的this指向,而不依赖于隐式绑定。
1.call
call
方法用于调用函数,并将指定的对象作为函数的上下文(this值),修改函数的this指向,接收零散的参数。call()不传参数或传null、undefined参数时都指向Window(全局对象)
。
语法: function.call(thisArg, arg1, arg2, ...)
。其中,thisArg是要绑定的对象,它将代替调用函数中的this关键字。后面的参数arg1、arg2等是可选的参数,它们被传递给调用的函数。
function sayHello() {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
}
let person = { name: "张三" };
sayHello(person);
sayHello.call();
sayHello.call(person);
注意: 这里以谷歌浏览器中的运行结果为准。
如上图所示,当没有使用call()或者使用call()但是不传入参数时函数中的this都指向全局变量Window
。使用call()并且传入参数person时,this指向person
对象。
2.bind
bind
修改函数的this指向,会返回一个新的函数,接收零散的参数。
语法:function.bind(thisArg, arg1, arg2, ...)
。这个方法与call方法的区别在于它会返回一个新的函数实例,这个新的函数实例可以稍后再调用。
function sayHello() {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
}
let person = { name: "张三" };
let newHello = sayHello.bind(person);
sayHello.bind(); //无输出
sayHello.bind(person); //无输出
newHello();
如图所示,sayHello.bind(person)执行后返回一个函数newHello,去调用newHello才会有输出结果。
3.apply
apply
修改函数的this指向,参数以数组形式接收。
语法:function.apply(thisArg, [argsArray])
。其中,thisArg是要绑定的对象,argsArray是一个数组类型的参数列表,它们被传递给调用的函数。
function sayHello() {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
}
let person = { name: "张三" };
sayHello.apply();
sayHello.apply(person);
call和apply方法在函数调用时立即执行,而bind方法返回一个新的函数,需要手动调用。apply的参数需要以数组形式传入。
function foo(x, y, z) {
console.log(this.num, x + y + z);
}
var obj = {
num: 666
}
foo.call(obj, 1,2,3); //输出:666 6
foo.apply(obj, [1,2,3]);//输出:666 6
var foo2 = foo.bind(obj, 1,2,3);
foo2(); //输出:666 6
三、手写call()方法
实现思路:为了让所有函数都可以调用这个方法,方法应该添加到 Function 的原型上。创建一个对象新属性,将函数挂在对象的新属性上,改变this的指向,调用函数,删除新属性,返回函数执行结果(没有执行结果返回undefined)。
// 自定义简陋版call方法实现
Function.prototype.myCall = function (context, ...args) {
// 判断调用对象是不是函数
if(typeof this !== "function"){
throw new TypeError('error');
}
// 如果没有传入上下文对象,则默认为全局对象 window
context = context || window;
// 创建一个唯一的键名,防止名字冲突
const fn = Symbol("fn");
// this是调用myCall的函数,将函数绑定到上下文对象的新属性上
context[fn] = this;
// 调用绑定的函数,并传入参数
const result = context[fn](...args);
// 删除绑定的函数,使对象保持原样
delete context[fn];
// 返回函数执行的结果
return result;
};
测试数据如下。注意:当对象参数person不写时,还有bug,本人较菜,暂未解决。
//测试
function sayHello(x, y, z) {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
console.log(x + y + z);
}
let person = { name: "张三" };
// 这里省略myCall函数的定义
sayHello.myCall(person,1,2,3);
// 函数中的this: {name: '张三', Symbol(fn): ƒ}
// 你好, 张三
// 6
通过一个小例子解释下...args
的含义。
let [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); //1
console.log(tail); //[ 2, 3, 4, 5 ]
function test(...args) {
console.log(args); //[ 1, 2, 3 ]
console.log(...args); //1 2 3
}
test(1,2,3);
let [head, ...tail] = [1, 2, 3, 4, 5]
这行代码中使用了数组解构赋值
语法将数组中的第一个元素赋值给 head 变量,剩余的元素赋值给 tail 变量。
function test(...args)
函数使用了rest
参数语法,它允许我们在函数中传入任意个数
的参数,并将它们都封装在一个数组中作为 args 参数来使用。
console.log(...args)
这行代码使用了扩展运算符
,它可以将数组 args 中的所有元素展开为单独的参数。
四、手写apply()方法
实现思路:和call的几乎一样,就是apply的传入参数是以数组形式传入的。
// 自定义简陋版apply方法实现
Function.prototype.myApply = function (context, args) {
// 判断调用对象是不是函数
if(typeof this !== "function"){
throw new TypeError('error');
}
// 如果没有传入上下文对象,则默认为全局对象 window
context = context || window;
// 创建一个唯一的键名,防止名字冲突
const fn = Symbol("fn");
// this是调用myCall的函数,将函数绑定到上下文对象的新属性上
context[fn] = this;
// 调用绑定的函数,并传入参数数组
const result = context[fn](...args);
// 删除绑定的函数,使对象保持原样
delete context[fn];
// 返回函数执行的结果
return result;
};
//测试
function sayHello(x, y, z) {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
console.log(x + y + z);
}
let person = { name: "张三" };
// 这里省略myApply函数的定义
sayHello.myApply(person,[1,2,3]);
// 函数中的this: {name: '张三', Symbol(fn): ƒ}
// 你好, 张三
// 6
五、手写bind()方法
function foo(x, y, z) {
this.name = "张三";
console.log(this.num, x + y + z);
}
var obj = {
num: 666
}
var foo2 = foo.bind(obj, 1,2,3);
console.log(new foo2());
// undefined 6
// foo { name: '张三' }
如上述代码示例所示,如果我们去new
foo2函数,结果会得到undefined 6
和foo { name: '张三' }
,相当于去new了foo函数,我们在自己实现bind时要参考官方bind的这个效果。
实现思路:bind执行后会返回一个新函数,那么我们怎么判断这个新函数是被直接调用的还是被new了呢?(实例对象 instanceof 构造函数)一定是true,也就是(this instanceof 构造函数)是true。
// 自定义简陋版bind方法实现
Function.prototype.myBind = function(context, ...args) {
// 判断调用对象是不是函数
if(typeof this !== "function"){
throw new TypeError('error');
}
// 如果没有传入上下文对象,则默认为全局对象window
context = context || window
// 保存原始函数的引用,this就是要绑定的函数
const _this = this
// 返回一个新的函数作为绑定函数
return function fn(...innerArgs) {
// 判断返回出去的函数有没有被new
if(this instanceof fn){
return new _this(...args, ...innerArgs);
}
// 使用 apply 方法将原函数绑定到指定的上下文对象上
return _this.apply(context, [...args, ...innerArgs]);
};
};
//测试
function sayHello(x, y, z) {
console.log("函数中的this: ", this);
console.log("你好, " + this.name);
console.log(x + y + z);
}
let person = { name: "张三" };
let bind1 = sayHello.myBind(person, 1,2,3);
console.log(bind1());
console.log(new bind1());
// 函数中的this: {name: '张三'}
// 你好, 张三
// 6
// undefined
// 函数中的this: sayHello {}
// 你好, undefined
// 6
// sayHello {}
六、最后的话
在JavaScript中,call、bind 和 apply 方法是非常有用的工具,我们可以使用call、bind、apply方法来改变函数执行中的this指向,帮助我们更好地控制函数的执行上下文。在实际开发中,这些方法都有各自的应用场景,需要根据具体情况选择使用。
能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝大家生活愉快!
原文链接:https://juejin.cn/post/7245567988250099749 作者:牛哥说我不优雅