一文学会js里bind方法的模拟实现,掌握bind、apply和bind区别和用法

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

bind

一句话介绍 bind:

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

由此我们可以首先得出bind 函数的两个特点:

  • 返回一个函数
  • 可以传入参数

返回函数的模拟实现

从第一个特点开始,我们举个例子:

var foo = {
    value: 1
};

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

// 返回了一个函数
var bindFoo = bar.bind(foo); 
bindFoo(); // 1

关于指定 this 的指向,我们可以使用 call 或者 apply 实现

我们来写第一版的代码:

// 第一版
Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}
// bar得是一个函数 在可以在原型链上找到bind2方法
var bind2Foo = bar.bind(foo); 
bind2Foo(); // 1

之所以 return self.apply(context),是考虑到绑定函数可能是有返回值的

传参的模拟实现

接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind 的时候,是否可以传参呢?我在执行 bind 返回的函数的时候,可不可以传参呢?让我们看个例子:

var foo = {
    value: 1
};

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

}

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

函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个 name,
在执行返回的函数的时候,再传另一个参数 age!

我们用 arguments 进行处理

// 第二版
Function.prototype.bind2 = function (context) {
    var self = this;
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}

构造函数效果的模拟实现

完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是

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

也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 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 = 'kevin';

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

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin

注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,如果大家了解new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。

所以我们可以通过修改返回的函数的原型来实现,让我们写一下:

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

构造函数效果的优化实现

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype
这个时候,我们可以通过一个空函数来进行中转

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

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

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d

调用 bind 的不是函数咋办?
抛出错误

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

最终代码

所以最最后的代码就是:

Function.prototype.bind2 = 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.call(arguments, 1);

    var fNOP = function () {};

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

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

// 下面这样调用时 就会抛出错误
Function.prototype.bind2.call('test') // Uncaught Error: Function.prototype.bind - what is trying to be bound is not callable

在这里插入图片描述

细说 call、apply 以及 bind 的区别和用法

call 和 apply 的共同点

它们的共同点是,都能够改变函数执行时的上下文将一个对象的方法借给另一个对象来使用,并且立即执行

调用 call 和 apply 的对象,必须是一个函数 Function

call 和 apply 的区别

它们的区别,主要体现在参数的写法上。先来看一下它们各自的具体写法。

call的写法

Function.call(obj, a,b,c,...)

需要注意以下几点:

  • 调用 call 的对象,必须是个函数 Function
  • call 的第一个参数,是一个对象。 Function 的调用者,this将会指向这个对象。如果不传,则默认为全局对象 window。
  • 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
function func (a,b,c) {}

func.call(obj, 1,2,3)
// func 接收到的参数实际上是 1,2,3

func.call(obj, [1,2,3])
// func 接收到的参数实际上是 [1,2,3],undefined,undefined

apply 的写法

Function.apply(obj,argArray)

需要注意的是:

  • apply的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
func.apply(obj, [1,2,3])
// func 接收到的参数实际上是 1,2,3

func.apply(obj, {
    0: 1,
    1: 2,
    2: 3,
    length: 3
})
// func 接收到的参数实际上是 1,2,3
function bar () {
   console.log(arguments)  
   console.log(arguments instanceof Array)
 }

在这里插入图片描述

什么是类数组?

先说数组,这我们都熟悉。
它的特征有:

  • 可以通过角标调用,如 array[0];
  • 具有长度属性length;
  • 可以通过 for 循环或forEach方法,进行遍历。

那么,类数组是什么呢?顾名思义,就是具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组。

let arrayLike = {
    0: 1,
    1: 2,
    2: 3,
    length: 3
};

类数组 arrayLike

  • 可以通过角标进行调用,
  • 具有length属性,
  • 同时也可以通过 for 循环进行遍历。

类数组,还是比较常用的,只是我们平时可能没注意到。
比如,我们获取 DOM 节点的方法,返回的就是一个类数组
再比如,在一个方法中使用 arguments 获取到的所有参数,也是一个类数组。

但是需要注意的是:类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。

function bar () {
   console.log(arguments)  
   console.log(arguments instanceof Array) // false
 }
 bar(1,2,3)

在这里插入图片描述

call 和 apply 的用途

下面会分别列举 call 和 apply 的一些使用场景。

call 的使用场景

  • 对象的继承。如下面这个例子:
function superClass () {
    this.a = 1;
    this.print = function () {
        console.log(this.a);
    }
}

function subClass () {
    superClass.call(this);
    this.print();
}

subClass();
// 1

subClass通过 call 方法继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。

  • 借用方法。还记得刚才的类数组么?如果它想使用Array 原型链上的方法,可以这样:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
// 上面会把类数组转化为真数组

这样,domNodes 就可以应用 Array 下的所有方法了。

apply 的一些妙用

  • Math.max()。用它来获取数组中最大的一项。
const array = [1,5,2,10]
let max = Math.max.apply(null, array)
console.log(max) // 10

同理,要获取数组中最小的一项,可以这样:

const array = [1,5,2,10]
let min = Math.min.apply(null, array)
console.log(min) // 1

bind 的使用

最后来说说 bind。
在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。
并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

Function.bind(obj, a, b, ...)

bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 apply 和 call 则是立即调用。

来看下面这个例子:

function add (a, b) {
    return a + b;
}

function sub (a, b) {
    return a - b;
}

add.bind(sub, 5, 3); // 这时,并不会返回 8
add.bind(sub, 5, 3)(); // 调用后,返回 8

如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。

总结

call 和 apply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。

bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。

知乎上有人分享的,关于 call 和 apply 的便捷记忆法:

猫吃鱼,狗吃肉,奥特曼打小怪兽。 有天狗想吃鱼了

猫.吃鱼.call(狗,鱼)

狗就吃到鱼了

猫成精了,想打怪兽

奥特曼.打小怪兽.call(猫,小怪兽)

猫也可以打小怪兽了

推荐阅读

连点成线

文章转载于
JavaScript深入之bind的模拟实现
「干货」细说 call、apply 以及 bind 的区别和用法


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值