CALL和APPLY以及BIND语法(含BIND的核心原理)

177 阅读5分钟

在Function.prototype中有这么几个方法:

call:[function].call([context],params1,params2,...)

[function]作为Function内置类的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;在call方法执行的时候,会把[function]执行,并且把函数中的THIS指向为[context],并且把params1,params2...等参数值分别传递给函数

例1:
    let obj = {
        name: "obj"
    };
     
    function func(x, y) {
        console.log(this, x, y);
    }
    // func(10, 20); //=>THIS:WINDOW,10,20
    // obj.func(); //=>Uncaught TypeError: obj.func is not a function

如何让func里面的this指向obj呢?

    func.call(obj, 10, 20);//利用call把func指向,把this指向obj,并且把参数传给func

call方法的第一个参数,如果不传递或者传递的是null/undefiend,在非严格模式下都是让this指向window(严格模式下传递的是谁,this就是谁,不传递this是undefined)

func.call();
func.call(null);
func.call(undefined);
func.call(11);//这里this指向的是实例Number{11};

call和apply的唯一区别在于传递参数的形式不一样

apply:[function].apply([context],[params1,params2,...])

和call作用一样,只不过传递给函数的参数需要一数组的形式传递给apply

func.apply(obj, [10, 20]);

bind:[function].bind([context],params1,params2,...)

语法上和call类似,但是作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向[context],把params这些参数值预先存储起来,但是此时函数并没有被执行

let body = document.body;
let obj = {
    name: "obj"
};
 
function func(x, y) {
    console.log(this, x, y);
}
body.onclick = func; //=>把func函数本身绑定给body的click事件行为,此时func并没有执行,只有触发body的click事件,我们的方法才会执行
body.onclick = func(10, 20); //=>先把func执行,把方法执行的返回结果作为值绑定给body的click事件
 
// 需求:把func函数绑定给body的click事件,要求当触发body的点击行为后,执行func,但是此时需要让func中的this变为obj,并且给func传递10,20

body.onclick = func.call(obj, 10, 20); //=>这样不行,因为还没点击func就已经执行了
body.onclick = func.bind(obj, 10, 20);
 
// 在没有bind的情况下我们可以这样处理(bind不兼容IE6~8)
body.onclick = function anonymous() {
    func.call(obj, 10, 20);
};

执行BIND(BIND中的THIS是要操作的函数),返回一个匿名函数给事件绑定或者其它的内容,当事件触发的时候,首先执行的是匿名函数(此时匿名函数中的THIS和BIND中的THIS是没有关系的)

BIND的内部机制就是利用闭包(柯理化函数编程思想)预先把需要执行的函数以及改变的THIS再以及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接拿来用,这就是经典的预先存储的思想

为了更好的理解bind,我们手写一个:

Function.prototype.bind = function bind(context = window, ...params) {
    //this->func
    let _this = this;
    return function anonymous(...inners) {
    //这时bind已经把匿名函数返回了,那body再执行,this就是body了,所以我们在外层把this获取一下
        // _this.call(context, ...params);
        _this.apply(context, params.concat(inners));
    };
};
假如我们需要给匿名函数传参数
body.onclick = func.bind(obj, 10, 20);
body.onclick = function anonymous(ev) { //=>ev事件对象 
     func.call(obj,10,20,ev);
};
也可以用定时器来看,当1s后执行的匿名函数,此时this谁执行就是谁
setTimeout(func.bind(obj), 1000);
// setTimeout(function anonymous() {
// 
// }, 1000);

现在我们来做两个案例练习一下:

需求:需要把类数组转换为数组

类数组:具备和数组类似的结构(索引和LENGTH以及具备INTERATOR可迭代性),但是并不是数组的实例(不能用数组原型上的方法),我们把这样的结构称为类数组结构

例2:
    function func(){
        console.log(arguments)
    }
    func(10, 20, 30, 40);
    //此时打印出来的并不是数组,它具有跟数组的索引和LENGTH以及具备INTERATOR可迭代性,
    //但是原型链指向的是Object,而并不是Array
    第一种:Array.from
    function func(){
        let arr = Array.from(arguments);
        console.log(arr); //[10,20,30,40]
    }
    第二种:基于ES6的展开运算符
    let arr = [...arguments];
    
    第三种:手动循环
    let arr = [];
    for(let i = 0; i < arguments.length; i++){
        arr.push(arguments[i]);
    }
    
    第四种:ARGUMENTS具备和数组类似的结构,所以操作数组的一些代码(例如:循环)也同样适用于ARGUMENTS;如果我们让ARRAY原型上的内置方法执行,并且让方法中的THIS变为我们要操作的类数组,那么就相当于我们在“借用数组原型上的方法操作类数组”,让类数组也和数组一样可以调用这些方法实现具体的需求
    
     Array.prototype.slice = function slice() {
        // this->arr
        let args = [];
        for (let i = 0; i < this.length; i++) {
            args.push(this[i]);
        }
        return args;
    };
    //数组的slice如果不传参,相当于是重新克隆一份
    let arr = [10, 20, 30];
    console.log(arr.slice());
    //那此时,数组的slice方法,与我们的手动循环是一样的,只是this变成了arguments,所以我们只需把this指向arguments即可
    
    let args = Array.prototype.slice.call(arguments);
    let args = [].slice.call(arguments);
    console.log(args); //[10,20,30,40]

我们可以用类似的方法做很多事情:

    [].forEach.call(arguments, item => {
        console.log(item);
    });

我们再做一个例子:

需求:获取数组中的最大值

let arr = [12, 13, 2, 45, 26, 34];

//第一种:排序
let max = arr.sort((a, b) => b - a)[0];
console.log(max);

//第二种:遍历 
let max = arr[0];
arr.forEach(item => {
    if (item > max) {
        max = item;
    }
});
console.log(max);
 
第三种:Math.max.apply
Math.max(n1,n2,...)//该方法必须是把单独项传一个一个传进来,才会筛选出最大值
let max = Math.max(...arr);
//那我们用apply把一个数组传进来当做第二个参数,
//第一个参数把Math自己传进去(既然不改变this,那就把自己传进来)
let max = Math.max.apply(Math, arr);
console.log(max);