这三个都是函数原型中的方法,用来改变某一个函数中 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);}};//=> ES6Function.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,所以执行 f1f2.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))}}//=> ES6function 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 进行比较,如果比假设的值要大,把当前项赋给 maxlet 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.maxvar 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);})(); //=> 2let a = 1 === 1?(12, 23, 14):null; //=> a=14//=> 不建议过多使用括号表达式,因为会改变 thislet 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 绑定给点击事件,点击的时候执行 fndocument.onclick = fn(); //=> 在绑定的时候,先把 fn 执行,把执行的返回值(undefined)绑定给事件,当点击的时候执行的是 undefined//=> 需求:点击的时候执行 fn,让 fn 中的 this 是 objdocument.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);}
