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没有半毛钱关系
相关推荐
Captaincc22 分钟前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚1 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿1 小时前
MCP
前端
layman05281 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝1 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML1 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia3111 小时前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生2 小时前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇2 小时前
一文搞定CSS Grid布局
前端
0xHashlet2 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端