10.手写apply、call、bind以及认识arguments

977 阅读27分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
  • 课程对照进度:JavaScript高级系列39-44集(coderwhy)
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 在本章节中,我们会来进行手写显式调用的三个函数
    • 在实现的过程中,我们既要知道如何实现,又需要知道为什么这么做,才能够在将来进行运用的时候有一定的思考空间
    • 并且通过考虑为什么这么做,可以知晓后推出的bind函数方法,解决了之前call、apply的哪些痛点问题
  • 在学习了这些内容后,我们会来了解前面章节所提到的arguments,到底是用来做什么的,以及为什么现在已经不再流行的原因
    • 箭头函数中为什么没有arguments,为什么不加上?以及arguments如何进行使用,它到底是数组还是对象,我们都会探索到

一、手写apply、call、bind函数

实现apply、call、bind函数

  • 注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况

    • 边界情况在实际需求之中是需要的, 未正确处理边界情况可能会导致程序崩溃、执行错误、数据损坏或安全漏洞。因为需要防止一些恶意用户和小白用户的错误使用,所以要对不规范不正确的内容进行处理,以此提高函数方法的通用性和健壮性,很多的代码和精力都不得不用在了防范上面。很多的边界情况还需要针对业务去专门考虑,而我们是以学习这些函数的思想为主,所以不需要考虑边界情况
  • 真正实际的这三个函数是使用C++进行实现的,我们采用JS来进行模拟实现

  • 在这里,我们先来复习一下三个函数的语法,方便我们等下的实现,尤其注意call和bind的差别

func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [argsArray])
const newFunc = func.bind(thisArg, arg1, arg2, ...)

1.1. call函数的手写实现

  • 给函数上面添加一个叫做hycall方法的方式:

    1. 在之前我们的做法只能给某个函数添加方法,用于比较专一的场景下
      • foo.hycall = function(){}
    2. 而像call这类函数都是所有地方都可以调用的,相当于给所有的函数添加一个call的方法
      • Funtion.prototype.hycall = function(){},我们使用hycall来作为我们的函数实现,和call函数进行区分,防止覆盖问题
      • 而这是通过原型链进行实现的,我们会在面向对象的部分学到原型链的核心知识。目前只需要先清楚,定义在函数原型链上的方法可以用在所有的函数身上
Function.prototype.hycall = function(){
    console.log("原型链调用了");
}

function foo(){
    console.log("foo函数调用了");
}

foo.hycall()//原型链调用了
----------------------------------------------------------
//我们在执行foo.hycall的时候会发现只执行了hycall函数,但是却把原本foo函数自带的信息给掩盖掉了,这肯定是不合理的,我们是要在原有的基础上进行调用,而不是另起大厦
  • 我们的目标对象就是foo函数,在这函数身上试验我们hycall的this改变效果
  • 首先我们需要能够在调用hycall函数的时候,也能够执行foo函数,因为call函数能够做到这一步
  • 这是因为call的设计初衷是允许我们立即调用一个函数(也被称为立即执行函数),如图10-1,并能够改变这个函数执行时的this上下文,改变的this只会在该执行阶段生效,这是它的核心功能之一
  • 直接控制函数的执行时机的设计方式,让我们在调试的时候不需要额外的调用一遍foo函数去检测this到底变没变,在foo的执行体身上可以直接打印一下this就能立马看到效果。在代码简洁度上有一定的提升,而且更灵活,用途也更明确
  • 作为一个方法而言,是要执行步骤的,而绑定this是在执行时去顺便做到的,call是作为功能上的补充,而非独立。这也是我们为什么往往采用链式调用的原因,因为如果不进行使用该函数的话,this也不会被用到,而需要用到this的时候,也是调用该函数的时候,则这时候call函数将包揽这些作用,绑定后立刻执行函数,简化我们的步骤
function foo(){
  console.log('foo被调用');
}

foo.call('我是小余')

API语法参数分类

图10-1 call调用会执行函数

  • 我们在实现调用函数本身的时候,有两种方式
    1. 直接固定调用我们想要的函数
    2. 根据调用该方法的函数去针对调用
  • 毫无疑问的,我们应该要选择第二种方式。第一种方式,我们只能调用foo函数,这样太死板了,相当于这hycall只能用在foo函数身上
    • 而在调用hycall方法的时候,是属于隐式调用,此时的this就绑定到我们所需要的函数上了,所以我们可以利用这点使用方式2拿到this,直接调用执行
    • var fn = this获取对应的函数上下文,fn()执行其函数上下文。谁调用了hycall,this就指向于谁,也就执行于谁
    • 虽然在这个情况下直接调用this()也是OK的,但将this赋值给fn对于代码的清晰性或者在复杂的情况下能避免关于this的混淆,提高阅读性
//方式1:差的写法,缺乏复用性
Function.prototype.hycall = function(){
    console.log("原型链调用了");
    foo()
}

function foo(){
    console.log("foo函数调用了");
}

foo.hycall()
//方式2:好的写法,可以多次复用
//本身我们在进行调用的时候,就相当于已经是隐式绑定了,foo.hycall()的时候,this的绑定就已经绑到foo上面了
Function.prototype.hycall = function(){
    console.log("原型链调用了");
    var fn = this
    fn()
}

function foo(){
    console.log("foo函数调用了");
}

foo.hycall()

1.2. 怎么实现给函数传递参数

而call方法的核心功能就是函数立即调用以及改变this指向内容

  • 我们已经能够拿到正常情况下的this了,实现了函数调用的效果
  • 那如何来改变this指向效果呢?我们需要先解决一件事情,那就是怎么给hycall方法传递参数,因为改变this的指向效果,我们想改变成什么,取决于我们传递进来什么内容。需要先有参数内容,才有内容可以去指向。先有目标才能做出改变,人生也是如此
    • 在我们这个案例中,我对hycall方法传递进了一个对象参数,在foo中打印了this,此时foo的this会改变吗?
//方式2:好的写法,可以多次复用
//本身我们在进行调用的时候,就相当于已经是隐式绑定了,foo.hycall()的时候,this的绑定就已经绑到foo上面了
Function.prototype.hycall = function(){
  console.log("原型链调用了");
  var fn = this
  fn()
}

function foo(){
  console.log("foo函数调用了",this);
}

foo.hycall({ name: '小余' })
  • 从图10-2的结果来看,并没有发生改变
    • 我参数已经传递进来了,我要怎么将这一参数扭转为新的this呢?
    • 首先,this的指向是受到四个基础规则的影响的,而我们的hycall方法内部,fn()是默认绑定规则,也就是独立调用,this本就该指向于window。我们想要进行扭转,就应该从这里下文章

方式2返回结果

图10-2 方式2返回结果

  • 默认绑定规则是优先度最低的规则,如果用传递进去的参数来调用fn,是不是就形成了显式调用规则,而这规则的优先度是大于默认调用的。从而形成this指向的改变
    • 对此,我突然想到,是不是可以采用上一章节中,改变this指向的方法呢?
    • 传递进来的参数是一个对象,我们将this指向这个对象的方法有什么方式,来回忆一下吧
var obj = {
  name:"小余",
  fn:function(){
    console.log(this);
  }
}
obj.fn()//隐式调用,this指向于obj
  • 是不是很熟悉了,我们传递进来的是一个对象,重复这句话,那我是不是可以创建一个属性来存放这个var fn = this指向于foo函数的fn呢?
    • 然后在通过对象调用这个属性,就成功的改变了this指向
    • 当调用了之后,我们传递进来的对象,里面会有两个内容,一个是本身传递的内容name:"小余",一个则就是我们创建的fn属性,而这fn属性就是自调用了我们的foo函数。当下将这个函数打印出来了没有关系,但当调用结束后,我们就将这个fn属性删掉,防止对传递进来的对象内容造成多余的改变
    • 通过JS进行模拟的,和使用C++进行实现的call方法还是有一定的差别。原生的call方法,不会让重新绑定的this出现新的属性,比如我们这里的fn,如结果图10-3,我们只能尽可能减少影响,在一调用结束就将多余的fn属性立刻删除
Function.prototype.hycall = function (thisArg) {
  var fn = this
	//调用需要被执行的函数
    thisArg.fn = fn
    thisArg.fn()
    //等函数执行完之后删掉这个属性
    delete thisArg.fn
}

function foo() {
  console.log("foo函数调用了", this);
}

foo.hycall({ name: '小余' })
foo.call({ name: "coderwhy" })

API语法参数分类

图10-3 JS模拟的call方法缺陷

  • 此时手写的函数已经可以传递参数了,但能传递的仅仅是对象,如果传递数字会报错

    • 这是因为我们改变this指向的方式,明显是利用隐式调用的特性,数字作为基本类型是没有像对象这种复杂类型可以自动创建出fn这样的属性的
    123.fn = fn//肯定会报错的
    
    • 而像这类情况还有一些,比如传递的是字符串呢?这些就都是应该处理的边界情况,从而提升该方法的通用性
//使用call函数传递数字参数的话
foo.call(123)//[Number: 123]
  • 我们可以将其转化为对象的格式,用Object()将其转化对象就OK了
    • 通过和原版call方法进行对比,可以实现一样的效果,不管我们传递的是基本类型还是复杂类型都可以正常显示,如图10-4
Function.prototype.hycall = function (thisArg) {
  var fn = this
  thisArg = Object(thisArg)//不管thisArg是什么类型,都转化为对象
  thisArg.fn = fn
  thisArg.fn()

  delete thisArg.fn
}

function foo() {
  console.log("foo函数调用了", this);
}

foo.hycall(123)
foo.hycall('字符串')
foo.call(123)
foo.call('字符串')

API语法参数分类

图10-4 转化为对象的处理方式结果

  • 但还有两种既不是基本类型,也不是复杂类型:undefinednull 是两个特殊的数据类型
    • Object()只能将其转化为空对象,而我们的call方法在面对这两种方式的时候,则是默认指向window的
    • 对此我们可以进行判断经过Object()转化后,是否存在内容,不存在则设置为window
//对thisArg转成对象类型(防止传入非对象类型报错)
thisArg = thisArg ? Object(thisArg) : window

1.3. ES6中的剩余参数

  • call除了第一个参数是用来改变this绑定的,后面还可以绑定一堆参数,这个放到我们自己实现要怎么去做呢?在之前是可以使用arguments,现在ES6中有更好的解决方法了,而什么是arguments我们会放在最后进行了解学习
  • 对于接收多少个参数我们是不确定的,那这个时候,我们形参的部分就不能够写死了,不然是写不尽写不完的,也不能够100%猜到用户想输入几个参数
    • 在ES6之后,出现了剩余参数语法(rest parameters),使用三个点...后跟一个数组名来表示。这个数组将包含从该位置开始到实参列表末尾的所有实参
function sum(...args){
    //打印出来的args是数组形式 
    console.log(args)
    //展开运算符spread
    console.log(...args)//得到的直接是数值
}
  • 剩余参数必须是函数参数列表中的最后一个参数,因为它是用来收集所有剩余的实参的
    • 当我们需要将函数的参数传递给另一个函数时,剩余参数让操作更简单。可以直接传递收集到的参数数组而不需要额外的逻辑
    • 通过这个特性,我们可以完成call方法所具备的特性,在绑定this之后,继续往后处理各种参数
    • 需要注意的是函数的length属性,也就是我们期望的参数数量,不包括剩余参数
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结果,取决于外面是否需要用到这个结果做其他操作
    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.hycall("小余",500,20,1)
//foo的this指向是 String {'小余', fn: ƒ} 三数相加的结果= 521
  • 而call方法为什么要做到能够传递更多参数进去呢?它的作用不是做到改变this指向就行了吗
    • 首先是为了通用性,因为不是所有的函数都不需要传递参数的,为了满足需要传递参数的函数,提高泛用性,所以这么做
    • 第二点则是我们前面所说的,call方法是在正常函数上的进行补充,这是一种功能融合的编程艺术
  • 在我们下方的案例中,采用了两种不同的方式来进行使用
    • 我们需要知道一点,每次调用foo函数的时候,都是会重新创建出来一个新的函数执行上下文。而this绑定的就是执行上下文
    • 如果我们采用第二种方式的话,那就是属于刻舟求剑的行为
    • 所以说,我们想要调用改变后的this,就一定要在改变this的那一次函数调用中进行传递参数进去,所以JS官方这么设计是在考量了多种情况下的优解
//方式1:结合的方式
foo.call("小余",500,20,1)
//方式2:先绑定后使用,有坑
foo.call('coderwhy')
foo(500,20,1)//重新调用,会创建出新的执行上下文,this指向回到window

1.4. apply函数的手写实现

  • apply函数与call函数的区别在于如何接收参数
    • 对于this的显式指向是一样的
    • 而我们之所以在前面特地说明JS官方为什么要设计接收参数的原因,就是因为这显示绑定的三个函数方法不同之处就在这里
  • call 需要一个参数列表,而 apply 需要一个参数数组
    • apply 在处理不确定数量的参数时更为方便,尤其是当参数已经以数组形式存在时,他们所针对的是传递参数的不同情况
    • 所以如果只是为了改变this指向,用哪个方法都是无所谓的,需要进行区分的关键在于传递参数的需求
//自己实现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,'coderwhy');

在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,'coderwhy');

1.5. 手写call、apply的补充

在我们改变this指向的时候,输入0的时候,会指向window,这是由于我们三元运算符那些写法导致的,也有其他情况可以避免,这样是对边界效应的一种延伸考虑

 thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window

1.6. bind函数的手写实现

首先我们来看看JS中bind传参数的3种方式

  • 第一种传值方式跟call的方式很像,从第二种跟第三种开始,跟其他两个函数(call、apply)发生了不一样的变化,我们来看看他们是怎么实现的吧
    • 可以看到,bind的参数是在call方法上的强化版本
    • 还记得我们上面"刻舟求剑"的方式吗?bind解决了这个问题,绑定this和传递参数可以分开使用,在一定程度上,方便了后续的延展性和灵活性
    • 拥有返回值,让我们可以有一个机会用变量去接收这个固化绑定的效果,可以一直去使用这个绑定的this去完成我们所需要做的事情
//方式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
  • 在分析了call函数方法后,我们可以清楚的知道,bind这个函数方法,JS官方都对其哪些痛点做出优化
  • 如果由我们来实现的话,和call函数方法对比的实现难点就在于这个痛点优化上,我们需要做到拥有返回值,并且返回值的this需要固定住。以及两个核心参数this绑定以及剩余参数的传递要足够的自由,可以随意传递
    • 我们是有变量接收值的,就像上方的bar来接收,对于这种情况就需要在手写的函数中最后进行一个return返回
Function.prototype.mybind = function(thisArg,...argArray){
    function proxyFn(){

    }

    return proxyFn
}
  • 首先我们揭秘第二种方式是怎么做到的,也就是得到了值还能继续往里面传值,那是因为我们手写bind函数返回的还是一个函数,而里面这个函数是可以接收值的,所有自然就造成这种情况了,第三种则是手写bind函数跟bind函数内部的函数都可以接收值,然后再做一个拼接就完成了
    • 在这里,我们就不会立即执行函数,第一次会返回一个函数(这时候我们传递进去的内容会形成最初值,也是一直固定的默认值)。只有第二次去执行返回函数的时候,才会真正的进行执行(此时可以在默认值基础上添加新的参数一起执行)
    • 这种第一次抽取相同内容,往后则是在同样基础内容上去添加个性化的内容,可以最大程度的降低代码的重复内容,是复用的一种体现
    • 所以相对于call 这种适用于需要立即执行函数并且已知所有参数的情况。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.mybind('小余',10,20)//[String: '小余'] { fn: [Function: foo] } 10 20 30 80
//正常bind效果:[String: '小余'] 10 20 30 80
bar(30,80)

二、认识arguments

  • argument是一个对应于 传递给函数的参数类数组(array-like)对象。允许访问调用函数时传递的每个参数,即使在函数定义时没有明确的参数名称
    • 类数组对象什么意思?就是长得像数组,但实际上是个对象
    • 我们在实参传递的个数如果超过形参的数量的话,多余的不是丢弃掉,而是跟着前面其他几个参数一起放到argument中了
    • arguments 中的值与函数参数有动态的联系。如果更改了函数参数的值,相应的 arguments 中的值也会改变,反之亦然
    • 但还是需要说,arguments在目前已经被剩余参数所替代,这里更多的是作为一个补充,补全我们体系的拼图,在阅读到相关代码的时候能够理解就OK了
  • 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)

2.1. arguments转数组

  • 尽管类数组对象看起来和数组相似,可以通过索引访问其元素,也有length属性,但它们不是由Array 构造函数创建的,因此不具有数组的原型方法。这是因为它们的原型链不同:真正的数组继承自Array.prototype,而类数组对象可能只继承自Object.prototype
  • 有时候,我们需要使用数组的方法处理类数组对象,就可以将其转换为真正的数组,而转化为数组主要有三种方法
    1. Array.from()
    2. 扩展运算符(Spread operator)
    3. Array.prototype.slice.call()
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)
  • 类数组对象在早期的JavaScript版本中特别有用,因为当时还没有现在我们熟知的现代JavaScript功能(如ES6之后的剩余参数)。设计arguments为类数组对象而非真正的数组,可能是出于实现简单性和性能考虑
    • 对于许多内建函数和方法,返回类数组对象而不是真正的数组可以保持API的简洁和一致。这样做避免了对数组原型的修改,可能在多个环境或库之间造成冲突或不一致的行为

2.2. 数组里slice手写实现

  • 在将arguments改变为数组的方式里面,Array.prototype.slice.call()是稍微难以理解一点的部分
    • call我们知道可以显示绑定this,但为什么要和slice结合起来呢?
    • 所以理解为什么要用 slice 方法是关键。slice 是数组的一个方法,它用来返回一个数组的一部分作为新数组,它也不会修改原数组
  • 然而,arguments 对象虽然看起来像数组(有索引和 length 属性),但我们知道它实际上不是一个真正的数组,因此它并没有继承数组的 slice 方法。所以直接调用 arguments.slice() 会报错
    • 为了能将数组的 slice 方法用在 arguments 对象上,我们需要借助 call 方法。call 方法可以指定函数的 this 上下文。在这种情况下,我们想把 arguments 对象作为 this 上下文来调用数组的 slice 方法
  • 在这里,我们来手写实现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(["小余","coderwhy","JS高级"],1,3)//相当于使用call来调用slice,我们上方函数里的this指向就被改变到我们手写的这个数组内容上了
console.log(newArray);
  1. Array.prototype.slice 指向数组的 slice 方法
  2. .call(arguments) 调用这个方法,把 arguments 对象作为 slice 方法的 this 上下文
  3. 因为没有提供参数给 slice,它默认返回 arguments 对象的一个浅拷贝,此时作为一个真正的数组返回。相当于借鸡生蛋的做法

2.3. 箭头函数-无arguments

  • 我们在箭头函数中是没有arguments的,如果需要的话,js会去上层作用域里面找

    • 向下方这个,arguments到上层作用域,也就是全局作用域中寻找,那在全局作用域中能不能找到arguments呢?
    • 这得分两种情况的,在node中是有的,在浏览器中则是没有(显示我们没有定义)
  • 箭头函数之所以没有 arguments 主要还是为了保持函数定义的简洁性和保证词法作用域的一致性,同时希望我们使用更现代、更表达性强的语法特性,如剩余参数

var name = "小余"

var foo = ()=>{
    console.log(name)
    console.log(arguments);
}

foo()

2.3.1. 案例

  • 在ES6中用...剩余函数来替代arguments,用来接收所有参数,形成数组
    • 在我们这个案例中对foo进行调用并传递进了内容,但foo函数返回的是bar函数
    • 而fn的执行就相当于bar函数的执行。并且我们知道bar函数是箭头函数,所以this是会往上找到foo函数的,并不会受到独立调用的影响去找到window
    • 找到了foo函数,而我们在foo函数有传递进了一个123作为参数。所以最终打印出来的才是foo函数的参数
  • 并且在我们的案例中,arguments 对象是在函数体内自动可用的,无需在函数的形参列表中显式声明,在逻辑角度上是不够清晰的
    • 箭头函数不绑定自己的 this,它们也不绑定 arguments。这意味着箭头函数内部的 thisarguments 都是从其封闭作用域(即包围箭头函数bar的常规函数foo)继承的。这种行为被称为“词法作用域”或“静态作用域”。在箭头函数中,thisarguments 的值与它们在箭头函数被定义的位置的上下文相同,而不是在箭头函数被调用时的上下文
    • 所以我们说箭头函数没有arguments,和this一样的道理。本身没有,但不代表不能用继承来的,只是箭头函数会一直往上找,如果我们箭头函数包箭头函数,最里层的箭头函数会一层层往上找,直到找到本身具备arguments的函数或者全局(全局在Node才有)为止
function foo(){
    var bar = ()=>{
        console.log(arguments);//打印出来的就是上层foo的arguments
    }
    return bar
}

var fn = foo(123)
fn()

arguments打印结果

图10-5 arguments打印结果

后续预告

  • 通过我们自己实现这三个改变this的函数,思考每一步为什么要这么做,可以领略这些思想和其中的取与舍
    • 代码本身是思想的延伸,追逐其中所透露出来的魅力让我们学习不会枯燥和乏味
    • 接下来我们将要进一步学习函数当中所蕴含的其他思想:纯函数以及柯里化
  • 函数就是函数,为什么还要加上"纯"呢?和普通的函数以及箭头函数都有哪些区别?柯里化又是什么函数的运用技巧?为什么要使用柯里化?柯里化改变了我们以往的哪些使用方式,在下一章节中,就让我们一起来探索其中所蕴藏的思想与考量吧