资讯专栏INFORMATION COLUMN

javascript原生一步步实现bind分析

coordinate35 / 601人阅读

摘要:绑定函数被调用时,也接受预设的参数提供给原函数。原型链官方文档上有一句话说明绑定过后的函数被实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的被忽略继承原函数的对象,但是参数还是会使用。

bind 官方描述

bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

使用介绍

由于javascript中作用域是由其运行时候所处的环境决定的,所以往往函数定义和实际运行的时候所处环境不一样,那么作用域也会发生相应的变化。
例如下面这个情况:

var id = "window";
//定义一个函数,但是不立即执行
var test = function(){
    console.log(this.id)
}
test() // window
//把test作为参数传递
var obj = {
    id:"obj",
    hehe:test
}
//此时test函数运行环境发生了改变
obj.hehe() // "obj"
//为了避免这种情况,javascript里面有一个bind方法可以在函数运行之前就绑定其作用域,修改如下

var id = "window";
var test = function(){
    console.log(this.id)
}.bind(window)
var obj = {
    id:"obj",
    hehe:test
}
test() // window
obj.hehe() // window

上面介绍了bind方法的一个重要作用就是为一个函数绑定作用域,但是bind方法在低版本浏览器不兼容,这里我们可以手动实现一下。

拆分一下关键思路

因为bind方法不会立即执行函数,需要返回一个待执行的函数(这里用到闭包,可以返回一个函数)return function(){}

作用域绑定,这里可以使用apply或者call方法来实现 xx.call(yy)/xx.apply(yy)

参数传递,由于参数的不确定性,需要用apply传递数组(实例更明了xx.apply(yy,[...Array...]),如果用call就不太方便了,因为call后面的参数需要一个个列出来

实现

有了上述的思路,大致的雏形已经明了了,代码应该也很容易实现

绑定作用域,绑定传参
Function.prototype.testBind = function(that){
    var _this = this,
        /*
        *由于参数的不确定性,统一用arguments来处理,这里的arguments只是一个类数组对象,有length属性
        *可以用数组的slice方法转化成标准格式数组,除了作用域对象that以外,
        *后面的所有参数都需要作为数组参数传递
        *Array.prototype.slice.apply(arguments,[1])/Array.prototype.slice.call(arguments,1)
        */
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]);
    //返回函数    
    return function(){
        //apply绑定作用域,进行参数传递
        return _this.apply(that,args)
    }    
}

测试

var test = function(a,b){
    console.log("作用域绑定 "+ this.value)
    console.log("testBind参数传递 "+ a.value2)
    console.log("调用参数传递 " + b)
}
var obj = {
    value:"ok"
}
var fun_new = test.testBind(obj,{value2:"also ok"})

fun_new ("hello bind")
// 作用域绑定 ok
// testBind参数传递 also ok
// 调用参数传递  undefined
动态参数

上面已经实现了bind方法的作用域绑定,但是美中不足的是,既然我们返回的是一个函数,调用的时候应该支持传递参数,很显然,上面的 fun_new 调用的时候并不支持传参,只能在 testBind 绑定的时候传递参数,因为我们最终调用的是这个返回函数

function(){
        return _this.apply(that,args)
    }    

这里面的args在绑定的时候就已经确定了,调用的时候值已经固定,
我们并没有处理这个function传递的参数。

我们对其进行改造

return function(){
        return _this.apply(that,
            args.concat(Array.prototype.slice.apply(arguments,[0]))
        )
    }    

这里的 Array.prototype.slice.apply(arguments,[0]) 指的是这个返回函数执行的时候传递的一系列参数,所以是从第一个参数开始 [0] ,之前的args = slice.apply(arguments,[1])指的是 testBind方法执行时候传递的参数,所以从第二个开始 [1],两则有本质区别,不能搞混,只有两者合并了之后才是返回函数的完整参数

所以有如下实现

Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]);
    return function(){
        return _this.apply(that,
                    args.concat(Array.prototype.slice.apply(arguments,[0]))
                )
    }    
}

测试

var test = function(a,b){
    console.log("作用域绑定 "+ this.value)
    console.log("testBind参数传递 "+ a.value2)
    console.log("调用参数传递 " + b)
}
var obj = {
    value:"ok"
}
var fun_new = test.testBind(obj,{value2:"also ok"})

fun_new ("hello bind")
// 作用域绑定 ok
// testBind参数传递 also ok
// 调用参数传递  hello bind

在以上2种传参方式中,bind的优先级高,从 args.concat(Array.prototype.slice.apply(arguments,[0])) 也可以看出来,bind的参数在数组前面。

原型链

官方文档上有一句话:

A bound function may also be constructed using the new operator: doing
so acts as though the target function had instead been constructed.
The provided this value is ignored, while prepended arguments are
provided to the emulated function.

说明绑定过后的函数被new实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的this被忽略(继承原函数的this对象),但是参数还是会使用。
这里就需要一个中转函数把原型链传递下去

fNOP = function () {} //创建一个中转函数
fNOP.prototype = this.prototype;
xx.prototype = new fNOP() 
修改如下
Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]),
        fNOP = function () {},
        //所以调用官方bind方法之后 有一个name属性值为 "bound "
        bound = function(){
            return _this.apply(that,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }    

    fNOP.prototype = _this.prototype;

    bound.prototype = new fNOP();

    return bound;
}

而且bind方法的第一个参数this是可以不传的,需要分2种情况

直接调用bind之后的方法

var f = function () { console.log("不传默认为"+this)  };f.bind()()
// 不传默认为 Window 

所以直接调用绑定方法时候 apply(that, 建议改为 apply(that||window,,其实不改也可以,因为不传默认指向window

使用new实例化被绑定的方法

容易糊涂,重点在于弄清楚标准的bind方法在new的时候做的事情,然后就可以清晰的实现

这里我们需要看看 new 这个方法做了哪些操作 比如说 var a = new b()

创建一个空对象 a = {},并且this变量引用指向到这个空对象a

继承被实例化函数的原型 :a.__proto__ = b.prototype

被实例化方法bthis对象的属性和方法将被加入到这个新的 this 引用的对象中: b的属性和方法被加入的 a里面

新创建的对象由 this 所引用 :b.call(a)

通过以上可以得知,如果是var after_new = new bindFun(); 由于这种行为是把原函数当成构造器,那么那么最终实例化之后的对象 this需要继承自原函数, 而这里的 bindFun 目前是

function(){
            return _this.apply(that || window,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }    

这里apply的作用域是绑定的that || window,在执行 testBind()的时候就已经固定,并没有把原函数的this对象继承过来,不符合我们的要求,我们需要根据apply的特性解决这个问题:

在一个子构造函数中,你可以通过调用父构造函数的 `apply/call` 方法来实现继承

例如

function Product(name, price) {
  this.name = name;
  this.price = price;

  if (price < 0) {
    throw RangeError("Cannot create product " +
                      this.name + " with a negative price");
  }
}

function Food(name, price) {
  Product.call(this, name, price); 
  this.category = "food";
}

//等同于(其实就是把Product放在Food内部执行了一次)
function Food(name, price) { 
    this.name = name;
    this.price = price;
    if (price < 0) {
        throw RangeError("Cannot create product " +
                this.name + " with a negative price");
    }

    this.category = "food"; 
}

所以在new新的实例的时候实时将这个新的this对象 进行 apply 继承原函数的 this 对象,就可以达到 new 方法里面的第 3 步的结果

apply(that||window,
//修改为 如果是new的情况,需要绑定new之后的作用域,this指向新的实例对象
apply(isNew ? this : that||window,  ==>

Function.prototype.testBind = function(that){
    var _this = this,
        slice = Array.prototype.slice,
        args = slice.apply(arguments,[1]),
        fNOP = function () {},
        //所以调用官方bind方法之后 有一个name属性值为 "bound "
        bound = function(){
            return _this.apply(isNew ? this : that||window,
                args.concat(Array.prototype.slice.apply(arguments,[0]))
            )
        }    

    fNOP.prototype = _this.prototype;

    bound.prototype = new fNOP();

    return bound;
}

这里的 isNew 是区分 bindFun 是直接调用还是被 new 之后再调用,通过原型链的继承关系可以知道,
bindFun 属于 after_new的父类,所以 after_new instanceof bindFun 为 true,同时
bindFun.prototype = new fNOP() 原型继承; 所以 fNOP 也是 after_new的父类, after_new instanceof fNOP 为 true

最终结果
Function.prototype.testBind = function(that){
        var _this = this,
            slice = Array.prototype.slice,
            args = slice.apply(arguments,[1]),
            fNOP = function () {},
            bound = function(){
                //这里的this指的是调用时候的环境
                return _this.apply(this instanceof  fNOP ? this : that||window,
                    args.concat(Array.prototype.slice.apply(arguments,[0]))
                )
            }    
        fNOP.prototype = _this.prototype;
    
        bound.prototype = new fNOP();
    
        return bound;
    }

我看到有些地方写的是

this instanceof fNOP && that ? this : that || window,

我个人觉得这里有点不正确,如果绑定时候不传参数,那么that就为空,那无论怎样就只能绑定 window作用域了。

以上是个人见解,不对的地方望指导,谢谢!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80744.html

相关文章

  • 从源码步步学习,Ryan Dahl的Deno实现原理

    摘要:之父在中的设计错误演讲中表示不允许将任意本地函数绑定至当中。所有系统调用都将通过消息传递完成序列化。两项原生函数与。这既简化了设计流程,又使得系统更易于审计。 Node之父ry:在Node中的设计错误演讲中表示: 不允许将任意本地函数绑定至 V8 当中。 所有系统调用都将通过消息传递完成(protobuf 序列化)。 两项原生函数:send 与 recv。 这既简化了设计流程,又使得...

    goji 评论0 收藏0
  • JS原生步步实现前端路由和单页面应用

    摘要:这里借鉴了一下的处理方式,我们把单独模块的包装成一个函数,提供一个全局的回调方法,加载完成时候再调用回调函数。 前端路由实现之 #hash 先上github项目地址: spa-routers运行效果图showImg(https://segmentfault.com/img/bVFi7l?w=581&h=312); 背景介绍 用了许多前端框架来做spa应用,比如说backbone,ang...

    idealcn 评论0 收藏0
  • 学习 apply 和 call

    摘要:官方描述方法在指定值和参数参数以数组或类数组对象的形式存在的情况下调用某个函数。两者基本一致,只有一个区别,就是方法接受的是若干个参数的列表,而方法接受的是一个包含多个参数的数组。 Function.prototype.apply() & Function.prototype.call() 官方描述 apply() 方法在指定 this 值和参数(参数以数组或类数组对象的形式存在)的...

    gclove 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<