call、apply和bind原理和模拟实现
前言
在 javascript 中,call、apply和bind 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
如果用一句话介绍 call那就是,使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
call() 和 apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。
bind()方法则会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
总的来说call和apply会自动执行,而bind不会。
举个例子:
var foo = { value: 1 }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); } bar.call(foo, 'vision', 25); bar.apply(foo,['vision', 25]); var bindFoo = bar.bind(foo, 'vision', 25); bindFoo() 复制代码
一、call和apply的模拟实现
模拟第一步
通过上面的例子,我们看到主要有以下两点:
this指向改变
函数执行了
改变this指向的一个思路是,我们可以把函数挂载到一个对象上,这样这个函数的this就指向了这个对象,但是为了不影响原本的对象,在我们执行完函数后需要使用delete
删除挂载在这个对象的函数。
例子:
var foo = { value: 1, bar: function() { console.log(this.value) } }; foo.bar() // 1 delete foo.bar 复制代码
所以一共有三步:
将函数设置为对象的属性:foo.fn = bar
执行函数:foo.fn()
删除函数:delete foo.fn
封装起来,第一版如下:
// 第一版 Function.prototype.call2 = function(context) { context.fn = this; // 把函数挂载到指定的对象上 context.fn(); //执行 delete context.fn; //删除 } // 打开浏览器验证一下 var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call2(foo); // 1 复制代码
ok!
模拟第二步
在 call 中是可以传参数的,传入的参数并不确定,所以我可以使用 Arguments 对象来获取参数,arguments 是一个对应于传递给函数的参数的类数组对象。在第一个例子中我们的 Arguments 对象是这样的。
// arguments = { // 0: foo // 1: 'vision', // 2: 25, // length: 3 // } var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } 复制代码
这样不确定长度的参数就获取到了,用ES6的写法来实现我们第二版
// 第二版 Function.prototype.call2 = function(context) { context.fn = this; let args = []; for(let i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } context.fn(...args); delete context.fn; } 复制代码
模拟第三步
还有2个细节需要注意:
1、this 参数可以传 null 或者 undefined,此时 this 指向 window
2、this 参数可以传基本类型数据,原生的 call 会自动用 Object() 转换
3、函数是可以有返回值的
// 第三版 Function.prototype.call2 = function(context) { context = context ? Object(context) : window; context.fn = this; let args = []; for(let i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } let result = context.fn(...args); delete context.fn; return result; } 复制代码
apply和call的区别就是传参的不同,所以apply的模拟
Function.prototype.apply2 = function(context, arr) { context = context ? Object(context) : window; context.fn = this; let result; if (!arr) { result = context.fn(); } else { result = context.fn(...arr); } delete context.fn; return result; } 复制代码
bind的模拟实现
bind的特点:
返回一个函数
可以传入参数
关于指定 this 的指向,我们可以参考 call 或者 apply 实现
// 第一版 Function.prototype.bind2 = function (context) { var self = this; return function () { return self.apply(context); } } 复制代码
返回一个函数,这个函数使用apply改变this。
bind的参数模拟
bind方法可以在调用bind()时传入参数,也可以在函数执行时传入其他参数
var foo = { value: 1 }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); } var bindFoo = bar.bind(foo, 'vision'); bindFoo('25'); // 1 vision 25 复制代码
函数需要传 name 和 age 两个参数,可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age,这里我们也可以用arguments来处理。
// 第二版 Function.prototype.bind2 = function (context) { // 第一个参数是context,所以从第二个参数开始,这里获取的是调用bind()传入的参数 var aArgs = Array.prototype.slice.call(arguments, 1), self = this; return function () { // 这里获取的是执行函数传入的参数,然后把两个参数 concat 组合起来 var bindArgs = Array.prototype.slice.call(arguments); return self.apply(context, aArgs.concat(bindArgs)); } } 复制代码
构造函数特性
完成了改变this和参数的问题,最难的部分要来了,bind还有一个特性就是
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind(foo, 'vision'); var obj = new bindFoo('25'); // 使用 new 创建对象,指定的this绑定失效 // undefined // vision // 25 console.log(obj.habit); // shopping console.log(obj.friend); // kevin 复制代码
整理一下:
用bind创建的函数继承原函数的原型
使用new 创建对象时this被忽略
想要创建出来的函数继承原函数的原型,我们可以让返回函数的prototype修改为绑定函数的 prototype,这样就解决了第一个问题
Function.prototype.bind2 = function (context) { var aArgs = Array.prototype.slice.call(arguments, 1), self = this, fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(context, aArgs.concat(bindArgs)); }; // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值 fBound.prototype = this.prototype; return fBound } 复制代码
还有个this指向问题,这时候的this还是指向了 context,所以我们要做个判断
// 第三版 Function.prototype.bind2 = function (context) { var aArgs = Array.prototype.slice.call(arguments, 1), self = this, fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 用instanceof判断当前是否被当做构造函数,如果是则把this指向实例,可以让实例获得来自绑定函数的值 return self.apply(this instanceof fBound ? this : context, aArgs.concat(bindArgs)); }; // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值 fBound.prototype = this.prototype; return fBound } 复制代码
还有一个问题,因为fBound.prototype = this.prototype;
的存在,当改变 fBound.prototype的时候,this.prototype也会改变。所以我们进行一下优化。
Function.prototype.bind2 = function (context) { var aArgs = Array.prototype.slice.call(arguments, 1), self = this, fNOP = function() {}, fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 用instanceof判断当前是否被当做构造函数,如果是则把this指向实例,可以让实例获得来自绑定函数的值 return self.apply(this instanceof fBound ? this : context, aArgs.concat(bindArgs)); }; // 维护原型关系 if (this.prototype) { fNOP.prototype = this.prototype; } // 下行的代码使fBound.prototype是fNOP的实例,因此 // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例 fBound.prototype = new fNOP(); return fBound; } 复制代码
至此我们就完成了bind方法的模拟。
作者:高级前端小白
链接:https://juejin.cn/post/7030308581979193351