bind函数作用、应用场景以及模拟实现
bind函数
bind 函数挂在 Function 的原型上
Function.prototype.bind
创建的函数都可以直接调用 bind,使用:
function func(){
console.log(this)
}
func.bind(); // 用函数来调用
bind 的作用:
bind() 方法调用后会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为新函数运行时的 this的值,之后的序列参数将会在传递的实参前传入作为新函数的参数。<MDN>
bind 接收的参数
func.bind(thisArg[,arg1,arg2...argN])
- 第一个参数thisArg,当 func 函数被调用时,该参数会作为 func 函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。
- [,arg1,arg2...argN] 作为实参传递给 func 函数。
bind 返回值
返回一个新函数
注意:这和函数调用 call/apply 改变this指向有所不同。调用call/apply 会把原函数直接执行了。
举个例子说明:
function func(){
console.log(this)
}
// 用call
func.call({a:1}); // func函数被执行了,打印:{a:1}
// 用bind
let newFunc = func.bind({}); // 返回新函数
newFunc(); // 只有当返回的新函数执行,func函数才会被执行
从以上得到如下信息:
- bind被函数调用
- 返回一个新函数
- 能改变函数this指向
- 可以传入参数
深入bind 使用
以上知道了 bind 函数的作用以及使用方式,接下深入到 bind 函数的使用中,具体介绍三个方面的使用,这也是之后模拟实现 bind 函数的要点。
- 改变函数运行时this指向
- 传递参数
- 返回的新函数被当成构造函数
改变函数运行时this指向
当调用 bind 函数后,bind 函数的第一个参数就是原函数作用域中 this 指向的值。
function func(){
console.log(this);
}
let newFunc = func.bind({a:1});
newFunc(); // 打印:{a:1}
let newFunc2 = func.bind([1,2,3]);
newFunc2(); // 打印:[1,2,3]
let newFunc3 = func.bind(1);
newFunc3(); // 打印:Number:{1}
let newFunc4 = func.bind(undefined/null);
newFunc4(); // 打印:window
以上要注意,当传入为 null 或者 undefined 时,在非严格模式下,this 指向为 window。
当传入为简单值时,内部会将简单的值包装成对应类型的对象,数字就调用 Number 方法包装;字符串就调用 String 方法包装;true/false 就调用 Boolean 方法包装。要想取到原始值,可以调用 valueOf 方法。
Number(1).valueOf(); // 1
String("hello").valueOf(); // hello
Boolean(true).valueOf(); // true
当多次调用 bind 函数时,以第一次调用 bind 函数的改变 this 指向的值为准。
function func(){
console.log(this);
}
let newFunc = func.bind({a:1}).bind(1).bind(['a','b','c']);
newFunc(); // 打印:{a: 1}
传递的参数
从 bind 的第二个参数开始,是向原函数传递的实参。bind 返回的新函数调用时也可以向原函数传递实参,这里就涉及顺序问题。
function func(a,b,c){
console.log(a,b,c); // 打印传入的实参
}
let newFunc = func.bind({},1,2);
newFunc(3)
打印结果为1,2,3。
可以看到,在 bind 中传递的参数要先传入到原函数中。
返回的新函数被当成构造函数
调用 bind 函数后返回的新函数,也可以被当做构造函数。通过新函数创建的实例,可以找到原函数的原型上。
// 原函数
function func(name){
console.log(this); // 打印:通过{name:'wy'}
this.name = name;
}
func.prototype.hello = function(){
console.log(this.name)
}
let obj = {a:1}
// 调用bind,返回新函数
let newFunc = func.bind(obj);
// 把新函数作为构造函数,创建实例
let o = new newFunc('seven');
console.log(o.hello()); // 打印:'seven'
console.log(obj); // 打印:{a:1}
新函数被当成了构造函数,原函数func 中的 this 不再指向传入给 bind 的第一个参数,而是指向用 new 创建的实例。在通过实例 o 找原型上的方法 hello 时,能够找到原函数 func 原型上的方法。
在模拟实现 bind 特别要注意这一块的实现,这也是面试的重点,会涉及到继承。
bind函数应用场景
以上只是说了 bind 函数时如何使用的,学会了使用,要把它放在业务场景中来解决一些现实问题。
场景一
先来一个布局:
<ul id="list">
<li>1</li>
<li>1</li>
<li>1</li>
</ul>
需求:点击每一个 li 元素,延迟1000ms后,改变 li 元素的颜色,
let lis = document.querySelectorAll('#list li');
for(var i = 0; i < lis.length; i++){
lis[i].onclick = function(){
setTimeout(function(){
this.style.color = 'red'
},1000)
}
}
以上代码点击每一个 li,并不会改变颜色,因为定时器回调函数的 this 指向的不是点击的 li,而是window,(当然你也可以使用箭头函数,let之类来解决,这里讨论的主要是用bind来解决)。此时就需要改变回调函数的 this 指向。能改变函数 this 指向的有:call、apply、bind。那么选择哪一个呢?根据场景来定,这里的场景是在1000ms之后才执行回调函数,所以不能选择使用call、apply,因为它们会立即执行函数,所以这个场景应该选择使用 bind解决。
setTimeout(function(){
this.style.color = 'red'
}.bind(this),1000)
场景二
有时会使用面向对象的方式来组织代码,涉及到把事件处理函数拆分在原型上,然后把这些挂在原型上的方法赋值给事件,此时的函数在事件触发时this都指向了元素,进而需要在函数中访问实例上的属性时,便不能找到成。
function Modal(options){
this.options = options;
}
Modal.prototype.init = function(){
this.el.onclick = this.clickHandler; // 此方法挂载原型上
}
Modal.prototype.clickHandler = function(){
console.log(this.left); // 此时点击元素执行该函数,this指向元素,不能找到left
}
let m = new Modal({
el: document.querySelector('#list'),
left: 300
})
m.init(); // 启动应用
以上代码,在 init 函数中,给元素绑定事件,事件处理函数挂在原型上,使用 this 来访问。当点击元素时,在 clickHandler 函数中需要拿到实例的 left 属性,但此时 clickHandler 函数中的 this 指向的是元素,而不是实例,所以拿不到。要改变 clickHandler 函数 this 的指向,此时就需要用到 bind。
Modal.prototype.init = function(){
this.el.onclick = this.clickHandler.bind(this)
}
以上场景只是 bind 使用的冰山一角,它本质要做的事情是改变 this 的指向,达到预期目的。掌握了 bind 的作用以及应用的场景,在脑海中就会树立一个印象:当需要改变this指向,并不立即执行函数时,就能想到 bind。
模拟实现
为什么要自己去实现一个bind函数呢?
bind()函数在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行(ie8以下)。
面试用,让面试官找不到拒绝你的理由
抓住 bind 使用的几个特征,把这些点一一实现就OK,具体的点:
- 被函数调用
- 返回新函数
- 传递参数
- 改变函数运行时this指向
- 新函数被当做构造函数时处理
被函数调用,可以直接挂在Function的原型上,为了补缺那些不支持的浏览器,不用再为支持的浏览器添加,可以做如下判断:
if(!Function.prototype.bind) {
Function.prototype.bind = function(){
}
}
这种行为也叫作 polyfill,为不支持的浏览器添加某项功能,以达到抹平浏览器之间的差距。
注意:如果浏览器支持,方便自己测试,可以把 if 条件去掉,或者把 bind 改一个名字。在下文准备改名字为 bind2,方便测试。
调用 bind 后会返回一个新的函数,当新函数被调用,原函数随之也被调用。
Function.prototype.bind2 = function(thisArg,...args){
let funcThis = this; // 函数调用bind,this指向原函数
// 返回新函数
return function (...rest) {
return funcThis.apply(thisArg,[...args,...rest]/*bind2传递的实参优先于新函数的实参*/)
}
}
// 测试
function func(a,b,c){
console.log(this)
console.log(a,b,c)
}
let newFunc = func.bind2({a:1},1,2);
newFunc(3);
// 打印:{a: 1}
// 打印:1 2 3
以上这个函数已经能够改变原函数 this 的指向,并传递正确顺序的参数。接下来就是比较难理解的地方,当新函数被当做构造函数的情况。
需要作出两个地方的改变:
- 新返回的函数要继承原函数原型上的属性
- 原函数改变this问题。如果用new调用,则原函数this指向应该是新函数中this的值;否则为传递的thisArg的值。
先做继承,让新函数继承原函数的原型,维持原来的原型关系。匿名函数没办法引用,所以给新函数起一个名字。
Function.prototype.bind2 = function(thisArg,...args){
let funcThis = this; // 函数调用bind,this指向原函数
// 要返回的新函数
let fBound = function (...rest) {
return funcThis.apply(thisArg,[...args,...rest]/*bind2传递的实参优先于新函数的实参*/)
}
// 不是所有函数都有prototype属性,比如 Function.prototype就没有。
if(funcThis.prototype){
// 使用Object.create,以原函数prototype作为新对象的原型创建对象
fBound.prototype = Object.create(funcThis.prototype);
}
return fBound;
}
// 测试
function func(name){
console.log(this); // {a: 1}
this.name = name;
}
func.prototype.hello = function(){
console.log(this.name); // undefined
}
let newFunc = func.bind2({a:1});
let o = new newFunc('seven')
o.hello();
// 打印:{a: 1}
// 打印:undefined
以上代码,新建的实例 o 能够调用到 hello 这个方法,说明继承已经实现,能够访问新函数上原型方法。
接下来是关于 this 指向问题,上面例子中,使用了 new 运算符调用函数,那么原函数中,this 应该指向实例才对。所以需要在改变 this 指向的 apply 那里对是否是使用 new 操作符调用的做判断。
用到的操作符是 instanceof,作用是判断一个函数的原型是否在一个对象的原型链上,是的话返回true,否则返回false。测试如下:
function Person(){}
let p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log(p instanceof Array); // fasle
也可以用 instanceof 在构造函数中判断是否是通过 new 来调用的。如果是用 new 来调用,说明函数中 this 对象的原型链上存在函数的原型,会返回true。
function Person(){
console.log(this instanceof Person); // true
}
new Person();
回到我们的 bind2 函数上,当调用 bind2 后返回了新函数 fBound,当使用 new 调用构造函数时,实际上调用的就是 fBound 这个函数,所以只需要在 fBound 函数中利用 instanceof 来判断是否是用 new 来调用即可。
Function.prototype.bind2 = function(thisArg,...args){
let funcThis = this; // 函数调用bind,this指向原函数
// 要返回的新函数
let fBound = function (...rest) {
// 如果是new调用的,原函数this指向新函数中创建的实例对象
// 不是new调用,依然是调用bind2传递的第一个参数
thisArg = this instanceof fBound ? this : thisArg;
return funcThis.apply(thisArg,[...args,...rest]/*bind2传递的实参优先于新函数的实参*/)
}
// 不是所有函数都有prototype属性,比如 Function.prototype就没有。
if(funcThis.prototype){
// 使用Object.create,以原函数prototype作为新对象的原型创建对象
fBound.prototype = Object.create(funcThis.prototype);
}
return fBound;
}
// 测试
function func(name){
console.log(this); // {a: 1}
this.name = name;
}
func.prototype.hello = function(){
console.log(this.name); // undefined
}
let newFunc = func.bind2({a:1});
let o = new newFunc('seven')
o.hello();
// 打印:{name:'seven'}
// 打印:'seven'
bind 函数源码已实现完成,希望对你有帮助。
如有偏差欢迎指正学习,谢谢。
来源:https://segmentfault.com/a/1190000017543088
bind函数作用、应用场景以及模拟实现的更多相关文章
- 【进阶3-4期】深度解析bind原理、使用场景及模拟实现(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/23 bind() bind() 方法会创建一个新函数,当这个新函 ...
- apply,call,bind函数作用与用法
作用 可以把方法借给其它对象使用,并且改变this的指向 a.apply(b,[3,2]);//this指向由a变为b, a的方法借给b使用 实例: function add(a,b){ ...
- 网络通讯中 bind函数的作用
面向连接的网络应用程序分为客户端和服务器端.服务器端的执行流程一般为4步,客户端程序相对简单,一般需要两个步骤. 服务器端执行流程4步如下: (1)调用socket函数,建立一个套接字,该套接字用于接 ...
- bind函数的作用
面向连接的网络应用程序分为客户端和服务器端.服务器端的执行流程一般为4步,客户端程序相对简单,一般需要两个步骤. 服务器端执行流程4步如下: (1)调用socket函数,建立一个套接字,该套接字用于接 ...
- 优雅手撕bind函数(面试官常问)
优雅手撕bind函数 前言: 为什么面试官总爱让实现一个bind函数? 他想从bind中知道些什么? 一个小小的bind里面内有玄机? 今天来刨析一下实现一个bind要懂多少相关知识点,也方便我们将零 ...
- 从bind函数看js中的柯里化
以下是百度百科对柯里化函数的解释:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术.概念太抽象,可能 ...
- javascript中bind函数的作用
javascript的bind的作用 <!DOCTYPE html> <html> <head> <meta charset="utf-8" ...
- 模拟实现兼容低版本IE浏览器的原生bind()函数功能
模拟实现兼容低版本IE浏览器的原生bind()函数功能: 代码如下: if(!Function.prototype.bind){ Function.prototype.bind=function( ...
- bind()函数的作用
bind()函数是Function原型上的一个属性,当某个函数调用此方法时,可以通过向bind()函数传入执行对象和调用bind的函数的参数来改变函数的执行对象 /*问题:改变func执行环境,使之输 ...
随机推荐
- jmeter csv Data Set Config 文件中带引号的数据转换问题(自动添加双引号解决办法)
1.我们从csv中获取数据,在jmeter中使用这些数据,其中csv的数据如图,有的数据包含引号. 2.问题:我们获取的json数据,被自动添加了双引号 3.解决方式: 在CSV Data Set C ...
- Redis大 key的发现与删除方法全解析
个推作为国内第三方推送市场的早期进入者,专注于为开发者提供高效稳定的推送服务,经过9年的积累和发展,服务了包括新浪.滴滴在内的数十万APP.由于我们推送业务对并发量.速度要求很高,为此,我们选择了高性 ...
- opencv_将图像上的4个点按逆时针排序
1:代码如下: #include "stdafx.h" #include "cxcore.h" #include "cvcam.h" #in ...
- Spring Data概览
总结:JpaRepository继承PagingAndSortingRepository,PagingAndSortingRepository继承CrudRespository,CrudResposi ...
- java代码如何在没有安装JDK的Windows下运行
java代码如何在没有安装JDK的Windows下运行? 对于Java桌面应用来说,比较烦琐的就是安装部署问题,如:客户端是否安装有jre.jre版本.jre在哪里下载.如何用jre启动Java应用等 ...
- leetcode-easy-others-118 Pascal's Triangle
mycode 16.47% class Solution(object): def generate(self, numRows): """ :type numRow ...
- QBXT七月D1
今天是lyd神仙讲课的第一天,可以感觉到的是这位神仙有着不同于他人的气质,比如他的表情包(雾) 好了来讲正经的) 今天讲的比较多的是模拟算法和一些比赛中的好习惯 high-level 这个名词的大体意 ...
- IDEA全局配置
进入全局设置界面: 取消每次启动IDEA就默认打开上一次最后关闭的项目 编译器代码字体设置: 控制台字体大小和颜色设置 同一个文件代码里面的各个不同方法之间显示分割线 代码自动提示不区分大小写 格式化 ...
- python - lambda 函数使用
# if we need it only once and it's quite simple def make_incrementor(n): return lambda x: x + n f = ...
- Kotlin之定义变量
java : int n = 30 ; final int m = 30 ; float k = 2.5f; string s = "sss"; short i = 5; bool ...