对于前面2个 apply,call大家应该非常熟悉了,都可以改变this指向,都可以传参数,但是bind的话很多人可能觉得和它们没有什么区别,估计用bind也用的少,下面我来一一分析下各自的实现原理:
1.call方法
1.第一个参数是this指向的对象。
2.使用的单个参数进行传递。
3.用于确定了函数的形参有多少个的时候用。
举个例子:
var name = '李四'
var b = {name:'张三'};
function a(n){
console.log(this.name+n+'岁了')
}
a.call(b,'18');//'张三18岁了'
a.call(null,'18');//'李四18岁了'
我们可以这样理解:把a方法放到b里面,然后我在b的环境下执行a(如果b没有值,那就相当再window环境下执行a,this是指向window的),相当b.a(18)或者a(18),然后我再b里面删除a方法。知道原理了,那么我们按照上面的例子来自己封装下call方法:
Function.prototype.call = function (obj) {
// 当call的第一个参数没有或者是null的时候,this的指向是window
var obj= obj || window;
// 把a方法放进里面
obj.fn = this;
// 用于存储call后面的参数
var args = [];
var len = arguments.length;
// 这里是为了将一个函数的参数传入到另外一个函数执行
for (var i = 1; i < len; i++) {
args.push('arguments[' + i + ']');
};
// 在eval的环境下 args数组会变成一个一个参数字符串(默认是会调用Array.toString())
var result = eval('obj.fn(' + args + ')');
// 删除b里面的a方法
delete obj.fn;
// 因为函数可能有返回值,所以把结果也返回出去给他们
return result;
};
2.apply方法(和call没啥区别)
1.第一个参数是this指向的对象。
2.使用的参数是数组进行传递。
3.用于确定了函数的形参的个数不确定的情况下使用。
原理我就不累赘了,和call没有什么区别,只有传递的形参不用,apply传递的一定要是是数组,那么我们就可以在传参的进行判断下。我们来模拟下代码的实现:
Function.prototype.apply = function (obj, arr) {
// 当apply的第一个参数是null的时候,this的默认指向是window
var obj = obj || window;
// 把该函数挂载到对象上
obj.fn = this;
//判断有没有传值
if (!arr) {
result = obj.fn();
} else {
//判断传入的是不是数组,不是的话抛出异常
if (!Array.isArray(arr)) {
throw new Error('上传的必须是数组');
};
var args = [];
// 用于存储apply后面的参数
for (var i = 0; i < arr.length; i++) {
args.push('arr[' + i + ']');
};
// 这里的args默认是会调用Array.toString()方法的
var result = eval('obj.fn(' + args + ')');
}
// 删除函数
delete obj.fn;
// 因为函数可能有放回值,所以把结果也返回出去给他们
return result;
}
2.bind方法
这里重要讲下bind,bind和call和apply还是有点区别的。
1.bind会创建一个函数(称为绑定函数),创建一个新函数而不执行,这是bind和call与apply方法的一个重要差别,call和apply这两个方法都会立即执行函数,返回的是函数执行后的结果。而bind函数只创建一个新函数而不执行。
2.函数的柯里化(使用一个闭包返回一个函数),柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
下面我们来慢慢模拟一下bind的实现(首选返回一个函数):
Function.prototype.bind = function(obj){
var self =this;
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(obj,args);
};
return newFn;
};
//例子:
var name = '李四'
var b = {
name: '张三',
};
function a(age) {
console.log(this.name + age + '岁了')
}
var p =a.bind(b,'18');
p();//"张三18岁了"
和预想的一样,bind得底部实现还是apply方法,很稳!!,有没有发现啥问题,我只在返回的新函数里面传了第2个参数,调用的时候都没传参数,这是个问题!然后补充一下:
Function.prototype.bind = function(obj){
var self =this;
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
return newFn;
};
//例子改下参数
var name = '李四'
var b = {
name: '张三',
};
function a(age,sex) {
console.log(this.name + age + '岁了,性别'+sex);
}
var p =a.bind(b,'18');
p('男');//张三18岁了,性别男
和预想的一样!可以接受2个参数,实现了函数的柯里化。是不是还觉得哪里有问题?对的,函数才能使用bind!这里需要判断一下,再改下代码:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函数才可以调用bind');
};
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
return newFn;
};
好了,到这里的话,感觉好像代码实现的差不多,但是这里有个重点,当我们把创建出来的新函数当做构造函数的时候,官方文档有这么一句话:“说明绑定过后的函数被new实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的this被忽略(继承原函数的this对象),但是参数还是会使用。” 看上去不是很明白,其实主要是说,当new完之后 新创建出来的实例要继承原函数的原型,并且this指向新创建出来的实例对象(绑定的this将失效)。首先实现下原理继承:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函数才可以调用bind');
};
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
//继承原函数的原型
newFn.prototype = this.prototype;
return newFn;
};
如果这样写的话是有问题的,如果我改变了新创建出来的函数的原型同样也修改了原函数的原型,所以这里需要写一个过渡函数(如果不懂继承的同学,可以点击这里)。
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函数才可以调用bind');
};
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
};
//过渡函数
var f = function(){};
f.prototype = this.prototype;
newFn.prototype = new f();
return newFn;
};
接下来需要判断this的值,判断到底是不是new出来的实例,通过instanceof 判断。简单解释下 instanceof ,假如a instanceof b,意思是a是不是b的实例,或者这样说:b的prototype是否在a的原型链上。再改下代码:
Function.prototype.bind = function(obj){
var self =this;
if(typeof this !=='function'){
throw new Error('只有函数才可以调用bind');
};
//第一个参数为它运行时的this,应该取第二个之后的参数
var args =Array.prototype.slice.call(arguments,1);
//返回一个新函数闭包
var newFn = function(){
self.apply(this instanceof f ? this : obj ,args.concat(Array.prototype.slice.call(arguments)));
};
//过渡函数
var f = function(){};
f.prototype = this.prototype;
newFn.prototype = new f();
return newFn;
};
到这里代码就修改的差不多了,最后拿这段代码“this instanceof f ? this : obj ”分析下,这里的意思是如果是new出来的,this指向它实例出来的对象,如果不是,那么this是指向绑定的obj对象。如果obj没有值,不传值或者是null,那么这个this是指向window的,有些同学代码在这里做了下个判断 “this instanceof f ? this : obj || this”,这样写的话,this不一定都指向window的,比如看下面这个例子:
var name = '张三';
var foo = {
name:'李四',
fn: fn.bind(null) //如果在这里执行,this是指向widonw的
};
function fn() {
console.log(this.name);
}
//foo这里调用fn,所以在这里把this的指向改变了,this===foo;
foo.fn() //'张三'
所以说这个判断是不对的。好了,到这里就差不多已经说完了(如果有不对之处,欢迎指正,不胜感激!!!),本来还想说下bind的几个难点,怕写的太多都看不下去了,还是下一篇再说吧,欢乐的时光总是过得特别快,又到时候和大家讲拜拜!!