模拟实现ES5中原生的bind函数

xiaoxiao2021-02-28  36

模拟原生bind


看到这个题目,首先要明确bind的用法,知道自己要完成一个什么样的目标。

1. 对于bind,大家都知道其可以改变this的指向。既然说到this,就回顾一下this的几种使用场景:

I. 作为构造函数执行(在执行构造函数时,this <- {}, this.__proto__ <- 构造函数.prototype,执行构造函数中的代码(如this.属性名 <- 属性值), return this。这里需要注意的是,在构造函数中,如果没有显式return表达式,则返回this,如果return后面跟的是NumberStringBooleannullundefined,则忽略,返回值仍为this,如果return后面是一个对象,则返回该对象) II. 作为对象属性执行:方法调用(this就指向该对象) III. 作为普通函数执行 :函数调用(this<-window) IV. call、apply、bind 这三种方法都能改变this的指向 Foo.method = function(){ alert(this) //function Foo(){} function test(){ alert(this) //window } test() }

2. bind的语法:

fun.bind(thisArg[,arg1[,arg2[,...]]])//[]为可选项

bind方法会创建一个新的函数,这个新函数的this是bind的第一个参数,后续的参数是这个新函数的参数。

3.bind返回的函数也能使用new操作符创建对象,这种行为就像把原函数当作构造器,忽略了bind指定的this值,同时调用时的参数被提供给新函数

实现方法


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

上面代码做到了改变this指向和bind的语法这两条,但是第三个目标没有实现。这里提一句,一定要搞清楚每个函数的用法,输入,输出,还有其自身特别的地方,比如这里的new 创建bind返回的函数,原先绑定的this失效。否则,这就不叫模拟!!!

上面这种方法其实是javascript语言精粹里面提到的柯里化(curry):把函数与传递给它的参数相结合,产生出一个新的函数。

加分项(1)


这是在一篇博客里看到的,里面提到上面的实现,是一个典型的”Monkey patching(猴子补丁) ” 即“给内置对象扩展方法”,也就是属性在运行时的动态替换。注意:一个错误特性被经常使用,那就是扩展object.prototype或者其他内置类型的原型对象,这样的monkey patching会破坏封装,虽然它被广泛的应用到一些JavaScript类库中比如Prototype,但为内置类型添加一些非标准的函数并不是一个好主意。扩展内置类型的唯一理由是为了和新的JavaScript保持一致,比如Array.forEach。所以,在这里可以进行一下“嗅探”,进行兼容处理。

Function.prototype.bind = Function.prototype.bind || function(context){ ... }

加分项(2)


需要注意:调用bind方法的一定是一个函数,所以可以在函数内部做一个判断:

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

写到这里,自我感觉还不错,回头看看我们的目标,咦,漏掉了第三条。。

要实现bind返回的绑定函数也能使用new操作符创建对象,就像把原函数当作构造器,之前提供的this值被忽略。要完成这个目标,首先还是要搞清楚new操作符的一系列动作(见文章顶部)。思考一下new的第二步操作,this.proto <- 构造函数.prototype,在我们的代码里,构造函数就是return的函数,所以我们应该把之前bind返回的函数的prototype属性赋值为原函数的prototype(在javaScript中所有函数都有prototype属性,网上以为细心的同学发现原生bind返回的函数没有prototype属性)。实现代码如下:

Function.prototype.bind = function(context){ var slice = Array.prototype.slice; var args = slice.apply(arguments); var me = this; var bound = function(){ var thisArgs = slice.apply(arguments) return me.apply(context, args.slice(1).concat(thisArgs)); } bound.prototype = this.prototype; return bound; };

接着还需要考虑,怎么判断这个函数是被new的还是被直接调用的,这需要在bound里面判断,如果this是原函数的实例,则该函数作为构造函数使用,否则apply的第一个参数仍为context。


补充知识


如何判断一个对象是不是某函数的实例呢?这就得从原型链说起,下面简单来一张原型链的示意图来说明一下:

我们都知道instanceof用于判断引用类型属于哪个构造函数: 例如objF instanceof Foo的判断逻辑: 从objF的proto开始一层一层往上找,看能否找到Foo.prototype.

objF.__proto__ === Foo.prototype //true objF.__proto__.__proto__ === Object.prototype //true objF instanceof Object //true

话不多说,继续探讨上面未完的问题,想要知道this是不是原函数的实例,也就是从this._proto_开始一层一层网上找,看能不能找到原函数.prototype.因此,我们的代码修正为:

Function.prototype.bind = function(context){ var slice = Array.prototype.slice; var args = slice.apply(arguments); var me = this; var tempProto = this.prototype var bound = function(){ var thisArgs = slice.apply(arguments) return me.apply(this.__proto__ === tempProto ? this:context, args.slice(1).concat(thisArgs)); } bound.prototype = this.prototype; return bound; };

经测试,以上代码确实达到了目标三。

Function.prototype.bind = function(context){ var slice = Array.prototype.slice; var args = slice.apply(arguments); var me = this; var tempProto = this.prototype var bound = function(){ var thisArgs = slice.apply(arguments) alert(this.__proto__ === tempProto) return me.apply(this.__proto__ === tempProto ? this:context, args.slice(1).concat(thisArgs)); } bound.prototype = this.prototype; return bound; }; function fn1(name){ alert(name) console.log(this) } var fn2 = fn1.bind({x:2}) fn2("summer") //输出:false,summer,{x:2} var obj = new fn2('summer') //输出:true,summer,bound{} typeof obj //输出:"object" obj instanceof fn1 //输出:true obj instanceof Function //输出:false obj instanceof Object //输出:true obj.__proto__ === fn1.prototype //true

到此为止,实现了最开始说的三个目标。对比一下简书里某大神的答案,发觉自己写的代码健壮性不行,也不美观,一些情况没有考虑到。 比如,context为null或者undefined,这种情况下返回this,也就是全局变量window。这里目前我还不太理解,我还是觉得没必要,因为巨酸context是null或者undefined,那么bound里return的第一个参数为null或者undefined不就行了么,也不报语法错误。。加上this指向全局变量也行。。

除此之外,和大神的代码相差最大的就是中间那一段

F = function(){}; F.prototype = this.prototype; ... this instanceof F

我觉得这样写的目的也就是代码更美观了,其实我们的目的是一样的,可能一般很少会直接把this.prototype存到一个变量,所以添加一个构造函数,让该构造函数来承接原函数的prototype属性。在代码return的上一行,我直接从this.prototype里取值复制给Bound.prototype,大神说是这样会污染this原型,至于怎么算是污染,我还不懂,以后明白了,再来补充吧!最后把完整的代码贴上来:

Function.prototype.bind = Function.prototype.bin || function(context){ if (typeof this !== "function") { throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var slice = Array.prototype.slice; var args = slice.apply(arguments); var me = this; var F = function(){}; F.prototype = this.prototype; var bound = function(){ var thisArgs = slice.apply(arguments) return me.apply(this instanceof F ? this : context || this, args.slice(1).concat(thisArgs)); } bound.prototype = new F(); return bound; };

以上就是对实现bind函数的一些思考,有哪些理解错误的东西,还望留言批评指正,共同学习☺(●’◡’●)

这里还有一个进阶的问题,不使用apply,call实现bind,这里先mark一下。先贴上一个大神的解决方案http://qdxmq.com/2017/05/02/一道面试题:不用call和apply方法用原生JavaScript模拟实现ES5的bind方法/

参考资源


我可能看了假源码:http://www.jianshu.com/p/6958f99db769JavaScript秘密花园:http://bonsaiden.github.io/JavaScript-Garden/zh/观V8源码中的array.js,解析Array.prototype.slice为什么能将类数组对象转为真正的数组:http://www.cnblogs.com/henryli/p/3700945.html
转载请注明原文地址: https://www.6miu.com/read-250237.html

最新回复(0)