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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容