call / bind / apply
Function.prototype:
call:
[function].call([context],params1,params2,...)
[function]作为Function内置类的一个实例,可以基于__proto__找到Function.prototype的call方法,并且把找到的call方法执行;在call方法执行的时候,会把[function]执行,并且把函数中的THIS指向为[context],并且把params1,params2...等参数值分别传递给函数
call 的实现原理
核心原理:给CONTEXT设置一个属性(属性名尽可能保持唯一,避免我们自己设置的属性修改默认对象中的结构,例如可以基于Symbol实现,也可以创建一个时间戳名字),属性值一定是我们要执行的函数(也就是THIS,CALL中的THIS就是我们要操作的这个函数);接下来基于CONTEXT.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的THIS(还可以把PARAMS中的信息传递给这个函数即可);都处理完了,别忘记把给CONTEXT设置的这个属性删除掉(人家之前没有你自己加,加完了我们需要把它删了)
如果CONTEXT是基本类型值,默认是不能设置属性的,此时我们需要把这个基本类型值修改为它对应的引用类型值(也就是构造函数的结果)
Function.prototype.call = function call(context, ...params) {
//【非严格模式下】不传或者传递NULL/UNDEFINED都让THIS最后改变为WINDOW
context == undefined ? context = window : null;
// CONTEXT不能是基本数据类型值,如果传递是值类型,我们需要把其变为对应类的对象类型
if (!/^(object|function)$/.test(typeof context)) {
if (/^(symbol|bigint)$/.test(typeof context)) {
context = Object(context);
} else {
context = new context.constructor(context);
}
}
let key = Symbol('KEY'),
result;
context[key] = this;
result = context[key](...params);
delete context[key];
return result;
};
/* let obj = {
name: "obj"
};
function func(x, y) {
console.log(this);
return x + y;
}
console.log(func.call(obj, 10, 20)); */
// // 只要按照成员访问这种方式执行,就可以让FUNC中的THIS变为OBJ【前提OBJ中需要有FUNC这个属性】,当然属性名不一定是FUNC,只要属性值是这个函数即可
// obj.?xxx = func;
// obj.?xxx(10,20);
// 创建一个值的两种方法:对于引用数据类型来讲,两种方式没啥区别,但是对于值类型,字面量方式创建的是基本类型值,但是构造函数方式创造的是对象类型值;再但是,不管基本类型还是对象类型都是所属类的实例,都可以调用原型上的方法;(基本值无法给其设置属性,但是引用值是可以设置属性的)
// // 1.字面量创建
// let num1 = 10;
// let obj1 = {};
// new num1.constructor(num1);
// // 2.构造函数创建
// let num2 = new Number(10);
// let obj2 = new Object();
apply
[function].apply([context],[params1,params2,...])
和call作用一样,只不过传递给函数的参数需要一数组的形式传递给apply
bind
[function].bind([context],params1,params2,...)
语法上和call类似,但是作用和call/apply都不太一样;call/apply都是把当前函数立即执行,并且改变函数中的this指向的,而bind是一个预处理的思想,基于bind只是预先把函数中的this指向[context],把params这些参数值预先存储起来,但是此时函数并没有被执行
这三个方法都是用来改变函数中的THIS的
let body = document.body;
let obj = {
name: "obj"
};
function func(x, y) {
console.log(this, x, y);
}
1 call和apply的唯一区别在于传递参数的形式不一样
func(10, 20); //=>THIS:WINDOW
obj.func(); //=>Uncaught TypeError: obj.func is not a function
func.call(obj, 10, 20);
func.apply(obj, [10, 20]);
// call方法的第一个参数,如果不传递或者传递的是null/undefiend,在非严格模式下都是让this指向window(严格模式下传递的是谁,this就是谁,不传递this是undefined)
func.call();
func.call(null);
func.call(undefined);
func.call(11);
2 call和bind用法
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);
};
3 BIND 源码实现原理
执行BIND(BIND中的THIS是要操作的函数),返回一个匿名函数给事件绑定或者其它的内容,当事件触发的时候,首先执行的是匿名函数(此时匿名函数中的THIS和BIND中的THIS是没有关系的)
BIND的内部机制就是利用闭包(柯理化函数编程思想)预先把需要执行的函数以及改变的THIS再以及后续需要给函数传递的参数信息等都保存到不释放的上下文中,后续使用的时候直接拿来用,这就是经典的预先存储的思想
Function.prototype.bind = function bind(context = window, ...params) {
// this->func
let _this = this;
return function anonymous(...inners) {
_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);
};
setTimeout(func.bind(obj), 1000);
setTimeout(function anonymous() {
}, 1000);
call /bind /apply 的应用
call 的应用 :把类数组转换为数组
我不是某个类的实例,不能直接用它原型上的方法,但是我可以让某个类原型上的方法执行,让方法中的THIS(一般是需要处理的实例)变为我,这样就相当于我在借用这个方法实现具体的功能 -
这种借用规则,利用的就是call改变this实现的,也是面向对象的一种深层次应用
需求:需要把类数组转换为数组
类数组:具备和数组类似的结构(索引和LENGTH以及具备INTERATOR可迭代性),但是并不是数组的实例(不能用数组原型上的方法),我们把这样的结构称为类数组结构
function func() {
// 1.Array.from
let args = Array.from(arguments);
console.log(args); */
// 2.基于ES6的展开运算符
let args = [...arguments];
console.log(args);
// 3.手动循环
let args = [];
for (let i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
console.log(args);
// 4. ARGUMENTS具备和数组类似的结构,所以操作数组的一些代码(例如:循环)也同样适用于ARGUMENTS;如果我们让ARRAY原型上的内置方法执行,并且让方法中的THIS变为我们要操作的类数组,那么就相当于我们在“借用数组原型上的方法操作类数组”,让类数组也和数组一样可以调用这些方法实现具体的需求
let args = Array.prototype.slice.call(arguments);
let args = [].slice.call(arguments);
console.log(args);
[].forEach.call(arguments, item => {
console.log(item);
});
}
func(10, 20, 30, 40);
Array.prototype.slice = function slice() {
// this->arr
let args = [];
for (let i = 0; i < this.length; i++) {
args.push(this[i]);
}
return args;
};
let arr = [10, 20, 30];
console.log(arr.slice());
应用2 求数组最大值
需求:获取数组中的最大值
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(n1,n2,...)
let max = Math.max(...arr);
let max = Math.max.apply(Math, arr);
console.log(max);
面试题
function fn1(){console.log(1);}
function fn2(){console.log(2);}
fn1.call(fn2);
fn1.call.call(fn2);
Function.prototype.call(fn1);
Function.prototype.call.call(fn1);