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

位流蝉翼
• 阅读 2366

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

今天看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:

  1. Call ToNumber on the input argument.
  2. If Result(1) is NaN, +0, −0, +∞, or −∞, return +0.
  3. Compute sign(Result(1)) * floor(abs(Result(1))).
  4. 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.

  1. 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()获得数组中的最大项
<script>
function maxNumber(...args) {
  this.maxNumber = Math.max.apply(null, args);
}
export default {
  data() {
    return {
      applyTest: {
        numbers: [1, 2, 4, 5, 3],
      },
    }
  },
  created() {
    maxNumber.apply(this.applyTest, 
    this.applyTest.numbers);
    console.log(this.applyTest); // {"numbers": [1,2,4,5,3],"maxNumber": 5}
  },
}
</script>
// 回调函数中强制指定函数上下文
// 手动实现一个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]

2019.8.23更新

在上面的示例中,本质上是call,apply实现伪造对象继承。
this.applyTest伪造继承了MaxNumber类,从而新建出单独包含maxNumber属性的实例。
array[i]伪造继承了callback类,从而新建出每一个传入参数之后的validate实例。

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

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

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

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
SQL利用函数或存储过程求男或女的总分平均分
!(https://oscimg.oschina.net/oscnet/633e11621f3e13e713cf063db00d72c8aa0.png)函数alterfunctionxb(@xingbievarchar(2))returnstableas
梦
4年前
微信小程序new Date()转换时间异常问题
微信小程序苹果手机页面上显示时间异常,安卓机正常问题image(https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/b691e1230e2f15efbd81fe11ef734d4f.png)错误代码vardate'2021030617:00:00'vardateT
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这