javascript原生一步步实现bind分析

数据库2025-11-05 10:55:2744288

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方法在低版本浏览器不兼容,这里我们可以手动实现一下。b2b信息网

拆分一下关键思路

因为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 被实例化方法b的this对象的属性和方法将被加入到这个新的 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作用域了。

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

本文地址:http://www.bhae.cn/news/981e21398805.html
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

全站热门

本文记录配置Linux服务器的初步流程,也就是系统安装完成后,下一步要做的事情。这主要是我自己的总结和备忘,假如有遗漏,欢迎大家补充。下面的操作针对Debian/Ubuntu系统,其他Linux系统也类似,就是部分命令稍有不同。 第一步:root用户登录 首先,使用root用户登录远程主机(假定IP地址是128.199.209.242)。 ssh root@128.199.209.242这时,命令行会出现警告,表示这是一个新的地址,存在安全风险。键入yes,表示接受。然后,就应该可以顺利登入远程主机。接着,修改root用户的密码。 passwd第二步:新建用户 首先,添加一个用户组(这里假定为admin用户组)。 addgroup admin然后,添加一个新用户(假定为bill)。useradd -d /home/bill -s /bin/bash -m bill 上面命令中,参数d指定用户的主目录,参数s指定用户的shell,参数m表示假如该目录不存在,则创建该目录。接着,设置新用户的密码。 passwd bill 将新用户(bill)添加到用户组(admin)。usermod -a -G admin bill 接着,为新用户设定sudo权限。visudovisudo命令会打开sudo设置文件/etc/sudoers,找到下面这一行。root ALL=(ALL:ALL) ALL在这一行的下面,再添加一行。root ALL=(ALL:ALL) ALLbill ALL=(ALL) NOPASSWD: ALL上面的NOPASSWD表示,切换sudo的时候,不需要输入密码,我喜欢这样比较省事。假如出于安全考虑,也可以强制要求输入密码。root ALL=(ALL:ALL) ALLbill ALL=(ALL:ALL) ALL然后,先退出root用户的登录,再用新用户的身份登录,检查到这一步为止,是否一切正常。exitssh bill@128.199.209.242第三步:SSH设置 首先,确定本机有SSH公钥(一般是文件~/.ssh/id_rsa.pub),假如没有的话,使用ssh-keygen命令生成一个(可参考我写的SSH教程)。 在本机上另开一个shell窗口,将本机的公钥拷贝到服务器的authorized_keys文件。 cat ~/.ssh/id_rsa.pub | ssh bill@128.199.209.242 mkdir -p .ssh && cat - >>~/.ssh/authorized_keys# 或者在服务器端,运行下面命令echo ssh-rsa [your public key] >~/.ssh/authorized_keys然后,进入服务器,编辑SSH配置文件/etc/ssh/sshd_config。sudo cp /etc/ssh/sshd_config ~sudo nano /etc/ssh/sshd_config在配置文件中,将SSH的默认端口22改掉,可以改成从1025到65536之间的任意一个整数(这里假定为25000)。Port 25000然后,检查几个设置是否设成下面这样,确保去除前面的#号。Protocol 2PermitRootLogin noPermitEmptyPasswords noPasswordAuthentication noRSAAuthentication yesPubkeyAuthentication yesAuthorizedKeysFile .ssh/authorized_keysUseDNS no上面主要是禁止root用户登录,以及禁止用密码方式登录。接着,在配置文件的末尾,指定允许登陆的用户。 AllowUsers bill保存后,退出文件编辑。接着,改变authorized_keys文件的权限。 sudo chmod 600 ~/.ssh/authorized_keys && chmod 700 ~/.ssh/然后,重启SSHD。sudo service ssh restart# 或者sudo /etc/init.d/ssh restart下面的一步是可选的。在本机~/.ssh文件夹下创建config文件,内容如下。Host s1HostName 128.199.209.242User billPort 25000最后,在本机另开一个shell窗口,测试SSH能否顺利登录。ssh s1第四步:运行环境配置 首先,检查服务器的区域设置。 locale假如结果不是en_US.UTF-8,建议都设成它。sudo locale-gen en_US en_US.UTF-8 en_CA.UTF-8sudo dpkg-reconfigure locales然后,更新软件。sudo apt-get updatesudo apt-get upgrade最后,再根据需要,做一些安全设置,比如搭建防火墙,关闭HTTP、HTTPs、SSH以外的端口,这里就不一一介绍了,谢谢阅读,希望能帮到大家,请继续关注脚本之家,我们会努力分享更多优秀的文章。

荣耀5A移动版(荣耀5A移动版的关键特点及用户评价)

BoseM2耳机(探索无与伦比的音乐体验,尽在BoseM2耳机)

探索Folder2的功能与优势(解析Folder2的关键特性及其在文件管理中的应用)

本文向大家介绍一些我在 Ubuntu 15.10 安装之后所做的几项配置。1.了解新功能特性Ubuntu 15.10 Wily Werewolf 虽然不是一个颠覆性的功能更新版本,但还是带来了不少的功能特性和用户体验改进,之前的 Ubuntu 15.10 新特性文章 中已经有过介绍,这里不再赘述。2.更新系统虽然是才发布的最新 Ubuntu 15.10 版本,但在安装好之后还是有些系统更新和软件更新需要进行的,这在 Linux 世界不足为奇。我们可以在软件更新器中直接点击立即安装进行更新。3.安装Linux显卡驱动要获得 Ubuntu 更好的显示性能就必需安装好适配的显卡驱动程序,以让 Ubuntu 桌面能够平稳运行,在安装好 Linux 显卡驱动后也可以让你在游戏和多媒体应用中获得最佳的帧速率。Ubuntu 目前已支持大多数图形硬件的开源驱动,要为 Ubuntu 15.10 安装驱动程序可以在软件和更新—附加驱动中进行安装。4.安装媒体编解码器受限于法律限制 Ubuntu 无法直接集成 MP3 等流行的媒体编解码器以让用户开箱即用,不过这个问题很好解决,我们可以直接在 Ubuntu 软件中心中搜索 Ubuntu restricted extras 进行安装。5.调整菜单当我们在 Ubuntu 15.10 中打开应用或窗口之后,默认在顶部都会显示菜单(几秒)之后便会自动隐藏,当我们将鼠标焦点再移到顶部栏时菜单才会再次出现。该功能可以节省显示空间,以减少对用户的操作干扰。假如你不喜欢此功能,可以在系统设置—外观—中调整到在窗口的标题栏,将菜单栏调整到应用窗口显示,而不在顶部栏显示。6.处理Flash问题虽然互联网上掀起了抛弃 Flash 的浪潮,但还是有些网站需要使用到 Flash 的。但 Adobe 早在几年前便宣布了停止对 Linux 的 Flash 支持,许多浏览器都宣布要放弃 NPAPI 架构。目前来说,我比较推荐大家使用 Google Chrome 浏览器,因为其最新版本可以很好的处理 Flash 插件问题。7.换一个好看的皮肤由于 Ubuntu 几年来的默认主题都没太大变化,给人的感觉总是没太多变化和新意,这让我们的 Ubuntu 显示比较单调。因此,在装好 Ubuntu 15.10 之后换一个好看点的主题也是一项比较建议的操作。假如你只想更改 Ubuntu 15.10 桌面背景的话,可以到系统设置—外观中进行更改。8.管理隐私当我们在 Unity Dash 中搜索文件时,总会出现应用程序、在线结果和重要文件的相关数据。假如不希望包含个人信息的数据出现在搜索结果中,我们可以选择将其关闭。在系统设置—安全和隐私的文件和应用选择卡中选择关闭并去掉要禁止搜索结果中显示的数据类型即可。假如不想在 Unity Dash 显示在线搜索结果,只需在搜索选项卡中禁用在线搜索结果即可。9.安装所需的应用Ubuntu 15.10 虽然为我们内置了大名鼎鼎的 LibreOffice 和 Firefox 等应用,但这些应用也许不是你最钟爱和常用的。Ubuntu 软件中心为我们提供了数量众多的免费应用,其中就包括:◆Steam – 游戏分销平台◆GIMP – 好用的图像编辑器◆VLC – 国际上非常流行的媒体播放器◆Geary – 桌面电子邮件客户端◆Chromium – 来自 Google 的开源 Web 浏览器◆Skype – 这个不用我介绍吧当然,我个人比较推荐的一些软件如下(不包括在Ubuntu 软件中心):◆Google Chrome – 这个不用介绍◆Spotify for Linux – Linux 下流行的流媒体音乐服务◆Corebird – Linux 桌面 Twitter 客户端◆Lightwork – 业非线视频编辑器◆Viber – Skype的替代方案◆Vivaldi – 面向开发人员的 Web 浏览器10.安装搜狗输入法由 Ubuntu Kylin 团队与搜狗公司联合开发的搜狗Linux 输入法2.0 版本版于 2015 年10 月16 日正式发布,具有支持双拼、智能拼音、自动匹配、多样化皮肤支持等功能。以上就是Ubuntu 15.10安装之后需要做的十件事,希望对大家的学习有所帮助。

IC技术支持的重要性与应用(提升生产效率和产品质量的关键服务)

西部数据4T硬盘的卓越表现与性能特点(高容量存储方案助力数据管理和备份安全)

i3盒装风扇的性能与使用体验(高效散热、低噪音,i3盒装风扇为您带来舒适的使用体验)

友情链接

滇ICP备2023000592号-9