ECMAScript 函数this全解析 下

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

听话,用我做this

在上篇中提到了函数正常调用的两种场景, 可以理解为遵循了协议的 潜规则去调用函数,this 需要按照 潜规则去动态查找。

而本文相反,是显式的告诉函数,听话,用我指定的值作为this使用,实际听话还是不听话呢, 那还不好说, 嗷,就是这么桀骜不驯。

这里可能会有人会跳出来说, 箭头函数不算显式嘛,不算,顶多算是显式说该函数创建的函数环境记录不提供 this。 至于this到底是谁,一个字, 查。

显式的方式呢,常见的有:

javascript 复制代码
function log(...messages){ 
  console.log(this.eName,messages); 
}
log.call({eName:"eName"}, "msg1", "msg2") // eName ['msg1', 'msg2']
javascript 复制代码
function log(...messages){ 
  console.log(this.eName,messages); 
}
log.apply({eName:"eName"}, ["msg1", "msg2"]) // eName ['msg1', 'msg2']
javascript 复制代码
obj::func;
//等同于:
func.bind(obj)

但是这个函数绑定运算符最后的活动迹象是2015年,而且还是 stage-0 阶段,虽然 babel也有插件@babel/plugin-proposal-function-bind 支持。

表象

Function.prototype.callFunction.prototype.apply

  • 除了传参,没有区别

普通函数

javascript 复制代码
function log(...messages){ 
  console.log(this.eName,messages); 
}

var obj = {eName:"eName"};
obj.log = log;
obj.log("msg1", "msg2");            // eName ['msg1', 'msg2']
log.call(obj,"msg1", "msg2");       // eName ['msg1', 'msg2']
log.apply(obj, ["msg1", "msg2"]);   // eName ['msg1', 'msg2']

箭头函数

javascript 复制代码
window.eName = "global eName"
const log = (...messages) => { 
  console.log(this.eName,messages); 
}

var obj = {eName:"eName"};
obj.log = log;
obj.log("msg1", "msg2");          // global eName  ['msg1', 'msg2']           
log.call(obj,"msg1", "msg2");	// global eName  ['msg1', 'msg2']
log.apply(obj, ["msg1", "msg2"]); // global eName  ['msg1', 'msg2']

看完表现,跟着协议走一遭,看看其协议逻辑。

Function.prototype.call

跟着协议,一起先看看Function.prototype.call的第一个thisArg的传递链路

回顾一下上一篇文章的OrdinaryCallBindThis ( F, calleeContext, thisArgument ),函数调用准备阶段this绑定关系设置过程,Function.prototype.call的第一个参数,是会顺利传递给 OrdinaryCallBindThis ( F, calleeContext, thisArgument )的第三个参数。

从上可以得出结论:

  • 箭头函数,不会在函数对应的函数环境记录创建this绑定关系(上上图标注1)
  • 如果成功设置了,执行过程的this就是this绑定关系的值

其实到这里,逻辑就很清晰了:

  • Function.prototype.call第一个参数能够绑定到函数环境记录的ThisValue,的前提是函数本身不是箭头函数
  • Function.prototype.call和函数作为某个对象的属性被调用没有太大区别,都只是给底层传递了期望的this绑定关系的值。 有些同志可能会说,不一样啊,call可以传入 null和undefined,其对应的就是 作为全局对象的属性被调用。
javascript 复制代码
function log(...messages){ 
  console.log(this.eName,messages); 
}

log.call(null);
globalThis.log = log;  // undefined []
globalThis.log();      // undefined []

Function.prototype.apply

这个就简单了,第一个参数(thisArg)和Function.prototype.call传值和底层处理逻辑完全一样。

Function.prototype.bind

先看个简单的例子

javascript 复制代码
window.eName = 'global eName';
function log(...messages){ 
  console.log(this.eName,messages); 
}

const arrowLog = (...messages) => {
   console.log(this.eName,messages); 
}

const log2 = log.bind({eName:'bind eName'}, "msg1");
log2("msg2");                              // bind eName  ['msg1', 'msg2']
// call时无视传入的this
log2.call({eName:'custon eName'}, "msg2")  // bind eName  ['msg1', 'msg2']

// 原型比较
Object.getPrototypeOf(log) === Object.getPrototypeOf(log2)   // true

// 箭头函数bind无效
arrowLog.bind({eName: 'bind eName'})()  // global eName []

从表象来看,bind返回的也是一个函数对象,本质上说,这个返回的函数对象和普通的函数对象还不一样。 其是 绑定函数特异对象Bound Function Exotic Objects

更多关于绑定函数特异对象Bound Function Exotic Objects,可以阅读章节 Built-in Exotic Object 。

  • 其和最初的函数(本例为log)有一样的原型
  • 在内部又多了一些属性,如下用来保存额外的信息,原函数,this的值,预设的参数。
  • 其自己还定义了[Call]方法,被调用时的逻辑和普通函数是有区别的。

要想知道最终的this的值,必然要知道其调用的背后逻辑。

Bound Function Exotic Object被调用的代码路径如下

[[Call]] ( thisArgument, argumentsList ) Bound Function Exotic Object的call

=> Call ( F, V [ , argumentsList ] ) 抽象的Call逻辑

=> [[Call]] ( thisArgument, argumentsList ) Function本身的call

=> OrdinaryCallBindThis ( F, calleeContext, thisArgument ) 绑定this

=> OrdinaryCallEvaluateBody ( F, argumentsList ) 执行脚本

其后面的流程是完全一致的,关键就在于函数绑定特异对象自身的 [[Call]] ( thisArgument, argumentsList ), 如下图:

虽然你依旧可以传入thisArgument, 可其压根就不会被使用,而是使用自身的 [[BoundThis]]作为一路传递给 OrdinaryCallBindThis ( F, calleeContext, thisArgument )

OrdinaryCallBindThis 内部的逻辑是如果不是箭头函数,会在环境记录上产生this绑定关系,这个绑定关系的值会是代码执行的this的值。

所以呢,如果函数不是箭头函数,期望会被回应,期望的this绑定会生成。

当然,某个函数进行bind之后的返回值,还可以继续进行bind,只不过,能生效的this,是第一次绑定的值, 更多详情参见之前的 特异对象章节。

结论

  1. bind返回的函数绑定特异对象会取自身 [[BoundTHis]]的值作为预期的this绑定关系的值, 如果函数不是箭头函数,会产生绑定关系。
  2. 外面传入的 thisArgument会被无视,可以参见本小节开头示例代码
  3. 多次bind, 第一次绑定的 this的值生效
  4. 箭头函数执行bind之后的生成的函数绑定特异对象,不会改变this的值

思考

如果执行 F.bind().bind(), 内部的执行逻辑是怎么样的呢?

this优先级

Function.prototype.call, Function.prototype.apply, Function.prototype.bind操作,从逻辑上来说,改变的都是传入OrdinaryCallBindThis ( F, calleeContext, thisArgument )thisArgument的值。

OrdinaryCallBindThis方法内部,一旦检测到函数是箭头函数,根本不会在对应的函数环境记录上初始化this绑定关系,那么自然什么都不会改变,期望自然得不到回应。

  1. 如果是箭头函数,不管是call, applybind的期望都不会得到回应
  2. bind 之后返回的函数绑定特异对象,进行调用时会无视第一个传入的参数 thisArgument
  3. call apply的期望,在非函数绑定特异对象,箭头函数 外,才能得到回应
    1. 严格模式下,期望是什么,this就是什么
    2. 非严格模式
      1. 如果是undefined或者null, this等于全局对象
      2. 包装为Object期望值

检查期望的this是否会生效

如果函数进行 callapply,想知道期望的 this 是否会生效,该怎么做呢?

诶,核心科技,检查:

  • 不是函数绑定特异对象
  • 不是箭头函数
javascript 复制代码
// 是否是函数绑定特异对象
function isBoundFunction(fn){
  if(typeof fn !== 'function'){
    return false
  }
  // 函数名: "bound 原函数名"
  // 函数绑定特异对象 没有prototype
  return fn.name.startsWith("bound ") && !Object.hasOwnProperty.call(fn, "prototype")
}

// https://github.com/inspect-js/is-arrow-function/blob/main/index.js
// 是不是箭头函数
var fnToStr = Function.prototype.toString;
var isNonArrowFnRegex = /^\s*function/;
var isArrowFnWithParensRegex = /^([^)]*) *=>/;
var isArrowFnWithoutParensRegex = /^[^=]*=>/;

function isArrowFunction(fn) {
	if (typeof fn !== 'function') {
		return false;
	}
	var fnStr = fnToStr.call(fn);
	return fnStr.length > 0
		&& !isNonArrowFnRegex.test(fnStr)
		&& (isArrowFnWithParensRegex.test(fnStr) || isArrowFnWithoutParensRegex.test(fnStr));
};

// 是否可以使用自定义的this
function canUseCustomThis(fn){
  return !isBoundFunction(fn) && !isArrowFunction(fn)
}


canUseCustomThis((a,b)=>{});                    // false
canUseCustomThis(a=>{});                        // false
canUseCustomThis(function a(){});               // true
canUseCustomThis((function a(){}).bind());      // false

new时的this

new 后面的操作数是一个函数对象,

  • 普通函数是函数对象
  • 函数绑定特异对象也是函数对象
  • class 也是函数对象
  • 箭头函数也是函数对象

而箭头函数是不可被new的

javascript 复制代码
const arrowFn = ()=> {};
new arrowFn()   // Uncaught TypeError: arrowFn is not a constructor

之前应该有提到过,怎么识别函数是不是可以被new呢,是不是构造函数呢?

很简单,如果函数有 [[Construct]] 内部方法,就是构造函数,可以被new。

普通函数和class都可以被new,又怎么识别,通过函数的另外一个属性 [[ConstructorKind]]

属性 说明
[[ConstructorKind]] base or derived 函数是否为派生类构造函数。base为普通函数derived派生类构造函数。

普通函数对象和特异函数对象的执行逻辑是区别的, 但是最终结果一致。

普通函数构造函数

普通函数new的逻辑如下:

上面的newTargetReflection有关,本节不包含此场景。

[[Construct]] ( argumentsList, newTarget )的内容如下:

本节未涉及class场景,所以 kind的值都是 base,注意上面标注的

  • 默认的 thisArgument 是基于构造函数创建的一个对象,其原型等于构造函数或者内置对象的prototype属性。
  • 箭头函数不可作为构造函数,所以 OrdinaryCallBindThis(F, calleeContext, thisArgument) 这一步肯定会在函数环境记录上创建 this绑定关系
  • 之后上下文 ResolveThisBinding() 时就会从函数环境记录中拿到 上面的 this绑定的值。

到这里就应该知道 new 运算符情况,函数里面的this 是:一个对象,其原型为构造函数的prototype属性。

javascript 复制代码
const log = console.log;
function Person(){
    this.name = 'name'; 
    // this的原型 是构造函数的prototype属性
    log(Person.prototype === Object.getPrototypeOf(this)); // true
    log(Person.prototype === this.__proto__);              // true
    log(this.getName());                                   // name
}
Person.prototype.getName = function(){ return this.name }
new Person(); 

函数绑定特异对象

函数绑定特异对象的执行逻辑比普通函数更复杂一点,这是因为函数绑定特异对象有自己的 [[Constrcut]], 调用链路如下:
13.3.5.1.1 EvaluateNew ( constructExpr, arguments ) =>
7.3.14 Construct ( F [ , argumentsList [ , newTarget ] ] ) =>

函数绑定特异对象的 10.4.1.2 [[Construct]] ( argumentsList, newTarget ) =>

函数的 7.3.14 Construct ( F [ , argumentsList [ , newTarget ] ] ) =>

函数真构造逻辑10.2.2 [[Construct]] ( argumentsList, newTarget )

最终执行的是原函数

重点关注 函数绑定特异对象的10.4.1.2 [[Construct]] ( argumentsList, newTarget )

本章均先不涉及Reflect(反射), 最初的newTarget没有值,所以最后传给

F.[[Construct]](argumentsList, newTarget) 的 newTarget最终等于原始函数。

其最终行为和原始函数一致。

构造函数执行时this是内置创建的对象

构造函数执行时this是内置创建的对象,压根和bind的this没有半毛钱关系, 如下图

所以 如下的this.name才会是 undefined

javascript 复制代码
const log = console.log;
function Person(){
    log(Person.prototype === Object.getPrototypeOf(this))
    log(this.name);                                   
}
const Person2 = Person.bind({name: 'bName'})
new Person2(); // true  undefined

const Person3 = Person2.bind({name: 'bbName'});
new Person3();  // true  undefined

所以呢,函数绑定特异对象,在new运算符操作时,表现得和原函数一模一样,this的值当然也是。

小结

  • 箭头函数在其对应的函数环境记录上不会产生this绑定关系,所以用户执行过程中需要从外层的环境记录中取查找 this绑定关系。
  • call, applybind都值是表示函数调用时期望this是什么值,如果是箭头函数,期望不会得到响应的
  • bind 之后返回的函数绑定特异对象,进行调用时会无视第一个传入的参数 thisArgument
    可以理解为bind的期望优先级会高于 callapply的期望。
  • call apply的期望,在非函数绑定特异对象,箭头函数外 ,才能得到回应
    1. 严格模式下,期望是什么,this就是什么
    2. 非严格模式
      1. 如果是undefined或者null, this等于全局对象
      2. 包装为Object期望值
  • 函数绑定特异对象被new时
    • 最终被执行的是原函数
    • 执行时,this 和 bind传入的this没有半毛钱关系
相关推荐
apcipot_rain4 小时前
【应用密码学】实验五 公钥密码2——ECC
前端·数据库·python
油丶酸萝卜别吃4 小时前
OpenLayers 精确经过三个点的曲线绘制
javascript
ShallowLin5 小时前
vue3学习——组合式 API:生命周期钩子
前端·javascript·vue.js
Nejosi_念旧5 小时前
Vue API 、element-plus自动导入插件
前端·javascript·vue.js
互联网搬砖老肖5 小时前
Web 架构之攻击应急方案
前端·架构
pixle05 小时前
Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
前端·3d·echarts
麻芝汤圆6 小时前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
juruiyuan1118 小时前
FFmpeg3.4 libavcodec协议框架增加新的decode协议
前端
Peter 谭8 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
周胡杰9 小时前
鸿蒙接入flutter环境变量配置windows-命令行或者手动配置-到项目的创建-运行demo项目
javascript·windows·flutter·华为·harmonyos·鸿蒙·鸿蒙系统