call,apply,bind实现以及原理

(一) call源码解析

例子:

function add(c, d){
    return this.a + this.b + c + d
}

var obj = { a: 1, b: 2 }
console.log(add.call(obj, 3, 4))

网上介绍call大致说法是,call改变了this的指向,本来上下文环境调用的thiswindow,而callthis指向了obj,brabra,不管你懂不懂,成功绕晕你就已经ok了,但是实际发生的过程,可以看成下面的样子。

var o = {
  a: 1,
  b: 2,
  add: function(c, d) {
    return this.a + this.b + c + d
  }
};
  1. 调用之后,新创建一个对象o,obj上的属性变成o的属性
  2. 给o对象添加一个add属性,这个时候 this 就指向了 o,
  3. o.add(5,7)得到的结果和add.call(o, 5, 6)相同。
  4. 但是给对象o添加了一个额外的add属性,这个属性我们是不需要的,所以可以使用delete删除它。

so, 基本上就三步

// 1. 将函数设为对象的属性
 o.fn = bar
// 2. 执行该函数
 o.fn()
// 3. 删除该函数
 delete o.fn

基于ES3实现 call

Function.prototype.es3Call = function (context) { 
    var content = context || window; 
2    content.fn = this; 
    var args = []; 
    // arguments是类数组对象,遍历之前需要保存长度,过滤出第一个传参 
    for (var i = 1, len = arguments.length ; i < len; i++) { 
        // 避免object之类传入 
        args.push('arguments[' + i + ']');  
    } 
    var result = eval('content.fn('+args+')'); 
    delete content.fn; 
    return result; 
    
} 
console.error(add.es3Call(obj, 3, 4)); // 10 
console.log(add.es3Call({ a: 3, b: 9 }, 3, 4)); // 19
console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4

解析,第2步是关键,将实例的环境this传入,实际上就是函数调用,称其为实际执行函数,args是参数,实际上就是调用了这个函数,也就是传说中的改变this指向了

ES6实现call,差别不大,es6新增...rest运算符,进行对有iterator属性进行取值操作

// ES6 call 实现 
Function.prototype.es6Call = function (context) { 
    var context = context || window; 
    context.fn = this; 
    var args = []; 
    for (var i = 1, len = arguments.length; i < len; i++) {      
        args.push('arguments[' + i + ']');  
    } 
    const result = context.fn(...args); 
    return result; 
    
}
    console.error(add.es6Call(obj, 3, 4));  
    console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4 

(二) apply源码解析

apply 和 call 区别在于 apply 第二个参数是Array,而 call 是一个个传入

// 基于es3的实现

Function.prototype.es3Apply = function (context, arr) { 
    var context = context || window; 
    context.fn = this; 
    var result; 
    if (!arr) { 
        result = context.fn();  
    }  else { // 获取参数 
        var args = []; 
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']'); 
            
        } 
        // 执行函数 
        result = eval('context.fn(' + args + ')') 
        
    } 
    delete context.fn; return result 
    
} 
console.log(add.es3Apply(obj, [1, 'abc', '2'])); // 4abc 
console.log(add.apply(obj, [1, 2]));  // 6

基于es6的实现

Function.prototype.es6Apply = function(context, arr){
    var context = context || window;
    context.fn = this;
    var result;
    if(!arr) {
        result = context.fn();
    } else {
        if(!(arr instanceof Array)) 
            throw new Error('params must be array');
        result = context.fn(...arr)
    }
    delete context.fn;
    return result;
}

console.log(add.es6Apply(obj, [1, 2])); // 6

(三)bind 源码解析

bind()方法回创建一个新函数。
当这个新函数被调用时,bind()第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数

bind方法实例

function foo(c, d){
    this.b = 100;
    console.log(this.a);
    console.log(this.b);
    console.log(this.c);
    console.log(this.d);
}
// 将foo bind到{ a: 1 }
var func = foo.bind({ a: 1 }, '1st');
func('2nd');
// 1 100 1st 2nd
// 即使再次call也不能改变 this 
func.call({ a: 2 } , '3rd');  
// 1 100 1st 2nd

// 当 bind 返回的函数作为构造函数的时候
// bind 时指定的 this 会失效, 但传入的参数依然生效
// 所以使用func为构造函数时,this不会指向{ a: 1 },this.a 为undefined
new func('4th')
//undefined 100 1st 4th

bind()方法绑定了首参数的时候,就已经是改不了引用了,一直都用这个引用,除非当成是构造函数进行调用,this会改变指向,调用构造函数的话,this会指向实例

首先用ES3来实现

Function.prototype.es3Bind = function (context) {
    if(typeof this !== 'function')
        throw new TypeError('what is trying to be bound is not callback');
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fBound = function() {
        //获取函数的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        // 返回函数的执行结果
        // 判断函数是作为构造函数还是普通函数
        // 构造函数this instanceof fNOP返回true,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值。
        // 当作为普通函数时,this 指向 window 此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fNOP ? this: context, args.concat(bindArgs));
    }
    // 创建空函数
    var fNOP = function() {};
    // fNOP函数的prototype为绑定函数的prototype
    fNOP.prototype = this.prototype;
    // 返回函数的prototype等于fNOP函数的实例实现继承
    fBound.prototype = new fNOP();
    // 以上三句相当于Object.create(this.prototype)
    return fBound;
}

var func = foo.es3Bind({a: 1}, '1st'); 
func('2nd'); // 1 100 1st 2nd 
func.call({a: 2}, '3rd'); // 1 100 1st 3rd 
new func('4th'); //undefined 100 1st 4th 

es6实现

Function.prototype.es6Bind = function(context, ...rest) { 
    if (typeof this !== 'function') 
        throw new TypeError('invalid invoked!'); 
    var self = this; 
    return function F(...args) { 
        if (this instanceof F) {
            return new self(...rest, ...args)  
        } else {
            return self.apply(context, rest.concat(args))  
        }
    }      
} 
var func = foo.es3Bind({a: 1}, '1st'); 
func('2nd'); // 1 100 1st 2nd 
func.call({a: 2}, '3rd'); // 1 100 1st 3rd new func('4th'); //undefined 100 1st 4th 

面试问题:

function fn1(){
   console.log(1);
}
function fn2(){
    console.log(2);
}

fn1.call(fn2);     //输出 1
 
fn1.call.call(fn2);  //输出 2

fn1.call(fn2), 只是改变了fn1内部this的指向,不是指向调用它的上下文环境也就是 window ,而是指向了fn2,但是不影响 fn1 代码的执行,输出 1

fn1.call.call(fn2)要注意,其实是分两段来解读
一 是 call(),后面加括号才代表是call函数,不然就是对象,也就是说fn1.call对象
fn1.call会通过原型链找到最终对象,本质上是Function.prototype.call

对于fn1.call.call(fn2)

首先,调用call函数时,也就是 fn1.call.call(fn2) 的加粗部分,先将 fn2 作为临时的 context 对象。然后将 fn1.call 这个函数对象作为 实际执行函数属性: context.fn = fn1.call
注意: fn1.call会通过原型链找到最终对象,其本质为 Function.prototype.call; 没有其他参数,直接执行 fn1.call() 函数,即 context.fn(); 此时函数的本质还是 Function.prototype.call 函数对象。 不过执行这个函数的环境是在 Function.prototype.call() 中, 只不过是第一次调用 call() 函数。 第一次调用 call() 函数将 this 关键字指向了 fn2,故而 在 fn1.call.call(fn2)加粗部分的 函数中执行的 call函数执行过程中的 this指向的是 fn2;传入的参数为空,故而 新的 call()函数对象 的this关键字 被替换为window; 而执行 this()时,就是执行 fn2();不涉及 this操作。故最终输出2

你可能感兴趣的:(call,apply,bind实现以及原理)