JavaScript之call、apply和bind的手动模拟实现

本文详细介绍了JavaScript中Function.prototype.call()、apply()和bind()方法的实现原理,包括改变上下文、传递参数、null和undefined处理以及构造函数模拟。通过实例演示和代码实现,帮助理解这些核心函数的作用和使用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. call的实现

参考资料:js 实现call和apply方法,超详细思路分析 - 听风是风 - 博客园 (cnblogs.com)

MDN的解释:Function.prototype.call() - JavaScript | MDN (mozilla.org)

function.call(thisArg, arg1, arg2, ...)

thisArg:

可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

1.1 设计思路

我们上看看call实现的一般效果。

var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
const obj = {
    foo: "fujiaxu",
};
function fn(){
    console.log(this.foo);
}
fn();// fjx
fn.call(obj);// fujiaxu

可以看到,call()可以给函数指定一个执行的上下文环境,用来改变该函数this的指向值(当然对于箭头函数来说是没有用的)。其次,它还可以传递参数,形式是散列传递。

那么我们如何手动模拟来实现call()呢?我们把代码换一个格式:

const obj = {
    foo: "fujiaxu",
    fn:function(){
        console.log(this.foo);
    }
};
obj.fn();// fujaixu

我们把函数放在对象里面,当做它一个方法,通过这样的方式,我们就可以达到call的效果了。那么我们开始动手来手动模拟实现call吧。

1.2 实现改变上下文

在JavaScript中call()方法是在Function.prototype.call()中,所以我们定义函数的时候,也必须放在同样的位置,这样就可以使得所有的函数都可以调用我们自己编写的call()方法。

var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(){
    console.log(this.foo);
}
const obj = {
    foo: "fujiaxu",
};
Function.prototype.call_ = function(context){
    context.fn = this;// 指向调用的函数
    context.fn();
    delete context.fn;
}
fn.call_(obj);// fujiaxu

这样,我们就是实现的改变上下文的操作。

1.3 传递参数

一种简单的方法就是使用ES6的剩余参数和扩展参数的方法,代码如下:

var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
    console.log(this.foo);
    console.log(a,b,c);
}
const obj = {
    foo: "fujiaxu",
};
Function.prototype.call_ = function(context,...args){
    context.fn = this;// 指向调用的函数
    context.fn(...args);
    delete context.fn;
}
fn.call_(obj,1,2,3);

没错,就是这么简单,但是面试官可不会放过我们。所以我们还是要学习不适用es6的方法。那就是使用arguments。从下标1开始,都是我们传入的参数,我们应该要将1 ~ length-1的数据提取出来,可惜的是arguments只不过是伪数组,没有数组的方法,当然也不能使用Array.prototype.slice.call(arguments),我们可是在模拟call啊,只能老老实实的使用循环来转换。另外一个问题,怎么把数据传递给函数呢?总不能直接传递数组啊,所以我们要将它转换为字符串。

数组转换为字符串[1,2,3] => "1,2,3",在加上eval()就可以实现了(为什么要使用eval,而不是直接传递转换好的字符串给函数呢?这是因为这个字符串知识相当于一个变量,而不是多个变量,所以要使用eval来传递多个参数)。当然还有一个问题,若["a","b","c"]=> "a,b,c",这在eval函数中是eval(“fn(a,b,c)”),这样话,肯定会报错的,我么在这里的需要的是fn(“a”,“b”,“c”),所以在循环压入数组的时候,压入的是"arguments[" + i + "]"),而不是里面的数据,让eval直接读取arguments的数据。

代码如下:

var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
    console.log(this.foo);
    console.log(a,b,c);
}
const obj = {
    foo: "fujiaxu",
};
Function.prototype.call_ = function(context){
    context.fn = this;// 指向调用的函数
    let arr = [];
    for(let i=1;i<arguments.length;i++){
        arr.push("arguments["+i+"]");
    }
    eval("context.fn("+arr+")");
    delete context.fn;
}
fn.call_(obj,"aw",2,3);

1.4 null和undefined处理,返回值的处理

对于函数返回值的处理,很简单,直接在最后返回就好了。

在非严格模式下,null和undefined 都指向的是window。

同时考虑到,传入的context非对象的情况下。

image-20220103211312488

    'use strict'
    var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
    function fn(a,b,c){
      console.log(this.foo);
      console.log(a,b,c);
      return "hahah";
    }
    const obj = {
      foo: "fujiaxu",
    };
    Function.prototype.call_ = function(context){
      /* 判断是不是严格模式 */
      let is = function(){
        return this;
      }()
      // 对于严格模式的处理
      if(is === window || (context !== undefined && context !== null)){
        context = new Object(context) || window;
      }else {
        context = undefined;
      }
      console.log(context);
      context.fn = this;// 指向调用的函数
      let arr = [];
      for(let i=1;i<arguments.length;i++){
        arr.push("arguments["+i+"]");
      }
      let res = eval("context.fn("+arr+")");
      delete context.fn;
      return res;
    }
    fn.call_(obj,"aw",2,3);

以上就是call()方法的实现过程。

2. apply的实现

在实现了call()的基础上,只需要吧传递的参数改为数组就好,直接看代码:

'use strict'
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
    console.log(this.foo);
    console.log(a,b,c);
    return "hahah";
}
const obj = {
    foo: "fujiaxu",
};
Function.prototype.apply_ = function(context,array){
    /* 判断是不是严格模式 */
    let is = function(){
        return this;
    }()

    if(is === window || (context !== undefined && context !== null)){
        context = new Object(context) || window;
    }else {
        context = undefined;
    }
    let res;
    let arr = [];
    context.fn = this;// 指向调用的函数
    if(!array){// 这里不能用长度判断,因为,不知道是否传入有参数,那么可能没有length这个属性
        res = context.fn();
    }else{
        for(let i=0;i<array.length;i++){
            arr.push("array["+i+"]");
        }
        res= eval("context.fn("+arr+")");
    }
    delete context.fn;
    return res;
}
fn.apply_(obj,["aw",2,3]);

3. bind() 的实现

参考资料:

Function.prototype.bind() - JavaScript | MDN (mozilla.org)

js 手动实现bind方法,超详细思路分析! - 听风是风 - 博客园 (cnblogs.com)

JavaScript深入之bind的模拟实现 · Issue #12 · mqyqingfeng/Blog (github.com)

首先看一下bind()有哪些特点

  1. 返回一个新的函数(this的值改变),且这个函数的this值不会再被改变(闭包的原理)
  2. 函数柯里化,也就是说可以先传递一部分参数
  3. 绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。并且会继承prototype上的值

3.1 改变this并返回函数

bind的调用与call()、apply()不同,它不是立即执行,而是返回一个函数。

所以我们可以使用apply()来实现。

var age = 1;// 必须使用var,否则let无法挂载在window
var foo = {
    age:2
}
var bar = {
    age:3
}
function fn() {
    console.log(this.age);
}
fn();//1
Function.prototype.bind_ = function (contenxt){
    let fn = this; // 保存函数
    return function(){
        fn.apply(contenxt);
    }
}
let fnn = fn.bind_(foo);
fnn();// 2

3.2 传参的实现

bind()支持函数柯里化,所以要使用闭包来传递一部分参数,然后剩余的参数以后传递。

var age = 1;// 必须使用var,否则let无法挂载在window
var foo = {
    age:2
}
var bar = {
    age:3
}
function fn(a,b) {
    console.log(this.age);
    console.log(a,b);
}
fn();
Function.prototype.bind_ = function (contenxt){
    let fn = this; // 保存函数
    let args1 = [...arguments].splice(1);// 当然也可已使用Array.prototype.slice.call(arguments, 1);
    return function(){
        /* 不能在这里保存函数,这里的this会变成window */
        let args2 = [...arguments];
        fn.apply(contenxt,[...args1,...args2]);// 或者 args1.concat(args2);
    }
}
let fnn = fn.bind_(foo,1);
fnn(2); // 1, 2  而且这里的this是不会指向window的,看看代码就知道了使用了闭包了

至此,已近实现了两个特性。还剩下最后一个bind出的函数可以作为构造函数使用。

而且从这里我们也可以看出,一旦使用bind变了this的值,那么将不会再改变。所以fnn(2)是没有指向window的。那么这是为什么呢?因为在调用fnn(2)的时候,它其实是在之前已经定义好的contenxt,这里就是用了闭包,所以this还不回被改变。

同理,若使用新的函数再去bind()一次,也是不会改变this的值的。那这又是为什么?请仔细看看代码,我们这里使用的apply()实现的bind(),也就是说,我们其实是在给定的context里面调用fn函数,但是在第一次bind中context已经确定了,所以不会再被改变了(同样是闭包的原理)。

3.3 构造函数效果的模拟实现

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。

那么这是什么意思呢?

var z = 1;
function foo(x,y) {
    this.age = 12;
    console.log(this.z);
    console.log(x,y);
}
foo.prototype.hh = "hh";
foo(2,3);
let fooo = foo.bind(z);
let person = new fooo(2,3);
console.log(person.hh);

结果为:image-20220105181009497

可以看到,这里的z居然是undefined,那么就说明绑定的this值失效了,而且person继承了foo的原型。

这就是我们需要实现的效果,所以在apply()的时候就需要判断这时的this是否为对象实例的this

var z = 1;
function foo(x, y) {
    this.age = 12;
    console.log(this.z);
    console.log(x, y);
}
foo.prototype.hh = "hh";
foo(2, 3);
Function.prototype.bind_ = function (contenxt) {
    let fn = this; // 保存函数
    let args1 = [...arguments].splice(1); // 当然也可已使用Array.prototype.slice.call(arguments, 1);
    let bound =  function () {
        /* 不能在这里保存函数,这里的this会变成window */
        let args2 = [...arguments];

        fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]); // 或者 args1.concat(args2);
    };
    bound.prototype = fn.prototype;
    return bound;
};
let fooo = foo.bind_(z);
let person = new fooo(2, 3);
console.log(person.hh);

这里我们采用的instanceof来判断是否是使用的new。

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

原理:

function myInstanceof(left, right) {
    // 这里先用typeof来判断基础数据类型,如果是,直接返回false
    if(typeof left !== 'object' || left === null) return false;
    // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {                  
        if(proto === null) return false;
        if(proto === right.prototype) return true;//找到相同原型对象,返回true
        proto = Object.getPrototypeof(proto);
    }
}

bound.prototype = fn.prototype;这句代码继承了原函数的的属性和方法,并且可以让instanceof来判断。所以this instanceof fn也是可以的。

但是我们还存在一个问题,那就是当新的函数修改自己的prototype时,会把之前的prototype也修改了,所以我们需要做一些处理。

可以使用一个空函数的实例,当然,也可以直接使用当前函数的实例。

var z = 1;
function foo(x, y) {
    this.age = 12;
    console.log(this.z);
    console.log(x, y);
}
foo.prototype.hh = "hh";
foo(2, 3);
Function.prototype.bind_ = function (contenxt) {
    let fn = this; // 保存函数
    let args1 = [...arguments].splice(1); // 当然也可已使用Array.prototype.slice.call(arguments, 1);
    let fNOP = function(){};
    let bound =  function () {
        /* 不能在这里保存函数,这里的this会变成window */
        let args2 = [...arguments];

        fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]); // 或者 args1.concat(args2);
    };
    fNOP.prototype = fn.prototype;
    bound.prototype = new fNOP();
    return bound;
};
let fooo = foo.bind_(z);
let person = new fooo(2, 3);
console.log(person.hh);

这样就不会影响到原函数的prototype了。

最后在做一些处理:

  1. 原函数有返回值
  2. 不是函数调用

3.4 最终代码

Function.prototype.bind_ = function (contenxt) {
    if (typeof this !== "function") {
        throw new Error(
            "Function.prototype.bind - what is trying to be bound is not callable"
        );
    }
    let fn = this; 
    let args1 = [...arguments].splice(1);
    let fNOP = function () {};
    let bound = function () {
        let args2 = [...arguments];
        return fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]);
    };
    fNOP.prototype = fn.prototype;
    bound.prototype = new fNOP();
    return bound;
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值