资讯专栏INFORMATION COLUMN

从规范去看Function.prototype.apply到底是怎么工作的?

tanglijun / 1732人阅读

摘要:如果是或者,会将作为值。否则,被调用的函数,进行转换后,作为值。又怎么操作这个很神奇。能转换它的参数为到总共个整数中的一个,这个函数遵循以下规则。不断加入新方法的规范也是这个初衷。

今天看element-react源码的时候,又看到了这张似曾相识却又异常陌生的老面孔,那就是Function.prototype.apply()...

import React from "react";
import PropTypes from "prop-types";
import classnames from "classnames";

export default class Component extends React.Component {
  classNames(...args) {
         return classnames(args);
  }

  className(...args) {
         return this.classNames.apply(this, args.concat([this.props.className]));
  }

  style(args) {
         return Object.assign({}, args, this.props.style)
  }
}

Component.propTypes = {
  className: PropTypes.string,
  style: PropTypes.object
};

虽然高设,犀牛书以及你不知道的Javascrit都看过apply的原理,但依然没什么卵用,可能是缺少实践的原因,看完就忘。

Q:
其中这句this.classNames.apply(this, args.concat([this.props.className])),又把我搞得晕头转向,this到底是谁?没猜错的话应该是组件实例吧?
那为啥不直接 this.classNames(args.concat([this.props.className])),直接传一个数组不就行了吗?
为什么一定要用this.classNames.apply(this, args.concat([this.props.className]))
同理,印象中apply用来取数组最大最小值很方便,Math.max.apply(null,[1,3,8,2]),但是Math.apply( [1,3,8,2] )却返回传参类型错误?这又是为什么?

带着这些问题,我又一次开始了啃规范之旅。

Function.prototype.apply (thisArg, argArray)
The apply method takes two arguments, thisArg and argArray, and performs a function call using the [[Call]] property of the object. If the object does not have a [[Call]] property, a TypeError exception is thrown. If thisArg is null or undefined, the called function is passed the global object as the this value. Otherwise, the called function is passed ToObject(thisArg) as the this value. If argArray is null or undefined, the called function is passed no arguments. Otherwise, if argArray is neither an array nor an arguments object (see 10.1.8), a TypeError exception is thrown. If argArray is either an array or an arguments object, the function is passed the (ToUint32(argArray.length)) arguments argArray[0], argArray[1], …, argArray[ToUint32(argArray.length)–1].

apply有2个方法,一个是thisArg,一个是argArray,然后再执行对象的[[call]]属性。如果对象没有call属性,一个TypeError会被扔出来。
如果thisArg是null或者undefined,会将global作为this值。否则,被调用的函数,进行ToObject(thisArg)转换后,作为this值。
如果argArray是null或者undefined,被调用函数不传入任何参数。否则,argArray如果既不是数组也不是arguments object(类数组对象),会报错TypeError.
如果 argArray是数组或者类数组对象,被调用函数传入ToUnit32(argArray.length) arguments argArray[0],argArray[1]一直到argArray[ToUint32(argArray.length)–1].

ToObject内部怎么操作?这个不用管。
ToUnit32又怎么操作?这个很神奇。

ToUint32: (Unsigned 32 Bit Integer)
The operator ToUint32 converts its argument to one of 232 integer values in the range 0 through 232−1,
inclusive. This operator functions as follows:

Call ToNumber on the input argument.

If Result(1) is NaN, +0, −0, +∞, or −∞, return +0.

Compute sign(Result(1)) * floor(abs(Result(1))).

Compute Result(3) modulo 232; that is, a finite integer value k of Number type with positive sign and

less than 232 in magnitude such the mathematical difference of Result(3) and k is mathematically an
integer multiple of 232.

Return Result(4).

ToUnit32能转换它的参数为0到231总共232个整数中的一个,这个函数遵循以下规则。
1.对输入参数调用ToNumber()函数
2.如果Result(1)是 NaN, +0, −0, +∞, 或者 −∞, return +0
3.计算sign(Result(1)*floor(abs(Result(1))))
4.计算Result(3) modulo 232;就是说 一个无穷的正整数值k,大于0小于232,Result(3)与k的数学差异是232的整数倍。
5.返回Result(4)

A1:
多说无益,举个例子实在:
Math.max.apply(null,[1,3,2,""])

第一步:global值替换null,浏览器环境为window
Math.max.apply(window,[1,3,2,""])

第二步:[1,3,2,""].length传入到ToUnit32()中
ToUnit32(4)

第三步:计算ToUnit32(4), 作为数组转换成arguments类数组对象的编号
1.ToNumber(4)→Result(1)=4
2.Not pass the condition
3.Math.sign(4*Math.floor(Math.abs(4))) →Result(3)=1
4.1 Mod 232 → Result(4) =1
5.return 1

第四步:传入arguments到Math.max中
第三步会生成(1)arguments 类数组对象(是否为纯函数方式生成暂时未知),将这个arguments传入到Math.max中,与直接这样写Math.max(1,3,2,"")的效果一样,此时就会返回最大值3。

这里有一个很重要的坑,为什么arguments传入,也可以像正常Math.max(1,3,2,"")一样?
其实这个坑是因为Math.max(1,3,2,"")这个例子中的实参,在函数内部的实参就是arguments,且这个arguments的长度为4,这和绝大多数的函数一样,除箭头函数外的所有函数,都有这个arguemnts实参array-like object。

看完这个, Math.max.apply(null,[1,3,8,2])的内部原理应该也就清楚了吧。

A2:
回到源代码中this.classNames.apply(this, args.concat([this.props.className]))
1.为什么直接传入["foo","bar"]数组不行?
因为当我们传入["foo","bar"]到classNames时,此时return classnames(args);中的args会变成一个二维数组[["foo","bar"]],与classnames模块的预期值类型一维数组相悖。
2.为什么调用Function.prototype.apply后就可以?
因为如规范中的第三步中所示, this.classNames.apply(this, args.concat([this.props.className]))中,在apply的内部算法中,会将args.concat([this.props.className])这个数组,转换为一个实参类数组对象arguments,跳过形参赋值步骤,直接深入到 this.classNames()内部,这样一来,就实现了一个一个传值的效果,其中的...args就会成为一个一个独立的参数,而args会作为实参数组传给classnames函数。

说了这么多,总结起来其实就一句话:

callObject.method.apply(thisArg,thisArray),可以将thisArray转化为arguments,传入到callObject.method内部。

加粗加斜的这句真的很重要!
加粗加斜的这句真的很重要!
加粗加斜的这句真的很重要!

参考:
1.The mathematical function sign(x) yields 1 if x is positive and −1 if x is negative. The sign function is not
used in this standard for cases when x is zero
2.求模
对于整数a,b来说,取模运算或者求余运算的方法要分如下两步:
1.求整数商:c=a/b
2.计算模或者余数:r=a-(c*b)
求模运算和求余运算在第一步不同
取余运算在计算商值向正无穷方向舍弃小数位
取模运算在计算商值向负无穷方向舍弃小数位
例如:4/(-3)约等于-1.3
在取余运算时候商值向0方向舍弃小数位为-1
在取模运算时商值向负无穷方向舍弃小数位为-2
所以
4rem(-3)=1
4mod(-3)=-2
3.C和JS中%表示取余,python中表示取模
python中%表示取模,have a try 1 Mod 232 →1

Merry Christmas ~
That"s it !

2019.8.20更新

js忍者秘籍给出的精简解释是:“js可以通过apply和call显示指定任意对象作为其函数上下文。”强烈建议阅读P52~P55。言简意赅,通俗易懂。

主要有两个用途:

普通函数中指定函数上下文

回调函数中强制指定函数上下文

回调函数强制指定函数上下文很好地体现了函数式编程的思想,创建一个函数接收每个元素,并且对每个元素做处理。

本质上,apply和call都是为了增强代码的可扩展性,提升编程的效率。

我想这也是js中每一个方法或者api的初衷,提供更加便利的操作,解放初更多的生产力。不断加入新方法的es规范也是这个初衷。

由于我使用vue比较多,所以根据以上的应用场景出1个单文件组件示例和1个普通示例供参考:

// 普通函数中指定函数上下文
// 通过Math.max()获得数组中的最大项
// 回调函数中强制指定函数上下文
// 手动实现一个Array.prototype.filter
const numbers = [1, 2, 3, 4];
function arrayFilter(array, callback) {
  const result = [];
  for (let i = 0; i < array.length; i++) {
    const validate = callback.call(array[i], array[i]);
    if (validate) {
      result.push(array[i]);
    }
  }
  return result;
}

const evenArrays = arrayFilter(numbers, (n) => n % 2 === 0);
console.log(evenArrays);// [2, 4]

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

SegmentFault专栏:趁你还年轻,做个优秀的前端工程师

Github博客: 趁你还年轻233的个人博客

掘金主页:趁你还年轻233

SegmentFault技术圈: ES新规范语法糖

知乎专栏:趁你还年轻,做个优秀的前端工程师

前端开发交流群:660634678

努力成为优秀前端工程师!

加油,前端同学们!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90394.html

相关文章

  • 规范去看Function.prototype.call到底怎么工作

    摘要:抽象操作是在调用函数对象的内部的方法。指的是调用函数,指的是的值,然后是传入到内部方法相应参数的值。切换上下文执行,为函数调用栈在尾部调用函数做准备,切换运行中执行上下文,实现上下文的动态改变。万事具备,执行,调用函数即可。 showImg(https://segmentfault.com/img/bVbwBui?w=657&h=382); 今天在学习前端工程化的过程中,遇到一个是实验...

    DandJ 评论0 收藏0
  • 【2】this

    摘要:否则如果是或,则设绑定为全局对象。令为解释执行的结果。返回一个值类型的引用,其基值为且其引用名为,严格模式标记为。进入函数代码,为,非严格模式下将赋值为全局对象。内置函数如何使用的内置函数修改是通过给的内置方法传递来实现的。 this 说到this,需要明确三方面内容: this何时被赋值 this被赋了什么值 内置函数如何使用this的 this何时被赋值 进入函数代码 当控制流...

    Alex 评论0 收藏0
  • 深入call apply bind

    摘要:众所周知,这三个函数都是改变执行上下文的,那么我们来捋一捋,这些函数内部到底做了什么。 前言 稍微翻了一下call,apply, bind 的各种论坛上的文章, 发现讲的都太浅了,大部分都只讲了个用法, 对于实现的原理却都没有提,因此,在这里,我写下这篇文章, 希望能让大家认识到原理所在。 众所周知, 这三个函数都是改变执行上下文的 , 那么我们来捋一捋,这些函数内部到底做了什么。 c...

    Alex 评论0 收藏0
  • javascript中Function、ArrowFunction和GeneratorFunctio

    摘要:等价与注意如果构造函数有自己的返回,那么情况有所不同。,定义了的属性,默认是声明的函数名,匿名函数是。匿名函数表达式和函数声明都不会创建匿名作用域。 ECMAScript规范中对Function的文档描述,我认为是ECMAScript规范中最复杂也是最不好理解的一部分,它涉及到了各方面。光对Function就分了Function Definitions、Arrow Function D...

    cyixlq 评论0 收藏0
  • 看完这篇文章还不会call/apply/bind来找我。

    摘要:关于的指向的问题请参照我的学习笔记。那么在这里事实上都改变了函数方法被调用时的指向。那么回调函数在执行的时候指向还是。大家看完之后应该已经懂了把还是不懂的话在评论区留言,我给大家解答。 先从一个小题目开始吧: 要实现一个加法函数,这个时候向函数当中传递个数大于0的若干个整形数据,求所有这些数据的和。 Function.prototype.call Function.prototype...

    Java3y 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<