前言
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']
- Function.prototype.bind
- 函数绑定运算符
::
javascript
obj::func;
//等同于:
func.bind(obj)
但是这个函数绑定运算符最后的活动迹象是2015年,而且还是 stage-0 阶段,虽然 babel也有插件@babel/plugin-proposal-function-bind 支持。
表象
Function.prototype.call 与 Function.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 )的第三个参数。

从上可以得出结论:
- 此时的
localEnv
等于LexicalEnvironment
,也就是函数环境记录,具体的初始化过程可以参见 PrepareForOrdinaryCall ( F, newTarget )

- 箭头函数,不会在函数对应的函数环境记录创建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,是第一次绑定的值, 更多详情参见之前的 特异对象章节。
结论
- bind返回的函数绑定特异对象会取自身
[[BoundTHis]]
的值作为预期的this绑定关系的值, 如果函数不是箭头函数,会产生绑定关系。 - 外面传入的
thisArgument
会被无视,可以参见本小节开头示例代码 - 多次
bind
, 第一次绑定的 this的值生效 - 箭头函数执行
bind
之后的生成的函数绑定特异对象,不会改变this的值
思考
如果执行 F.bind().bind()
, 内部的执行逻辑是怎么样的呢?
this优先级
Function.prototype.call
, Function.prototype.apply
, Function.prototype.bind
操作,从逻辑上来说,改变的都是传入OrdinaryCallBindThis ( F, calleeContext, thisArgument ) 的thisArgument
的值。
而OrdinaryCallBindThis方法内部,一旦检测到函数是箭头函数,根本不会在对应的函数环境记录上初始化this绑定关系,那么自然什么都不会改变,期望自然得不到回应。
- 如果是箭头函数,不管是
call
,apply
,bind
的期望都不会得到回应 bind
之后返回的函数绑定特异对象,进行调用时会无视第一个传入的参数thisArgument
call
和apply
的期望,在非函数绑定特异对象,箭头函数 外,才能得到回应- 严格模式下,期望是什么,this就是什么
- 非严格模式
- 如果是undefined或者null, this等于全局对象
- 包装为Object期望值
检查期望的this是否会生效
如果函数进行 call
, apply
,想知道期望的 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的逻辑如下:
- EvaluateNew ( constructExpr, arguments ) =>
- Construct ( F [ , argumentsList [ , newTarget ] ] ) =>
- 函数的[[Construct]] ( argumentsList, newTarget )
上面的newTarget
和 Reflection有关,本节不包含此场景。
[[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
,apply
,bind
都值是表示函数调用时期望this是什么值,如果是箭头函数,期望不会得到响应的bind
之后返回的函数绑定特异对象,进行调用时会无视第一个传入的参数thisArgument
。
可以理解为bind
的期望优先级会高于call
和apply
的期望。call
和apply
的期望,在非函数绑定特异对象,箭头函数外 ,才能得到回应- 严格模式下,期望是什么,this就是什么
- 非严格模式
- 如果是undefined或者null, this等于全局对象
- 包装为Object期望值
- 函数绑定特异对象被new时
- 最终被执行的是原函数
- 执行时,this 和
bind
传入的this没有半毛钱关系