bind、call和apply的区别与联系
1、联系:
作用上:它们三个都可以绑定this指向,因为函数内部的this由运行的方式决定,this的指向常常会发生变化,这是就会产生绑定的需要。
使用上,三个函数的第一个参数都是this将要指向的对象,它们后面的参数(即除第一个参数外的其他参数位置)可以用来放被调用函数的参数
2、区别:
使用上,bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用执行 。基于此原因,在底层实现上,apply和call较为相似,所以我们今天就将它们的底层实现放在一起来探讨
实现上,apply和call的主要区别在传参方式上:call 传参是散开的,apply 传参则是数组形式的 [ ],
call和apply在传参方式上的区别是我们在底层实现上要关注的!!!
○ 代码展示call和apply传参方式上的区别:
func.call(this, arg1, arg2); // call的传参是散的
func.apply(this, [arg1, arg2]) // apply的传参则是数组形式的
手写一个call
js原生的call函数调用:
let name = null,
age = null; // 定义变量
let person = { // 定义对象
name: 'Youth',
age: 18
};
const fn = function(name, age) {}; // 定义call要调用的函数
fn.call(person, name, age); // call调用时第一个参数是作用对象,被指定的对象
手写之前的背景知识:
1、任何方法function都可以通过 fn.call() 来调用,
其中fn为某个函数,所以在实现上要具有普适性
2、 具有原型链思维:Function.prototype.call2 (call2是为了不与原生js的call发生冲突),有了原型链,我们可以在顶层设计完成call的实现
实现的思想:
让方法为指定的对象打工。
call的出现,让即使一个没有自身定义方法的对象也能够调用他人实现的方法,非常灵活;
在实现call的过程中,更形象的,我们把指定的对象可以当做老板,函数则作为一个散工,老板可以选择指定使用任何一个散工为其工作,并给其一些生产资料(相当于传的参数)去做事,实现某种功能。
正式实现:
1、定义测试的指定对象:老板
// 定义对象
let foo1 = { // 老板
value: 1
}
2、定义call函数,这是在建立老板与员工的联系,它们之间的联系在js中位于原型链上,主要体现在下面的Function.prototype.call2上(Function是一个构造函数,其实每一个函数都是它的实例,包括它自己)
// 函数本身也是一个对象,所以语法上允许Function.prototype
Function.prototype.call2 = function(context=window) { // 给参数赋默认值,这是es6的语法
// ○ 核心代码
context.fn = this;
// 将老板与员工联系起来,把员工(函数)的指向交给老板(指定对象)的一个属性
// .fn 是什么? 可以动态加属性,fn是老板content的一个属性
//这个this指向的是调用了该call2方法的对象(这里是函数,函数本身也是对象)
const args = [...arguments].slice(1); // 获取传入的其他参数
// 伪数组arguments,想要获取函数运行的参数:可以利用伪数组!
// ... 是将数组展开,属于es6语法
// slice(1) 是指从第二个元素开始截取后面的部分,因为call函数的第一个参数是指定对象,不需要
const result = context.fn(args);
// fn() 较难理解,它是属性,但其内容却是this,而this指向了员工(函数),不是全局window,所以它是可以执行的!
delete context.fn; // 在一个对象上删除某个属性,call完美结束!
// 以上的语句是为了去除我们的call在实现时对象绑定函数方式的副作用,让对象不受到污染!!!
return result; // 这是为了模仿函数具有返回值的特性,也可以不写
}
【小结】
1. 作为call函数来调用。要改变this的指向,逆向思维,改变函数的运行方式,让其利用f.call()简洁运行
2. 从打工者和老板的角度考虑call的绑定问题,打工者为函数,老板为指定对象
3. 要获取某个函数的this,可以将其作为对象,调用其他函数,其他函数中可以用到它的this
4. 伪数组获取个数未知参数。arguments是每个函数都带有的,同时我们可以通过 […arguements]将其展开
5. 消除call在 context.fn 实现时的副作用,它将传进来的对象context与函数fn绑定起来,污染了context对象,我们要通过 delete context.fn 删除该属性保证对象的纯洁性
3、定义员工:函数
function bar(name, age) { // 打工者
console.log(name);
console.log(age);
// this 指向全局? js语言的bug 对象的方法被调用时
// foo 调用方法bar,this指向由函数的调用方式来决定
console.log(this.value); // this不指向函数自己,而是其运行环境,
}
4、调用自定义的call方法:
let name = 'Youth',
age = 18;
bar.call2(bar, name, age) // call 调用方式
手写一个apply(与call基本类似)
apply在与call的实现上基本类似,区别在于传参方式上,这里我们仅看apply的实现方式和最后的调用方式:
实现方式:
Function.prototype.apply2 = function(context=window, args=[]) { // 给参数赋默认值,第二个参数传参是一个数组
// ○ 核心代码
context.fn = this; // 将老板与员工联系起来
const result = context.fn(...args); // fn ,函数本身也是对象,体现在1.prototype设置上,etc
// 将数组展开 :...args
delete context.fn; // 在一个对象上删除某个属性,call完就结束!
return result;
}
调用方式:
bar.apply2(bar, ['Luck', 18]) // call 调用方式,
小结
在手写call和apply时,要注意联系指定对象与函数的思想,即通过动态的给指定对象添加函数属性,以达到联系双方的目标,context.fn = this ,理解这一语句至关重要;
同时,还要理解 delete context.fn的意义,它是为了取消 对象.函数 的副作用,防止变量在使用call/apply后受到污染。