bind 函数挂在 Function 的原型上面试
Function.prototype.bind
建立的函数均可以直接调用 bind,使用:浏览器
function func(){ console.log(this) } func.bind(); // 用函数来调用
bind 的做用:app
bind() 方法调用后会建立一个新函数。当这个新函数被调用时,bind() 的第一个参数将做为新函数运行时的 this的值,以后的序列参数将会在传递的实参前传入做为新函数的参数。<MDN>
bind 接收的参数函数
func.bind(thisArg[,arg1,arg2...argN])
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函数才会被执行
从以上获得以下信息:this
以上知道了 bind 函数的做用以及使用方式,接下深刻到 bind 函数的使用中,具体介绍三个方面的使用,这也是以后模拟实现 bind 函数的要点。prototype
当调用 bind 函数后,bind 函数的第一个参数就是原函数做用域中 this 指向的值。rest
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 函数时如何使用的,学会了使用,要把它放在业务场景中来解决一些现实问题。
先来一个布局:
<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,具体的点:
被函数调用,能够直接挂在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 的指向,并传递正确顺序的参数。接下来就是比较难理解的地方,当新函数被当作构造函数的状况。
须要做出两个地方的改变:
先作继承,让新函数继承原函数的原型,维持原来的原型关系。匿名函数没办法引用,因此给新函数起一个名字。
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 函数源码已实现完成,但愿对你有帮助。
若有误差欢迎指正学习,谢谢。