一波带走!从认识到手写call、bind、apply!

我心飞翔 分类:javascript

前言

this关键字想想就让人头痛,各种场合下它的指向不尽相同,而在js中,call、bind、apply这些函数原型方法又和this指向有密切关系。好吧,人已经麻了。不过这些js基础知识还是很重要的,让我们慢慢理一理吧。

概述

先来看看MDN对这三大函数原型方法解释

call()和apply()

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
apply() 方法和call()方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组

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

function.apply(thisArg, [argsArray])

  • function的this会指向thisArg对象
  • 非严格模式下:thisArg指定为null,undefined时,function中的this则会指向window对象

返回值:返回function的执行结果。若没有返回值,则返回 undefined

bind()

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

function.bind(thisArg, param1, param2, ...)

  • 调用绑定函数时作为 this 参数传递给目标函数的值
  • 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。

返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

栗子

看完上面的解释是不是有点晕,不急,来个栗子就懂了。

call()调用构造函数实现继承

function Product(name, price) {
    this.name = name;
    this.price = price;
}

function Food(name, price) {
    console.log(this);//Food {}
    Product.call(this, name, price);
    console.log(this);//Food { name: 'cheese', price: 5 }
    this.category = 'food';
    console.log(this);//Food { name: 'cheese', price: 5, category: 'food' }
}
const chess = new Food('cheese', 5)
console.log(chess);// Food { name: 'cheese', price: 5, category: 'food' }
 

结合MDN的解释细细分析。看第一个输出结果发现,初始时this指向Food()毛的问题,然后调用call()后第二个this就指向了Food { name: 'cheese', price: 5 },咋回事呢?再看一眼解释call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数,也就是说Product.call(this, name, price) 使用了Food()的this值调用了Product(),换而言之就是在Food()上运行Product()的所有初始化代码,让Food()也具有了name和price属性。

call()参数为空

在非严格模式下,如果没有传递第一个参数,this 的值将会被绑定为全局对象。

var name = 'Bob';
function sayHello() {
  console.log('Hello ', this.name);
}
sayHello.call();  // Hello Bob
 

但如果是严格模式则是Hello undefined

apply()和数组

apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。

  • 使用apply 将数组各项添加到另一个数组
var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]
 

apply()和new方法的实现

上篇文章 啊4000字!面试理不透的原型知识汇总?还有拓展! 介绍了new关键字的手写实现,就是通过apply()方法实现的。

function myNew(obj, ...args) {
    //Object.create()方法创建一个新对象,
    // 使用现有的对象来提供新创建的对象的__proto__。
    //构造obj.prototype 的原型  NewObj
    const NewObj = Object.create(obj.prototype); 
    console.log(NewObj); 
    // 新对象的this绑定到构造函数中this
    const result = obj.apply(NewObj, args);  
    console.log(result); 
    // 若构造函数返回 非空对象 ,则返回该对象, 否则返回刚刚创建的对象
    return (typeof result === 'object' && result !== null) ? result : NewObj;
}
 

bind()

var a = {
    b: function () {
        console.log(this); //{ b: [Function: b], c: 'hello' }
        var func = function () {
            console.log(this.c,'----'); // hello
        };
        func.bind(this)(); // 绑定this
    },
    c: 'hello'
}
a.b(); 
console.log(a.c); // hello
 

如果没有func.bind(this)(),那么a.b()输出结果是undefined,此时默认this指向全局,所以通过bind()把a的this绑定到func上则会输出Hello。

小结

由上面的栗子可以发现,call、bind、apply都是借助某个方法来实现改变this的指向,然后就不用再次添加方法了。看起来都差不多,实际上还是有区别的。

  • call与apply的唯一区别就是参数。apply 使用参数数组而call使用一组参数列表。
  • call/apply和bind的区别就是执行结果和返回值。call/apply改变了函数的this后执行该函数并返回执行结果,bind改变了函数的this后不执行并返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

手写实现

根据以上的了解总结一下call、bind、apply的实现思路。

call()

call() 方法使用一个this和一个或多个参数调用函数并返回执行结果。

  • 改变this并执行方法返回
  • 传参 (函数有一个arguments属性,代指函数接收的所有参数,它是一个类数组)
Function.prototype.my_call = function (obj) {
     //判断是否为null或者undefined,同时考虑传递参数不是对象情况
     obj = obj ? Object(obj) : window;
     let args = [];
     // 注意i从1开始
     for (let i = 1, len = arguments.length; i < len; i++) {
         args.push("arguments[" + i + "]");
     };
     obj.fn = this; // 此时this就是函数fn
     let result = eval("obj.fn(" + args + ")"); // 执行fn
     delete obj.fn; //删除fn
     return result
};
 

ES6全新写法:

Function.prototype.my_call = function (obj) {
    obj = obj ? Object(obj) : window;
    obj.fn = this;
    // 利用拓展运算符直接将arguments转为数组
    let args = [...arguments].slice(1);
    let result = obj.fn(...args);

    delete obj.fn
    return result;
};
 

apply()

理解了call() ,apply也是一样的,只是接收的参数是数组,直接上代码。

Function.prototype.my_apply = function (obj, arr) {
    obj = obj ? Object(obj) : window;
    obj.fn = this;
    var result;
    if (!arr) {
        result = obj.fn();
    } else {
        var args = [];
        // 注意这里的i从0开始
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push("arr[" + i + "]");
        };
        result = eval("obj.fn(" + args + ")"); // 执行fn
    };
    delete obj.fn; //删除fn
    return result;
};
 

ES6 全新写法:

Function.prototype.my_apply = function (obj, arr) {
    obj = obj ? Object(obj) : window;
    obj.fn = this;
    let result;
    if (!arr) {
        result = obj.fn();
    } else {
        result = obj.fn(...arr);
    };

    delete obj.fn
    return result;
};
 

bind()

bind()并不是立即执行,而是返回一个新函数,且新函数的this无法再次被修改。

Function.prototype.my_bind = function (obj) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    };
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    //创建中介函数
    var fn_ = function () {};
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //通过constructor判断调用方式,为true this指向实例,否则为obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
        console.log(this);
    };
    fn_.prototype = fn.prototype;
    bound.prototype = new fn_();
    return bound;
};
 

总结

文章就写到这里了,这也是看了很多大佬的分享进行整合理解总结的,程序员应当知其然且知其所以然,手写实现是好难,不过弄懂了就会有满满的成就感,在下也是前端小白,关于这方面的知识可能还有理解不到位的地方,希望各位多多担待并辛苦在下方评论区指出!由于我的文章写的不清楚没看懂的小伙伴也欢迎一起交流,也可以查看下方大佬的参考链接,大佬们真的太强了!!

  • 参考资料

MDN

js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]

js 实现call和apply方法,超详细思路分析

js 手动实现bind方法,超详细思路分析!

回复

我来回复
  • 暂无回复内容