apply, bind, call方法剖析
Function.prototype.call(),Function.prototype.apply(),Function.prototype.bind()
是三种改变函数内部this指向(即函数执行时所在作用域)的方法。
1.Function.prototype.call(thisValue, param1, param2,....)
// 模拟源码 /** * 1. 让函数立即执行 * 2. 改变函数内部的this指向 */ Function.prototype.call = function call(context) { if (this.name === 'call' && typeof context !== 'function') { // 多个call函数连续调用 throw new TypeError('the first param should be a function'); } context = context ? Object(context) : window; context.fn = this; // 改变函数内部的this指向 const args = []; for(let i=1; i<arguments.length; i++) { args.push('arguments[' + i + ']'); } // 立即执行;利用了数组的toString()属性 const r = eval('context.fn('+ args + ')'); delete context.fn; return r; }
1)第一个参数
第一个参数必须是对象,
- 如果是 空,undefine, null 都默认绑定到window;
- 如果是基础类型(如字符型,数值型,布尔型)会自动将基础类型转为包装类型,如: Number(5)
- 如果是this, 表示绑定的是当前作用域
var n = 123; var obj = { n: 456 } function a() { return this; } // this === window a.call(); // window a.call(null); // window a.call(undefine); //window a.call(window); // window // this === obj a.call(obj); // obj // this === 包装对象 a.call(5); // Number(5) {[[PrimitiveValue]]: 5}
2) 其余的参数
其余的参数作为函数调用时传入的参数
function add(a,b) { return a + b; } //this表示固定当前this的指向; this === window add.call(this, 1, 2); // 3
3)应用
// 调用对象的原生方法 var obj = { a: 5 } console.log(obj.hasOwnProperty('toString')); // fasle obj.hasOwnProperty = function() { // 覆盖obj对象继承自原型链上的方法 return true; // 改变的是obj本身的方法,原型链上的方法不变 } console.log(obj.hasOwnProperty('toString')); // true // obj可以使用原型链上的方法,表示在obj的作用域使用hasOwnProperty的方法 console.log(Object.prototype.hasOwnProperty.call(obj)); // false
2. Function.prototype.apply(thisValue, [param1, ....])
// 模拟源码 /** * 1. apply方法让函数立即执行 * 2. apply方法绑定函数内部的this指向 * 3. 参数以数组形式传递 */ Function.prototype.apply = function apply(context, args) { if (this.name === 'apply' && typeof context !== 'function') { // 多个apply函数连续调用;apply作为函数原型链上的方法,只能被函数调用 throw new TypeError('the first param should be a function'); } context = context ? Object(context) : window; context.fn = this; if(!args) {// 如果不传参 return context.fn(); } const r = eval('context.fn('+ args +')'); delete context.fn; return r; }
1)第一个参数和call方法规则相同
2)第二个参数
第二个参数是数组, 将数组中的参数依次作为调用函数的参数,如果函数中参数个数少于apply传参个数,只取前面的。
function add(a,b) { console.log(a,b); // 1 2 return a + b; } add.apply(null, [1, 2, 3, 4, 5]);
第二个参数还可以是类数组,如arguments
function add(a,b) { console.log(a,b); // 3,4 return a + b; } function newAdd() { return add.apply(null, arguments); } var result = newAdd(3,4,5,5); console.log(result); // 7
3)应用
//1) 查找数组的最大值 const arr = [1,3,5]; Math.max.apply(null, arr); // 5 // 还可以 Math.max(...arr); // 5 // 2) 将数组的空项转为undefined;undefine可以被遍历,空会被遍历函数忽略 const arr = [1,,4]; Array.apply(null, arr); // [1,undefined,4] Array是数组的构造函数
3. Function.prototype.bind(thisValue, params, param2...)
// 模拟源码 /** * 1. bind可以绑定this的指向;bind还可以绑定参数; * 最后的参数=bind时传入的参数+新函数的参数 * 2. bind绑定后返回一个新的函数 * 3. 如果返回的函数被使用了new命令,this指向实例对象 * 4. new命令生成的实例可以找到原有类的原型对象 */ Function.prototype.bind = function bind(context) { context = context ? Object(context) : window; // 获取bind时传入的参数 const boundArgs = Array.prototype.slice.call(arguments, 1); const that = this; //原函数 let boundFn = function() { const args = Array.prototype.slice.call(arguments, 1); if (this instanceof boundFn) {// 说明使用了new命令 context = this; //new绑定 > 显示绑定 } return that.apply(context, boundArgs.concat(args)); }; // 新生成的函数继承bind之前的原型对象 function Fn() {} Fn.prototype = this.prototype; boundFn.prototype = new Fn(); // 返回新函数 return boundFn; }
1)第一个参数
同call,apply方法
2)剩余的参数
当bind(thisValue, ...)后面参数的个数小于原函数的个数时,绑定部分参数;
// 绑定部分参数;相当于参数复用,只需要处理剩余的参数 function fn(a,b) { return a + b } var newFn = fn.bind(null, 5) /* newFn = function(b) { return 5 + b } */ console.log(newFn(6)); // 11
3)应用
- bind方法每次运行都返回一个新的函数;在监听事件时需要注意
document.addEventListener('click', obj.fn.bind(this)) // 下面取消绑定无效;因为是不同的函数 document.removeEventListener('click',obj.fn.bind(this))
正确的写法写法应该是
var listener = obj.fn.bind(this); document.addEventListener('click', listener); document.removeEventListener('click', listener);
- 结合回调函数使用
obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 张三 // 张三 // 张三
-
和call方法结合使用;改变传参格式
[1, 2, 3].slice(0, 1) // [1] // 等同于 Array.prototype.slice.call([1, 2, 3], 0, 1) // [1] // 上面代码的意思是在Array.prototype.slice对象上调用call方法 // 即 var mySlice = Function.prototype.call.bind(Array.prototype.slice); console.log(mySlice([1,2,3], 0, 1)) // 能调用slice的对象是第一个参数 //同理 var myPush = Function.prototype.call.bind(Array.prototype.push); // bind方法传参也可以被改变 function f() { console.log(this.a); } var obj = {a: 1} var myBind = Function.prototype.call.bind(Function.prototype.bind); // 能调用bind方法只能是function myBind(f, obj)(); // 1
4. apply.call,bind之间的区别
1)call和apply方法调用后,相当于绑定this后立即执行
2)bind方法是返回一个新的函数,并不立即执行
应用:
1)将类数组转为数组
// 1)将类数组转为数组 // apply,call方法立即执行 Array.prototype.slice.apply({ 0: 1, length: 1,}); Array.prototype.slice.call({ 0: 1, length: 1,}); // bind方法生成一个新的函数,需要手动执行,后面加() Array.prototype.slice.bind({ 0: 1, length: 1,})();
2)给回调函数绑定对象
// 2)绑定回调函数的对象; // 未进行绑定前,回调函数中的this一般都是window var name = 'Hello World'; var obj = { name: 'Lyra', times: [1,2,4], print: function() { console.log(this === obj); // true this.times.forEach(function() { console.log(this === window); // true console.log(this.name); // Hello World }) } } obj.print(); // 使用bind方法绑定回调函数中的this var name = 'Hello World'; var obj = { name: 'Lyra', times: [1,2,4], print: function() { this.times.forEach((function() { console.log(this); // obj --3次 console.log(this.name); // Lyra --3次 }.bind(this))) // 不能用call,apply替换,因为会立即执行,就不再是函数了,会返回函数的默认返回值undefined } } obj.print(); // 使用call, apply方法绑定回调函数中的this; var name = 'Hello World'; var obj = { name: 'Lyra', times: [1,2,4], print: function() { const that = this; this.times.forEach((function() {// 因为apply,call会立即执行,所以要嵌套一层函数 (function IIFE() { console.log(this); // obj --3次 console.log(this.name); // Lyra --3次 }).call(that); // 可以替换成apply。IIFE需要用括号扩起来变为函数表达式。否则函数声明不能调用call方法。 })) } } obj.print();