详解如何实现call/apply/bind

call、apply 及 bind 函数内部实现是怎么样的?

一、call

  • call改变了this指向
  • 函数执行
  • 支持传参

⚠️注意:this可以传入undefined或者null,此时this指向window,this参数可以传入基本类型,call会自动用Object转换,函数可以有返回值

Function.prototype.call2 = function(context) {
    context = context ? Object(context) : window;
    context.fn = this;
    
    let args = [...arguments].slice(1);
    const result = context.fn(...args);
    
    // es3写法
    // var args = [];
    // for(var i = 1, len = arguments.length; i < len; i++) {
        // args 会自动调用 args.toString() 方法,因为'context.fn(' + args +')'本质上是字符串拼接,会自动调用toString()方法
    //    args.push('arguments[' + i + ']');
    // }
    // var result = eval('context.fn(' + args +')');
       
    delete context.fn;
    return result;
}

思考上面的写法会有什么问题?
这里假设context本身没有fn属性,这样肯定不行,我们必须保证fn属性的唯一性

  • 实现方式一:es3模拟实现
    首先判断 context中是否存在属性 fn,如果存在那就随机生成一个属性fnxx,然后循环查询 context对象中是否存在属性 fnxx。如果不存在则返回最终值。
function fnFactory (context) {
    let unique_fn = 'fn';
    while (context.hasOwnProperty) {
        unique_fn = 'fn' + Math.random();
    }
    reutrn unique_fn;
}

tips:有两种方式可以判断对象中是否存在某个属性
1.in操作符,会检查属性名是否存在对象及其原型链中,注意数组的话是检查索引而不是具体值
例如对于数组来说,4 in [2, 4, 6] 结果返回 false,因为 [2, 4, 6] 这个数组中包含的属性名是0,1,2 ,没有4。
2.Object.hasOwnProperty(...)方法,只会检查属性是否存在对象中,不会向上检查其原型链。

  • 实现方式二:es6模拟实现
    利用Symbol,表示独一无二的值,不能使用 new 命令,因为这是基本类型的值,不然会报错。
Function.prototype.call2 = function(context) {
    context = context ? Object(context) : window;
    let fn = Symbol();
    context[fn] = this;
    
    let args = [...arguments].slice(1);
    const result = context[fn](...args);
    
    delete context[fn];
    return result;
}

测试一下:

let value = 2;
let foo = {
    value: 1
};

function bar(name, age) {
    console.log(this);
    console.log(name, age);
}
bar.call2(foo, 'test', 18); // foo{value:1}   test 18
bar.call2(null); // window  undefined undefined
bar.call2(123, 'test', 18); // Number{123}   test 18

二、apply

apply与call的思路基本相同,区别在于传参为数组,实现如下

Function.prototype.apply2 = function(context, arr) {
    context = context ? Object(context) : window;
    let fn = Symbol();
    context[fn] = this;
    
    let result;
    if (arr) {
        result = context[fn](...arr);
    } else {
        result = context[fn]();
    }
    
    delete context[fn];
    return result;
}

测试一下:

let value = 2;
let foo = {
    value: 1
};

function bar(name, age) {
    console.log(this);
    console.log(name, age);
}
bar.apply2(foo, ['test', '18']); // foo{value:1}   test 18
bar.apply2(null); // window  undefined undefined
bar.apply2(123, ['test', '18']); // Number{123}   test 18

三、bind

bind() 函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现。

bind方法与call/apply最大的区别就是bind返回一个绑定上下文的函数,而call/apply是直接执行了函数,特性如下:

  • 可以指定this
  • 返回一个绑定了this的函数
  • 可以传参
  • 柯里化
    -- 获取返回函数的参数,然后同第3点的参数合并成一个参数数组,并作为 self.apply() 的第二个参数。

⚠️特别:绑定函数也能使用new操作符创建对象
这种行为就行把原函数当作构造器,提供的this被忽略,同时调用时的参数被提供给模拟函数,可以通过修改返回函数的原型来实现

Function.prototype.bind2 = function (context) {
    // 保存this指向调用函数的对象
    const self = this;
    // 截取第一个参数后的参数
    const args = [...arguments].slice(1);
    //const args = Array.prototype.slice.call(arguments, 1); 

    // 返回函数
    return function () {
        const bindArgs = [...arguments];
        // const bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}

测试一下:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
};

var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}

bind还有一个在上文说过的特性需要实现:

一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

例如:

const value = 2;
const foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
};
bar.prototype.friend = 'kevin';

const bindFoo = bar.bind(foo, 'bob');
const obj = new bindFoo(20);
// undefined 
// bob
// 20

obj.habit; // shopping
obj.friend; // kevin

👆上面的例子this.value 输出为 undefined,既不是全局value 也不是foo对象中的value,这说明绑定的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj。

这一特性可以通过修改返回的函数的原型来实现

说明:

  • 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit 属性。
  • 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
  • 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值,即上例中 obj 可以获取到 bar 原型上的 friend。

⚠️ 注意:调用 bind 的不是函数,需要抛出异常。

完整代码如下:

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') {
        throw new Errow('Function.prototype.bind - what is trying to be bound is not callable');
    }
    const self = this;
    const args = [...arguments].slice(1);
    
    const fBound = function () {
        const bindArgs = [...arguments];
        return self.apply(
            this instanceof fBound ? this : context,
            args.concat(bindArgs)
        );
    }
    // 直接使用ES5的 Object.create()方法生成一个新对象
    fBound.prototype = Object.create(this.prototype);
    // 或者:
    // const fNOP = function () {}; // 创建一个空对象
    // fNOP.prototype = this.prototype; // 空对象的原型指向绑定函数的原型
    // fBound.prototype = new fNOP(); // 空对象的实例赋值给 fBound.prototype
    
    return fBound;
}

本文参考链接

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