javascript原生一步步实现bind分析

简介:


bind

官方描述

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

使用介绍

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

例如下面这个情况:


 
 
  1. var id = 'window'
  2. //定义一个函数,但是不立即执行 
  3. var test = function(){ 
  4.     console.log(this.id) 
  5. test() // window 
  6. //把test作为参数传递 
  7. var obj = { 
  8.     id:'obj'
  9.     hehe:test 
  10. //此时test函数运行环境发生了改变 
  11. obj.hehe() // 'obj' 
  12. //为了避免这种情况,javascript里面有一个bind方法可以在函数运行之前就绑定其作用域,修改如下 
  13.  
  14. var id = 'window'
  15. var test = function(){ 
  16.     console.log(this.id) 
  17. }.bind(window) 
  18. var obj = { 
  19.     id:'obj'
  20.     hehe:test 
  21. test() // window 
  22. obj.hehe() // window  

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

拆分一下关键思路

  1. 因为bind方法不会立即执行函数,需要返回一个待执行的函数(这里用到闭包,可以返回一个函数)return function(){}
  2. 作用域绑定,这里可以使用apply或者call方法来实现 xx.call(yy)/xx.apply(yy)
  3. 参数传递,由于参数的不确定性,需要用apply传递数组(实例更明了)xx.apply(yy,[...Array...]),如果用call就不太方便了,因为call后面的参数需要一个个列出来

实现

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

绑定作用域,绑定传参


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

测试


 
 
  1. var test = function(a,b){ 
  2.     console.log('作用域绑定 '+ this.value) 
  3.     console.log('testBind参数传递 '+ a.value2) 
  4.     console.log('调用参数传递 ' + b) 
  5. var obj = { 
  6.     value:'ok' 
  7. var fun_new = test.testBind(obj,{value2:'also ok'}) 
  8.  
  9. fun_new ('hello bind'
  10. // 作用域绑定 ok 
  11. // testBind参数传递 also ok 
  12. // 调用参数传递  undefined  

动态参数

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


 
 
  1. function(){ 
  2.         return _this.apply(that,args) 
  3.     }     
  4.  
  5. 这里面的args在绑定的时候就已经确定了,调用的时候值已经固定, 
  6. 我们并没有处理这个function传递的参数。  

我们对其进行改造


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

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

所以有如下实现


 
 
  1. Function.prototype.testBind = function(that){ 
  2.     var _this = this, 
  3.         slice = Array.prototype.slice, 
  4.         args = slice.apply(arguments,[1]); 
  5.     return function(){ 
  6.         return _this.apply(that, 
  7.                     args.concat(Array.prototype.slice.apply(arguments,[0])) 
  8.                 ) 
  9.     }     
  10. }  

测试


 
 
  1. var test = function(a,b){ 
  2.     console.log('作用域绑定 '+ this.value) 
  3.     console.log('testBind参数传递 '+ a.value2) 
  4.     console.log('调用参数传递 ' + b) 
  5. var obj = { 
  6.     value:'ok' 
  7. var fun_new = test.testBind(obj,{value2:'also ok'}) 
  8.  
  9. fun_new ('hello bind'
  10. // 作用域绑定 ok 
  11. // testBind参数传递 also ok 
  12. // 调用参数传递  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对象),但是参数还是会使用。

这里就需要一个中转函数把原型链传递下去


 
 
  1. fNOP = function () {} //创建一个中转函数 
  2. fNOP.prototype = this.prototype; 
  3. xx.prototype = new fNOP()  
  4. 修改如下 
  5. Function.prototype.testBind = function(that){ 
  6.     var _this = this, 
  7.         slice = Array.prototype.slice, 
  8.         args = slice.apply(arguments,[1]), 
  9.         fNOP = function () {}, 
  10.         //所以调用官方bind方法之后 有一个name属性值为 'bound ' 
  11.         bound = function(){ 
  12.             return _this.apply(that, 
  13.                 args.concat(Array.prototype.slice.apply(arguments,[0])) 
  14.             ) 
  15.         }     
  16.  
  17.     fNOP.prototype = _this.prototype; 
  18.  
  19.     bound.prototype = new fNOP(); 
  20.  
  21.     return bound; 
  22. }  

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

  • 直接调用bind之后的方法

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

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

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

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

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

  1. 创建一个空对象 a = {},并且this变量引用指向到这个空对象a
  2. 继承被实例化函数的原型 :a.__proto__ = b.prototype
  3. 被实例化方法b的this对象的属性和方法将被加入到这个新的 this 引用的对象中: b的属性和方法被加入的 a里面
  4. 新创建的对象由 this 所引用 :b.call(a)

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


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

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

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

例如


 
 
  1. function Product(name, price) { 
  2.   this.name = name
  3.   this.price = price; 
  4.  
  5.   if (price < 0) { 
  6.     throw RangeError('Cannot create product ' + 
  7.                       this.name + ' with a negative price'); 
  8.   } 
  9.  
  10. function Food(name, price) { 
  11.   Product.call(this, name, price);  
  12.   this.category = 'food'
  13.  
  14. //等同于(其实就是把Product放在Food内部执行了一次) 
  15. function Food(name, price) {  
  16.     this.name = name
  17.     this.price = price; 
  18.     if (price < 0) { 
  19.         throw RangeError('Cannot create product ' + 
  20.                 this.name + ' with a negative price'); 
  21.     } 
  22.  
  23.     this.category = 'food';  
  24. }  

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


 
 
  1. apply(that||window, 
  2. //修改为 如果是new的情况,需要绑定new之后的作用域,this指向新的实例对象 
  3. apply(isNew ? this : that||window,  ==> 
  4.  
  5. Function.prototype.testBind = function(that){ 
  6.     var _this = this, 
  7.         slice = Array.prototype.slice, 
  8.         args = slice.apply(arguments,[1]), 
  9.         fNOP = function () {}, 
  10.         //所以调用官方bind方法之后 有一个name属性值为 'bound ' 
  11.         bound = function(){ 
  12.             return _this.apply(isNew ? this : that||window, 
  13.                 args.concat(Array.prototype.slice.apply(arguments,[0])) 
  14.             ) 
  15.         }     
  16.  
  17.     fNOP.prototype = _this.prototype; 
  18.  
  19.     bound.prototype = new fNOP(); 
  20.  
  21.     return bound; 
  22. }  

这里的 isNew 是区分 bindFun 是直接调用还是被 new 之后再调用,通过原型链的继承关系可以知道,

bindFun 属于 after_new的父类,所以 after_new instanceof bindFun 为 true,同时

bindFun.prototype = new fNOP() 原型继承; 所以 fNOP 也是 after_new的父类, after_new instanceof fNOP 为 true

最终结果


 
 
  1. Function.prototype.testBind = function(that){ 
  2.         var _this = this, 
  3.             slice = Array.prototype.slice, 
  4.             args = slice.apply(arguments,[1]), 
  5.             fNOP = function () {}, 
  6.             bound = function(){ 
  7.                 //这里的this指的是调用时候的环境 
  8.                 return _this.apply(this instanceof  fNOP ? this : that||window, 
  9.                     args.concat(Array.prototype.slice.apply(arguments,[0])) 
  10.                 ) 
  11.             }     
  12.         fNOP.prototype = _this.prototype; 
  13.      
  14.         bound.prototype = new fNOP(); 
  15.      
  16.         return bound; 
  17.     }  

我看到有些地方写的是


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

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

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


作者:谁谁

来源:51CTO

相关文章
|
1月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
4月前
|
前端开发 JavaScript 开发者
揭秘JavaScript魔法三剑客:call、apply、bind,解锁函数新世界,你的前端之路因它们而精彩!
【8月更文挑战第23天】在 JavaScript 的世界里,`call`、`apply` 和 `bind` 这三个方法常常让新手感到困惑。它们都能改变函数执行时的上下文(即 `this` 的指向),但各有特点:`call` 接受一系列参数并直接调用函数;`apply` 则接收一个参数数组,在处理不确定数量的参数时特别有用;而 `bind` 不会立即执行函数,而是创建一个新版本的函数,其 `this` 上下文已被永久绑定。理解这三个方法能帮助开发者更好地运用函数式编程技巧,提升代码灵活性和可维护性。
40 0
|
19天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
38 3
|
23天前
|
JavaScript 前端开发 安全
JavaScript与TypeScript的对比,分析了两者的特性及在实际项目中的应用选择
本文深入探讨了JavaScript与TypeScript的对比,分析了两者的特性及在实际项目中的应用选择。JavaScript以其灵活性和广泛的生态支持著称,而TypeScript通过引入静态类型系统,提高了代码的可靠性和可维护性,特别适合大型项目。文章还讨论了结合使用两种语言的优势,以及如何根据项目需求和技术背景做出最佳选择。
42 4
|
28天前
|
JavaScript 前端开发 API
Vue.js与Angular的优劣分析
Vue.js和Angular都是非常流行的JavaScript框架,它们在构建现代Web应用程序方面各有优劣
|
26天前
|
JavaScript 前端开发
js中的bind,call,apply方法的区别以及用法
JavaScript中,`bind`、`call`和`apply`均可改变函数的`this`指向并传递参数。其中,`bind`返回一个新函数,不立即执行;`call`和`apply`则立即执行,且`apply`的参数以数组形式传递。三者在改变`this`指向及传参上功能相似,但在执行时机和参数传递方式上有所区别。
25 1
|
1月前
|
运维 监控 JavaScript
鸿蒙next版开发:分析JS Crash(进程崩溃)
在HarmonyOS 5.0中,JS Crash指未处理的JavaScript异常导致应用意外退出。本文详细介绍如何分析JS Crash,包括异常捕获、日志分析和典型案例,帮助开发者定位问题、修复错误,提升应用稳定性。通过DevEco Studio收集日志,结合HiChecker工具,有效解决JS Crash问题。
49 4
|
1月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
220 9
|
1月前
|
移动开发 前端开发 JavaScript
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
于辰在大学期间带领团队参考网易游戏官网的部分游戏页面,开发了一系列前端实训作品。项目包括首页、2021校园招聘页面和明日之后游戏页面,涉及多种特效实现,如动态图片切换和人物聚合效果。作品源码已上传至CSDN,视频效果可在CSDN预览。
33 0
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
|
1月前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
45 1