JavaScript引擎执行包装函数步骤及包装函数this指向解析
注:示例代码存在一处笔误,计算执行时长的语句中
stop.getTime()应为end.getTime(),否则运行时会抛出stop is not defined的引用错误,以下分析基于修正后的代码展开。
一、包装函数的完整执行步骤
JS引擎执行这段代码的流程完全遵循ECMAScript标准的执行上下文、作用域规则,可拆为3个核心阶段:
- 全局上下文初始化阶段
引擎先扫描全局范围内的声明做变量提升:function声明的profile会被直接初始化为函数对象,var声明的profiledMax初始化为undefined,同时profile的词法作用域绑定为全局作用域。 profile函数调用阶段
执行var profiledMax = profile(Math.max, 'Math.max')时,引擎为profile创建新的执行上下文并入栈:- 绑定形参:
func指向传入的原生Math.max函数,funcName绑定为字符串'Math.max' - 创建内部要返回的匿名函数,这个匿名函数的内部属性
[[Environment]]会指向当前profile上下文的词法环境——这就是闭包的核心:哪怕后续profile执行完出栈,只要这个匿名函数还被引用,它就能一直访问到func和funcName两个形参的值,这部分内存不会被垃圾回收 profile执行完毕,把返回的匿名函数赋值给全局变量profiledMax,自身执行上下文出栈
- 绑定形参:
- 包装后函数调用阶段
执行profiledMax.call(Math, 1, 2)时,引擎为返回的匿名闭包创建执行上下文并入栈:- 由于用了
call显式绑定,这个匿名函数执行时的this被设置为传入的Math对象,类数组参数arguments绑定为[1, 2] - 逐行执行匿名函数内部逻辑:
- 初始化
start为当前时间的Date实例 - 执行
func.apply(this, arguments):把闭包当前的this、参数透传给闭包保存的原函数Math.max,执行后拿到返回值2,赋值给returnVal - 初始化
end为原函数执行完成后的当前时间Date实例 - 计算时间差得到执行时长
- 打印耗时日志
- 把
returnVal作为匿名函数的返回值返回
- 初始化
- 匿名函数执行完毕,执行上下文出栈,控制权回到全局上下文。
- 由于用了
二、被包装函数延后执行时的this判定规则
首先明确核心原则:JS里所有函数的this都是在实际执行的瞬间才完成绑定的,和函数定义在哪、被赋值给哪个变量、被传递过多少次没有任何关系,判定时按优先级从高到低匹配即可,不存在例外:
- new绑定(最高优先级):如果函数是通过
new关键字调用的,不管之前做过什么绑定,this都指向new新创建的实例对象 - 显式绑定:如果函数通过
call/apply/bind指定了this,this就指向传入的第一个参数;如果传入的是null/undefined,非严格模式下会自动替换为全局对象(浏览器为window,Node.js为globalThis),严格模式下保留原值。示例里的profiledMax.call(Math, 1,2)就属于这类,所以闭包内的this是Math对象 - 隐式绑定:如果函数作为对象的属性,通过
对象.函数()的形式调用,this指向点前面的那个对象。比如写const testObj = { getMax: profiledMax }; testObj.getMax(1,2),那profiledMax执行时的this就指向testObj - 默认绑定(最低优先级):如果函数不符合以上三种调用方式,直接独立运行,非严格模式下
this指向全局对象,严格模式下this为undefined
你写的这个profile是典型的函数装饰器实现,里面用func.apply(this, arguments)就是为了把包装函数收到的this和参数原封不动透传给原函数,保证包装后的函数和原函数的this行为完全一致,不会因为加了耗时统计逻辑就改变原函数的调用规则。
内容的提问来源于stack exchange,提问作者sens
相关产品推荐
相关产品推荐

