【手写JS】要想面试结果好,call、bind、apply手写少不了

一、什么是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);

【手写JS】要想面试结果好,call、bind、apply手写少不了
注意: 这里以谷歌浏览器中的运行结果为准。

如上图所示,当没有使用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();

【手写JS】要想面试结果好,call、bind、apply手写少不了

如图所示,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);

【手写JS】要想面试结果好,call、bind、apply手写少不了

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: '张三' }

如上述代码示例所示,如果我们去newfoo2函数,结果会得到undefined 6foo { 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 作者:牛哥说我不优雅

(0)
上一篇 2023年6月18日 上午10:21
下一篇 2023年6月18日 上午10:31

相关推荐

发表回复

登录后才能评论