这三个都是函数原型中的方法,用来改变某一个函数中 this
关键字指向的。
1. call
语法:[fn].call([this], [param],...)
- 第一个参数都是改变
this
指向的对象 - 后面的参数是传入该函数中的所有参数
f1.call(obj, a, b, c...); //=> a, b, c... 是传入的参数
当 call
中不传值,或者传入 null
或 undefined
,在非严格模式下,this
会被改为 window
。在严格模式下,传入什么就是什么,不传就是 undefined
区分:
fn.call
:当前实例(函数 fn)通过原型链的查找机制,找到Function.prototype
上的call
方法fn.call()
:把找到的call
方法执行
当 call
方法执行的时候,内部处理了一些事情:
- 首先将要操作函数中的
this
变为传入的第一个实参值 - 把第二个即以后的实参获取到
- 把要操作的函数执行,并把第二个以后传入的实参传给函数
理解 call
:
Function.prototype.call = function() {
let param1 = arguments[0],
paramOther = []; //=> 获取剩余的参数
//=> this:fn 当前要操作的实例
//=> 把 fn 中的 this 关键字修改为 param1,把 this 中的 this 关键字修改为 param1
//=> 把 fn 执行,把 paramOther 传入 fn
//=> this(paramOther)
if (obj === undefined || obj === null) {
this();
}else{
this.this = obj;
this(paramOther);
}
};
//=> ES6
Function.prototype.call = function (...arg) {
var obj = arg.shift();
if(obj===undefined || obj===null){
this(...arg);
}else{
this.this = obj;
this(...arg);
}
};
思考:怎么通过 call
让 this
执行?
function f1() {
console.log(1);
}
function f2() {
console.log(2);
}
f2.call();
//=> call 执行时,call 里面的 this 本身就指向 f2
//=> 让 this 执行,也就是让 f2 执行
f2.call(f1); //=>2 执行的是 f2
//=> 让 call 中的 this(f2) 中的 this 改成 f1,然后让 call 中的 this 执行,就是让 f2 执行
f2.call.call(f1);
//=> 执行的是第二个 call,把 f2.call 中的 this(本来是 f2) 指向 f1,然后再执行 this,也就是执行 f2.call
//=> 执行 f2.call,没有传参,就只是执行 this,而这个 this 在前面已经改为了 f1,所以执行 f1
f2.call.call.call(f1);
//=> 后面再多的 call 都已经没有意义,因为倒数第二个 call 中的 this(本来是f2.call) 指向了 f1,那么就是执行 f1,完全不用管前面有多少 call
2. apply
apply
和 call
基本上一模一样,唯一区别在于传参方式。
f2.apply(obj,xxx); //=> xxx 是一个数组或者类数组,是传入的参数组成的
apply
把需要传递的参数放到一个数组(或类数组)中传递进去,虽然写的是数组,但是也是相当于一个个传递给前面的函数。
3. bind
bind
语法和 call
一模一样,唯一的区别在于立即执行还是等待执行。
var f2 = f1.bind(obj,1,2,3);
f2(); //=> 里面的 this 绑定为了 obj
bind
改变前面函数中的this
,此时函数并没有执行,而是作为返回值赋给一个变量,在需要的时候执行。- 返回的函数执行时,再传参数,会跟
bind
传的参数(除了第一个参数)结合起来 bind
返回的函数与原先的函数不再是同一个函数fn.bind(this) === fn //=> false
bind
不兼容 IE6~8
理解 bind
:
//=> 执行 bind 形成一个不销毁的作用域,返回一个新的匿名函数(每一次返回的都不一样:不是相同的堆内存)
// [存储的内容]
// THIS: FN(当前要处理的函数)
// CONTEXT: OBJ(需要把 FN 中的 THIS 改变为这个对象)
// ARG: 需要传递给 FN 的实参(数组)
function myBind() {
var _this = arguments[0];
var that = this;
var ary = [];
for (var i = 1; i < arguments.length; i++) {
ary.push(arguments[i])
}
return function() {
var arr = [];
for (var i = 0; i < ary.length; i++) {
arr.push(arguments[i]);
}
that.apply(_this, ary.concat(arr))
}
}
//=> ES6
function myBind(context, ...arg) {
return (...arg2) => {
this.apply(context, arg.concat(arg2));
}
}
Function.prototype.myBind = myBind;
4. 应用
需求一:获取数组中的最大值
//=> 给数组先排序(从大到小),再获取第一项
let ary = [12, 23,78,34,56];
let max = ary.sort(function(a,b) {
return b - a;
})[0];
//=> 假设法:假设第一个值是最大值,依次遍历数组中后面的每一项,与 max 进行比较,如果比假设的值要大,把当前项赋给 max
let ary = [12, 23,78,34,56];
let max = ary[0];
for (let i = 1; i < ary.length; i++) {
max = max < ary[i] ? ary[i] : max;
}
//=> 利用 eval 和 Math.max
var arr = [12, 23,78,34,56];
var str = arr.toString();
var max = eval('Math.max(' + str + ')');
max //=> 78
//=> 括号表达式(小括号的应用)
//=> 用小括号包起来,里面有很多项,每一项用逗号隔开,最后只获得最后一项的内容(但是会把其它内容的项也都过一遍,就是当作普通代码执行一遍)
(function(){
console.log(1);
},
function(){
console.log(2);
})(); //=> 2
let a = 1 === 1?(12, 23, 14):null; //=> a=14
//=> 不建议过多使用括号表达式,因为会改变 this
let fn = function() {console.log(this);}
let obj = {fn: fn};
(fn, obj.fn)(); //=> 执行的是第二个 obj.fn,但是方法中的 this 是 window 而不是 obj
(obj.fn)(); //=> this: obj
//=> 利用 apply,把 arr 中的值全部变成参数传入,因为 Math.max 参数必须一个个传入
var max = Math.max.apply(Math, arr)
//=> ES6 扩展操作符
var max = Math.max(...arr);
需求二:去除最大值,去除最小值,然后求平均数
arr.sort((a,b) => a - b); //=> sort 里面这个函数实际上是在全局定义的,只是在 sort 函数内部执行
// 相当于 function f(){}; arr.sort(f)
arr.shift();
arr.pop();
var num = eval(arr.join('+')) / arr.length;
//=> 思路:
//=> 1. ES6扩展操作符
//=> 2. 通过 for 循环 将 arguments 转化为数组,再使用数组中的方法
//=> 3. 改变 arguments 的 __proto__ 为 Array.prototype,然后使用数组方法
//=> 4. 通过 call 或 apply 将类数组转化为数组,然后利用数组的方法
function f(...arg) {
arg.sort((a,b) => a - b);
arg.shift();
arg.pop();
return eval(arg.join('+')) / arg.length
}
function f2() {
var arr = [];
for (let i = 0; i < arguments.length; i++) {
arr.push(arguments[i]);
}
arg.sort((a,b) => a - b);
arg.shift();
arg.pop();
return eval(arg.join('+')) / arg.length
}
function f3() {
arguments.__proto__ = Array.prototype;
arguments.sort((a,b)=>a-b).shift();
arguments.pop();
return eval(arguments.join('+')) / arguments.length;
}
function f4() {
var arr = [].slice.call(arguments);
//=> 原因在于 slice 内部实现只使用了 length 和索引进行循环,所以改变成类数组之后,也可以使用
}
类数组调用数组中的方法
先去通过数组找到对应的方法,然后用 call / apply
先改变方法里边的 this
指向,然后执行这个方法
数组中有些方法可以用,有些方法不能用。
原因在于,有些方法内部只操作数组的索引和 length 实现,就可以通过类数组调用,而其他的对象就不可以
有些方法不是通过这些实现的,同样也不可以。
如:concat 也可以克隆数组,但是不可以用于类数组
sort 也可以
需求三:改变事件函数中的 this
function fn() {
console.log(this);
}
let obj = {name:"obj"};
document.onclick = fn; //=> 把 fn 绑定给点击事件,点击的时候执行 fn
document.onclick = fn(); //=> 在绑定的时候,先把 fn 执行,把执行的返回值(undefined)绑定给事件,当点击的时候执行的是 undefined
//=> 需求:点击的时候执行 fn,让 fn 中的 this 是 obj
document.onclick = fn.call(this); //=> 虽然 this 确实改为了 obj,但是绑定的时候就把 fn 执行了,(call 是立即执行的)点击的时候执行的是 fn 的返回值
document.onclick = fn.bind(obj); //=> bind 属于把 fn 中的 this 预处理为 obj,此时 fn 没有执行,当点击的时候才会把 fn 执行
需求四:给事件函数传递参数
在事件处理程序中,我们没有办法在执行的时候传递参数,因为执行的时候是不可控的。
而基于 bind 可以预先处理 this,还可以给函数预先传递参数,还可以把事件对象等信息传递给函数。
function fn(x, y, e) {
console.log(this, x, y);
}
let obj = {name: 'aa'}
document.body.onclick = fn.bind(obj, 10, 20);
//=> {name: 'aa'}, 10, 20
返回的函数执行时,再传参数,会跟 bind
传的参数(除了第一个参数)结合起来。也就是说,这里 bind 传入了两个参数 10 20,由 x/y 接受,而 e 是浏览器在执行的时候自动传入的参数,会与前面预先传入的参数结合起来,总是作为最后一个参数。
实际上,可以使用一个匿名函数包裹,里面使用 call 执行 fn,这就是 bind 的原理
document.body.onclick = function(e) {
fn.call(obj, 10, 20, e);
}