前言
最近有同学报告说报表下载不了。我用Chrome看了一下,一个代码报出了一个调用栈错误,然后, 我又用firefox
看了一下,功能正常,没有报错。
这个错误截图如下:
代码
引发这个错误的代码是apply函数,如下:
1 |
|
让我很是不解,不解的是:
- 为什么报出来的是调用栈错误,因为一般在死循环的情况下会出现此错误,代码里并没有循环。
- 为什么 firefox没有出错呢?
虽然前端er可能都知道apply,但还是想罗嗦一下:
apply
是Function对象原型上的方法,即Function.prototype.apply
,所有的function对象都可以调用它,它可以用来指定函数调用时的上下文(this
指向),它还有一个兄弟叫call(Function.prototype.call)
.具体用法可以通过MDN查看
apply函数的实现规范
代码里并没有循环,所以是apply的底层实现里使用了循环吗?为了搞清楚这个错误的来源,我去探寻了apply函数的ECMAScript规范,规范说明如下:
Function.prototype.apply (thisArg, argArray)
当以thisArg
和argArray
为参数在一个func
对象上调用apply
方法,采用如下步骤:
- 如果
IsCallable(func)
是false
, 则抛出一个TypeError
异常 .- 如果
argArray
是null
或undefined
, 则
a. 返回提供thisArg
作为this
值并以空参数列表调用func
的 [[Call]] 内部方法的结果。- 如果
Type(argArray)
不是Object
, 则抛出一个TypeError
异常 .- 令
len
为以 “length” 作为参数调用argArray
的 [[Get]] 内部方法的结果。- 令
n
为ToUint32(len)
.- 令
argList
为一个空列表 .- 令
index
为0
.- 只要
index < n
就重复
a. 令indexName
为ToString(index)
.
b. 令nextArg
为以indexName
作为参数调用argArray
的 [[Get]] 内部方法的结果。
c. 将nextArg
作为最后一个元素插入到argList
里。
d. 设定index
为index + 1
.- 提供
thisArg
作为this
值并以argList
作为参数列表,调用func
的 [[Call]] 内部方法,返回结果。 在外面传入的 thisArg 值会修改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。
整个规范看起来,可能不是那么直观。我们通过自己的代码简易实现一下就是:
1 |
|
在以上规范说明和代码中,也没有使用循环调用。那到底是什么原因导致了我们的调用栈错误呢?
灵机一动
这个时候我突然注意到thisArg[_innerThisKey](...argArray)
这句话,如果参数过多的话,是否也会导致调用栈溢出呢?带着这个问题,我去探寻了一下函数参数的限制。
ECMA规范中并没有对函数的参数作限制,但是浏览器有:
- 在
Chrome(80)
中, 对函数参数个数限制为 217-10759,也就是120313
,如果超出了,则会报告Uncaught RangeError: Maximum call stack size exceeded
- 在
macOS safari(13)
中,对函数参数个数限制为 216, 也就是65536
,如果超出了,则会报告RangeError: Maximum call stack size exceeded.
- 在
firefox(72)
中,函数参数个数限制为 219-24288, 也就是500000
,如果超出了,则会报告RangeError: too many function arguments
回到代码看问题
问题已经基本清楚了。
回头看看我们出问题的代码, 这段代码是将一个文件的内容读出来然后进行转码,在使用apply函数的过程中,由于文件内容过大达到了240k,导致了apply
的argArray
参数过于长,浏览器在apply
底层处理时抛出了调用栈错误。
所以只需要规避这个问题即可。
总结
这个问题,其实是浏览器的锅,不是apply的锅。
当我们碰到问题时,应该有刨根问底的决心和毅力,这样我们就会有长足的进步。
共勉。