实现apply、call、bind
- 实现apply、call、bind函数
- 注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况
call函数的手写实现
给函数上面添加一个叫做hycall方法的方式:
- 只能给某个函数添加方法
- foo.hycall = function(){}
- 给所有的函数添加一个hycall的方法
- Funtion.prototype.hycall = function(){}
Function.prototype.hycall = function(){ console.log("原型链调用了"); } function foo(){ console.log("foo函数调用了"); } foo.hycall()//原型链调用了 ---------------------------------------------------------- //我们在执行foo.hycall的时候会发现只执行了hycall函数,但是却把原本foo函数自带的信息给掩盖掉了,这肯定是不合理的,我们是要在原有的基础上进行调用,而不是另起大厦
改良写法
//差的写法,缺乏复用性 Function.prototype.hycall = function(){ console.log("原型链调用了"); foo() } function foo(){ console.log("foo函数调用了"); } foo.hycall() //好的写法,可以多次复用 //本身我们在进行调用的时候,就相当于已经是隐式绑定了,foo.hycall()的时候,this的绑定就已经绑到foo上面了 Function.prototype.hycall = function(){ console.log("原型链调用了"); var fn = this fn() } function foo(){ console.log("foo函数调用了"); } foo.hycall()
怎么实现给函数传递参数
我们往call函数内传递参数,会在调用的this中打印出来,但是在我们自己手写的函数里面,打印出来的却是windows,如何解决这个问题?
Function.prototype.hycall = function(thisArg){ var fn = this //调用需要被执行的函数 thisArg.fn = fn thisArg.fn() //等函数执行完之后删掉这个属性 delete thisArg.fn } function foo(){ console.log(this); } foo.hycall({name:"小余",age:"20"})//{ name: '小余', age: '20', fn: [Function: foo] }
此时手写的函数已经可以传递参数了,但能传递的仅仅是对象,如果传递数字会报错,接下来我们就解决这个问题
//使用call函数传递数字参数的话 foo.call(123)//[Number: 123]
//对thisArg转成对象类型(防止传入非对象类型报错) thisArg = thisArg ? Object(thisArg) : window
call除了第一个参数是用来改变this绑定的,后面还可以绑定一堆参数,这个放到我们自己实现要怎么去做呢?在之前是可以使用argument,现在ES6中有更好的解决方法了
ES6中的剩余参数
对于接收多少个参数我们是不确定的,那这个时候,我们形参的部分就不能够写死了,不然是写不尽写不完的,你也不能够100%猜到用户想输入几个参数
function sum(...args){ //打印出来的args是数组形式 console.log(args) //展开运算符spread console.log(...args)//得到的直接是数值 }
很显然通过ES6中的...的方式来解决,接着进到最后的一步,返回结果
Function.prototype.hycall = function(thisArg,...args){ // console.log("传递参数进来了噢",this); var fn = this //对thisArg转成对象类型(防止传入非对象类型报错) thisArg = thisArg ? Object(thisArg) : window //调用需要被执行的函数 thisArg.fn = fn var result = thisArg.fn(...args) //等函数执行完之后删掉这个属性 delete thisArg.fn //返回结果 return result } function foo(){ console.log(this); } foo.hycall(123,6,66,666,6666) //==========================================>换个函数执行一下 function foo(num1,num2,num3){ console.log("foo的this指向是",this,"三数相加的结果=",num1+num2+num3); } foo.mycall("小余",500,20,1) //foo的this指向是 String {'小余', fn: ƒ} 三数相加的结果= 521
apply函数的手写实现
//自己实现hyapply Function.prototype.myapply = function(thisArgs,argArray){//区别在于这里不需要ES6的...运算,因为传入的是一整个数组 var fn = this thisArgs = thisArgs ? Object(thisArgs) : window thisArgs.fn = fn var arr = thisArgs.fn(...argArray)//但是数组需要解构出来 delete thisArgs.fn return arr } function sum(num1,num2){ console.log("sum被调用",this,num1,num2); } var result = sum.myapply("小余",[200,30]) console.log(result,'老铁666');
在call函数的基础上进行小范围修改,貌似已经满足了apply函数的要求了,但是当我们除了第一个用于this指定之外,其他参数我们不传就会出现问题了
- 因为当我们不传递参数的时候,argArray就会是undefined,我们在解构的时候就会变成...undefined了,对underfunded进行扩展是错误的
- 这个时候我们就可以进行一个判断来解决这个问题
- 那为什么在call函数的手写的时候没有遇到这个问题呢?因为我们在call函数中的形参是...args,那这东西的格式就是个数组,你什么都不传都默认是空数组[]
Function.prototype.myapply = function(thisArgs,argArray){ var fn = this thisArgs = thisArgs ? Object(thisArgs) : window thisArgs.fn = fn argArray = argArray || []//不止这种写法,也可以使用三元运算符 var arr = thisArgs.fn(...argArray) delete thisArgs.fn return arr } function sum(num1,num2,num3){ console.log("sum被调用",this,num1,num2+num3); } var result = sum.myapply("小余") console.log(result,'老铁666');
手写call和apply的补充
在我们改变this指向的时候,输入0的时候,会指向window,这是由于我们三元运算符那些的写法导致的,也有其他情况可以避免,这样是对边界效应的一种延伸考虑
thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window
bind函数的手写实现
首先我们来看看JS中bind传参数的3种方式,第一种传值方式跟call的方式很像,从第二种跟第三种开始,跟其他两个函数(call、apply)发生了不一样的变化,我们来看看他们是怎么实现的吧
//方式1:在bind中传值 function foo(num1,num2,num3,num4){ console.log(this,num1,num2,num3,num4); } var bar = foo.bind('小余',10,20,30,40) bar() //方式2:在接收bind的bar中传值 function foo(num1,num2,num3,num4){ console.log(this,num1,num2,num3,num4); } var bar = foo.bind('小余') bar(10,20,30,40) //方式3:方式1跟方式2的结合,从方式1到方式2中按顺序传递 function foo(num1,num2,num3,num4){ console.log(this,num1,num2,num3,num4); } var bar = foo.bind('小余',10,20) bar(30,40) //以上三种方式的答案都是:String {'小余'} 10 20 30 40
我们是有变量接收值的,就像上方的bar来接收,对于这种情况就需要在手写的函数中最后进行一个return返回
Function.prototype.mybind = function(thisArg,...argArray){ function proxyFn(){ } return proxyFn }
首先我们揭秘第二种方式是怎么做到的,也就是得到了值还能继续往里面传值,那是因为我们手写bind函数返回的还是一个函数,而里面这个函数是可以接收值的,所有自然就造成这种情况了,第三种则是手写bind函数跟bind函数内部的函数都可以接收值,然后再做一个拼接就完成了
Function.prototype.mybind = function(thisArg,...argArray){ //1.获取真实要调取的函数 var fn = this //对特殊情况的处理 thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window function proxyFn(...args){ //将函数放到thisArg中进行调用 thisArg.fn = fn //对传入的两个参数进行合并 var finalArgs = [...argArray,...args] var result = thisArg.fn(finalArgs) delete thisArg.fn //返回结果 return result } return proxyFn } function foo(num1,num2,num3,num4){ console.log(this,num1,num2,num3,num4); } var bar = foo.bind('小余',10,20) bar(30,80)
认识arguments
- argument是一个对应于 传递给函数的参数 的 类数组(array-like)对象
- 类数组对象什么意思?就是长得像数组,但实际上是个对象
- 我们在实参传递的个数如果超过形参的数量的话,多余的不是丢弃掉,而是跟着前面其他几个参数一起放到argument中了
- array-like意味着它不是一个数组类型,而是一个对象类型:
- 但是它却拥有数组的一些特性,比如说length,比如说可以通过index索引来访问
- 但是它却没有数组的一些方法,比如forEach、map等等
//argument的基础使用 function foo(num1,num2,num3){ // console.log(arguments); //常见对argument的3个操作 //1.获取参数长度 console.log(arguments.length); //2。根据索引值获取某一个参数,像数组一样的操作 console.log(arguments[1]); //3.callee属性,获取argument中所在的函数 console.log(arguments.callee); } foo(10,20,30,40,50)
argument转数组
function foo(num1,num2,num3,num4){ //1.自己遍历 var newArr = [] for(var i = 0;i < arguments.length;i++){ newArr.push(arguments[i] *40) } console.log(newArr); //2.arguments转成array数组类型 //2.1自己遍历arguments中所有的元素 //2.2 使用slice var newArr2 = Array.prototype.slice.call(arguments) console.log(newArr2,"这是newArr2"); //这里其实跟2.2是一样的,this显示绑定arguments大于隐式绑定 var newArr3 = [].slice.call(arguments) console.log(newArr3) //2.3 ES6的语法 var newArr4 = Array.from(arguments) console.log(newArr4) //展开运算符 var newArr5 = [...arguments] console.log(newArr5) } foo(1,2,3,4)
数组里的slice实现
//补充:在原型链上加函数的方法,我们确实可以在每个函数上面都调用,但是当我们想要调用这个函数的方法本身的时候,就略显麻烦 Function.prototype.aaa = function(){ //xxxx } Function.prototype.aaa()//这样调用
//自己简单实现的slice Array.prototype.hyslice = function(start,end){//这里可以进行优化,看用户是否有传递进来这两个参数,没有的话我们就做一个判断处理 var arr = this start = start || 0 end = end || arr.length var newArray = [] for(var i = start;i < end ;i++){ newArray.push(arr[i])//就将this里的东西给填入进去,而this已经被我们手动改成我们想要的指向了 } return newArray } var newArray = Array.prototype.hyslice.call(["小余","大余","超大余"],1,3)//相当于使用call来调用slice,我们上方函数里的this指向就被改变到我们手写的这个数组内容上了 console.log(newArray);
箭头函数-无arguments
我们在箭头函数中是没有arguments的,如果你需要的话,js会去上层作用域里面找
向下方这个,arguments到上层作用域,也就是全局作用域中寻找,这全局作用域是分两种情况的,在node中是有的,在浏览器中则是没有(显示你没有定义)
var name = "小余" var foo = ()=>{ console.log(name) console.log(arguments); } foo()
案例
在ES6中用...剩余函数来替代arguments,用来接收所有参数,形成数组
function foo(){ var bar = ()=>{ console.log(arguments);//打印出来的就是上层foo的arguments } return bar } var fn = foo(123) fn()