Javascript bind 详细解析

3,237 阅读7分钟

写在前面

上一篇我们对于 apply 和 call 进行了一波详细解析,相信大家对于改变JavaScript的作用域使用 apply 和 call 完全没有问题了。那为什么已经有了 apply 和 call,为什么还要有 bind 呢。那下面就来和大家一起来探索一下bind。

bind 解释

Function.prototype.bind()

MDN中的解释:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

MDN 例子

const module = {
  x: 42,
  getX: function() {
    return this.x;
  }
};

const unboundGetX = module.getX;
console.log(unboundGetX());     // global scope
// expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42

上面这个例子就完美解释了bind的常规作用。先定义了一个module对象,里面有x和getX两个属性。然后将module对象的getX属性赋值给了unboundGetX。然后在全局调用了unboundGetX方法。注意谁调用this指向谁,这里unboundGetX在全局调用了这个方法,这里this就是全局window对象,全局并没有x属性,于是得到的是一个undefined。

下面使用bind方法改变了unboundGetX执行环境的作用域为module,这时候在执行boundGetX,这里this在module环境中,x则为42

在 MDN 中的其他例子,大家可以每个都深入看下,最好都手敲一遍,执行一下,会加深大家的理解的。

通过bind快捷调用

这个通过bind快捷调用,在看各类源码的时候,大家都会经常发现类似这种的写法,所以这里给大家分析一下

你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组

// 类数组对象
var arguments = {0:"a", 1:"b",2:"c", length:3}
var slice = Array.prototype.slice;
// 这里传入的arguments对象,是slice需要执行的作用域环境
var arr = slice.apply(arguments);

console.log(arr)        // ["a", "b", "c"]

上面这个 apply 或者 call 都可以实现同样效果,因为只是传了arguments作为作用域传入,并没有其他执行的函数进去

var arguments = {0:"a", 1:"b",2:"c", length:3}
arguments.__proto__.slice = Array.prototype.slice
arguments.slice()   // ["a", "b", "c"]

实际上这里类数组变成对象完全都是因为slice的设计方式上的问题,这要在继续分析就需要分析一下V8的源码了,我们暂时不继续展开,大家就上面这段代码理解下,知道实际上改变作用域环境实际上只是为了给arguments增加了一个数组才有的slice方法,只有有了这个slice方法,才能类数组对象变成数组

用 bind() 实现可以将这个

var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);

slice(arguments);

这里我们知道了这样可以实现,具体分析,我们先需要看下bind的具体实现原理

实现原理

先拆分一下实现一个bind 的关键思路

  • bind方法不是立即执行函数,所以需要返回一个待执行的函数。我们可以通过闭包的方式,返回一个函数( return function(){} )
  • bind实现了改变方法的作用域,这里我们可以通过call和apply来实现
  • 参数传递,因为bind可以在绑定方法时传递参数,也可以在调用bind之后返回的方法中进行传参。所以我们最好还是用apply了

下面我们通过几步来实现一个完整的bind

第一步 bind v1.0.0

实现作用域改变
Function.prototype.bind1 = function (context) {
    var self = this
    return function () {
        return self.apply(context)
    }
}
测试
function test () {
    console.log(this.value)
}
let obj = {value: 1}
let new_bind = test.bind(obj)
new_bind()      // 1
解析

这里稍微解释一下,new_bind通过执行test.bind(obj)返回的是一个待执行的方法

function () {
    return self.apply(context)
}

然后调用new_bind执行这个方法的时候,我们执行的是返回的function。这里通过self将test的this保存成了self,实际就是test.apply(obj),也即是将test方法执行的作用域指向了obj。所以调用后,拿到this.value就是obj的value

第二步 bind v1.0.1

实现参数传递
Function.prototype.bind2 = function(context) {
    var self = this
    // 截取第一个之后的所有参数
    // arguments是一个类数组的对象,有length属性
    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))
    }
}
测试
function test(name, age) {
    console.log(this.value)
    console.log(name)
    console.log(age)
}
let obj = { value: 1 }
let new_bind = test.bind2(obj)
new_bind(18)
解析

这一步增加了参数传递,在bind时可以传入参数,同时执行bind之后返回的方法也可以传入参数。一开始执行bind时候,传入参数除了需要绑定的对象外,后面可以加上参数,方法中args就是bind时候传入的。bindArgs则是在调用bind返回后方法传入的,然后在apply时候,将两个参数合并

第三步 bind v1.0.2

关于bind之后的函数被new实例化

注: bind返回的新函数也能使用new操作符创建对象,这就相当于把bind之后的函数当作构造函数,实例后的函数需要继承原函数的原型链方法,并且绑定过程中的this被忽略,但是参数仍然会使用

// 这里我们增加了一个temp中转函数
// 帮助我们如果要实例化对象时,将原型链传递下去
Function.prototype.bind3 = function(context) {
    var self = this,
        slice = Array.prototype.slice,
        args = slice.call(arguments, 1),
        temp = function () {},
        bound = function () {
            return self.apply(context, args.concat(slice.call(arguments))
        }
    temp.prototype = self.prototype;
    bound.prototype = new temp()
    
    return bound;
}

使用new实例化被绑定的方法 这里不是很好理解,前面有一篇文章,我们曾经分析过new这个方法在执行的过程中干了什么,大家想详细了解可以去找下看一下。我们这里就简单在复习一下。 比如 var foo = new Foo()

// 1.创建新对象
var obj = {}
// 2.将构造函数的上的prototype属性都复制一份给obj
obj.__proto__ = Foo.prototype
// 3.将构造函数Foo的this指向当前创建的对象
Foo.call(obj)
// 4.对于构造函数可能会return对象问题做了个处理
return typeof result === 'object' ? result : obj

通过上面这个new的简单分析可以得知,如果 var bind_new = new bindFun() 这里是把原函数当作构造器,那么最终实例化后的对象this需要继承原函数。目前bindFun是

function () {
    return self.apply(context || window, args.contact(Array.prototype.slice.apply(arguments,[0])))
}

这里我们apply执行的作用域是 context || window , 这在我们一开始bind3执行的时候就已经将作用域固定了,并没有把原函数的this对象继承过来。这里我们需要通过apply或者call实现一个小继承

继承详解
function Product (name,price) {
    this.name = name
    this.price = price
}
function Food (name, price) {
    Product.call(this,name,price)
    this.category = 'Food'
}

// 上面实现的继承,就是Food继承了Product,就相当于在Food内执行了一遍product,等同于下面

function Food (name, price) {
    this.name = name
    this.price = price
    this.category = 'Food'
}
// 所以apply或者call实现的继承是不能继承原型链上的内容的
// 这部分继承知识也在之前一篇文章分析了,大家也可以去捞一捞

所以最终,我们要实现的结果就是下面这样

Function.prototype.bind4 = function(context) {
    // 先判断this是不是函数,bind不是函数就要抛出异常
    if(typeof this !== 'function'){
        throw new Error('this must be function');
    }
    // 将执行bind的this存在self中
    var self = this,
        // 将数组的slice方法给slice
        slice = Array.prototype.slice,
        // 截取除了第一个后面的参数并变成数组
        args = slice.apply(arguments,[1]),
        // 临时方法,用于继承原函数
        temp = function() {},
        //最后返回的函数
        bound = function () {
            // 这里进行了一个this判断
            // 判断是不是使用了new来使用bind返回后的函数
            // 如果用了new 则 this instanceof 为 true
            // 如果是直接执行bind后方法则是直接是 context
            return self.apply(this instanceof temp ? this : context || window,
            args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }
    // 将调用bind方法的方法的原型对象属性赋值给临时对象的prototype
    temp.prototype = self.prototype
    // 将需要返回出去的待执行方法原型对象和temp通过原型链绑定
    bound.prototype = new temp()
    
    return bound
}

例子

下面通过几个简单例子在讲解一下

var obj = {
    value:1
};
function bar(name,age){
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age)
}
bar.prototype.mother = 'chencc';

var bindFoo = bar.bind(obj,'jack');

// 直接执行改变作用域绑定后的 bindFoo
bindFoo(19)     // 1 jack 19

// bindFoo 是什么
// bindFoo就是上面要返回出来的 bound

// bindFoo.prototype.__proto__ === bar.prototype
// 这里就相当于上面的 
// temp.prototype = self.prototype
// bound.prototype = new temp()
// bound.prototype.__proto__ === temp.prototype

var foo = new bindFoo();
// 将bind返回之后的方法当作构造器,进行实例化
// foo 就是 bound 的子类
// foo.__proto__ === bindFoo.prototype
// bindFoo.prototype.__proto__ === bar.prototype

总结

写到这里 bind 的实现原理就大概分析完了,bind的实现原理以及应用上,融合了我们前面写的call和apply解析,JavaScript在new的时候干了什么以及继承中构造器继承一些相关的东西。所以学习新的知识的时候,从来不是割裂的,而是相互都有关系的,希望大家在学习的时候,注意这些。一起提高