【深入学习JS】07_call、apply和bind

前言

这一篇是直接学习 JavaScript深入之call和apply的模拟实现JavaScript深入之bind的模拟实现的,建议直接点开链接学习!

call

call()方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。

第一版

var foo = {
    value: 1
};

function bar(){
    console.log(this.foo);
}

bar.call(foo);     // 1

可以将这个例子转换为:

var foo = {
    value: 1,
    bar: function(){
        console.log(this.value);
    }
}
// 然后调用之后,删除bar就行

所以实现如下:

Function.prototype.call2 = function(context){
    context.fn = this;
    context.fn();
    delete context.fn;
}

第二版

call要处理传参的问题:

var foo = {
    value: 1
}
function bar(name, age){
    console.log(name);
    console.log(age);
    console.log(this.value);
}

bar.call(foo, 'sugarMei', 18);    // sugarMei 18 1

函数内部自带有一个arguments参数,里面记录了所有的参数,形式如下:

arguments = {
    0: foo,
    1: "sugarMei",
    2: 18,
    length: 3
}

所以我们需要将除了第一个之外的所有参数保存起来,传给另一个函数,然后借助eval

Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(let i = 1, len = arguments.length; i < len; i++){
        args.push('arguments['+i+']');
    }
    eval('context.fn('+args+')');
    delete context.fn;
}

第三版

考虑一下如果thisnull,和带有返回值的情况:

Function.prototype.call2 = function(context){
    context = context || window;
    context.fn = this;
    var args = [];
    for(let i = 1, len = arguments.length; i < len; i++){
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn('+ args +')');
    delete context.fn;
    return result;
}

apply

Function.prototype.apply2 = function (context, arr) {
    context = context || window;
    context.fn = this;
    var result;
    if(!arr){
        result = context.fn();
    }else{
        var args = [];
        for(let i = 0, len = arr.length; i < len; i++){
            args.push('arguments['+i+']');
        }
        result = eval('context.fn('+args+')');
    }
    
    delete context.fn;
    return result;
}

bind

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

第一版

举个例子:

var foo = {
    value: 1
}

function bar(){
    console.log(this.value);
}

var bindFoo = bar.bind(foo);
bindFoo();        // 1

我们需要做的就是借用apply或者call返回一个新函数:

Function.prototype.bind2 = function(context){
    var self = this;
    return function(){
        return self.apply(context);
    }
}

第二版

bind函数支持传参:

var foo = {
    value: 1
}

var bar(name, age){
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFoo = bar.bind(foo, 'daisy');
bindFoo(18);
// 1
// daisy
// 18

在这里,可以在使用bind的时候传递参数,在执行的时候还能再传递参数。

Function.prototype.bind2 = function(context){
    var self = this;
    var args = Array.prototype.slice(arguments, 1);
    return function(){
        var bindArgs = Array.prototype.slice(arguments);
        return this.apply(context, args.concat(bindArgs));
    }
}

第三版

使用bind之后会返回一个新的函数,比如上面的bindFoothis指向的是foo,但是如果bindFoo被当做构造函数使用的时候呢?this指向的是谁?

在前面的学习中我们已经知道,this的指向优先级:箭头函数绑定 > new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定,即new绑定的优先级比bind这种显示绑定高;而且使用new时,实例的__proto__会指向构造函数的prototype,实例的this被绑定在构造函数中。

举个栗子:

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

function bar(name, age){
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'sugarMei';

var bindFoo = bar.bind(foo, 'daisy');
var obj = new bindFoo(18);
// undefined
// daisy
// 18
console.log(obj.habbit); // shopping
console.log(obj.friend); // sugarMei

由上面的栗子可以发现var obj = new binFoo(18);中,由于this指向实例,但是它并不存在value值,所以返回undefined

这里我们需要修改返回函数的原型:

Function.prototype.bind2 = function(context){
    var self = this;
    var args = Array.prototype.slice(arguments, 1);
    var fBound = function(){
        var bindArgs = Array.prototype.slice(arguments);
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 确保还是可以读取到原来的prototype上的属性
    fBound.prototype = self.prototype;
    return fBound;
}

第四版

如果使用上面的例子:

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

var bindFoo = bar.bind2(foo, 'daisy');
bindFoo.prototype.fiend = '王花花';
console.log(bar.prototype.fiend);    // 王花花

修改bindFoo.prototype上的属性,也会影响到bar.prototype,这里需要做一些调整:

Function.prototype.bind2 = function(context){
    var self = this;
    var args = Array.prototype.slice(arguments, 1);
    
    var fNOP = function(){};
    
    var fBound = function(){
        var bindArgs = Array.prototype.slice(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }
    
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

最终版

如果调用bind的不是函数,就要抛出异常:

Function.prototype.bind = function(context){
    if(typeof this !== 'function'){
         throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    
    var self = this;
    var args = Array.prototype.slice(arguments, 1);
    
    var fNOP = function(){};
    
    var fBound = function(){
        var bindArgs = Array.prototype.slice(arguments);
        return self.apply(this instanceof fNOP? this : context, args.concat(bindArgs));
    }
    
    fNOP.prototype = this.prototype
    fBound.prototype = new fNOP();
    return fBound;
}

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值