深入理解 this
this 在 JavaScript 中的指向一般都是指向调用者,如果没有调用者则指向 undefined ,非严格模式下指向 undefined 会默认指向全局对象(window)。
上述指向是指一般情况,采用 call
、apply
、bind
函数进行特殊处理后,可以修改原有的 this 指向。
那么问题来了,除却特殊调用,this 真的默认指向调用者么?
本文会先阐述两个不相关的内容 Type(类型),Reference(引用类型), 在阐述在函数中 this 指向的规则处理。
Type
在ECMA规范的第八节中:类型又分为语言类型和规范类型
- 语言类型:字符串(String),数值(Number),对象(Object),布尔值(Boolean) 等等
- 规范类型:是描述 ECMA 语言构造与 ECMA 语言类型语意的算法所有的元值对应的类型。规范类型包括引用类,列表,完结,属性描述式,属性标示,词法环境,环境记录。规范类型可用来描述 ECMA 表示运算的中途结果,但是这些值不能存为对象的变量或是 ECMA 语言变量的值
从规范上可以知道,语言类型是我们使用 JS 的过程中可以声明及获取到的类型,规范类型是存在与 ECMA 中的,是为了维持 JS 引擎的运行而存在的,不能外部直接获取。
Reference(引用类型)
ECMA 在8.7节中提到:引用类型是用来说明 delete,typeof,赋值运算符这些运算符的行为。 一个 Reference(引用) 是个已解决的命名绑定。一个引用由三个部分组成
- base:基值。可以是 undefined,Object,Boolean,String,Number,Environment record(环境记录项)
- reference name:引用名称。字符串(String)
- strict reference:严格引用标志。是一个 布尔值(Boolean)
可以参考下面这个对象
var foo = 1;
// 对应的 Reference 是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
Reference(引用类型) 有几个方法,仅作了解(ECMA内部判断使用,外部无法调用)
- GetBase(V)。返回引用值 V 的基值。返回一个 reference 的 base
- GetReferencedName(V)。返回引用值 V 的引用名称。返回一个 reference 的 reference name
- IsStrictReference(V)。返回引用值 V 的严格引用值。返回一个 reference 的 strict
- HasPrimitiveBase(V)。如果基值是 Boolean,String,Number,那么返回 true
- IsPropertyReference(V)。如果基值是个对象或 HasPrimitiveBase(V) 方法是 true,那么返回 true,否则返回 false
- IsUnresolveableReference(V)。如果基值是 undefined 那么返回 true ,否则返回 false
GetValue
- 如果 Type(V) 不是 Reference(引用类型),则返回 V
- 使 base 为调用 GetBase(V) 的返回值
- 如果 IsUnresolveableReference(V) 为 true,则抛出一个 ReferenceError 异常
- 如果 IsPropertyReference(V) 为 true,那么
- 如果 HasPrimitiveBase(V) 是 false,那么使 get 为 base 的 [[Get]] 内部方法,否则使 get 为下面定义的特殊的 [[Get]] 内部方法
- 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法。返回结果
- 如果 IsPropertyReference(V) 为 false,base 必须是一个 environment record(环境记录)
- 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue 具体方法,返回结果
比较复杂,我采用了代码形式来表示了一下上述逻辑
// 伪逻辑
function GetValue(V) {
// 如果 Type(V) 不是 Reference(引用类型),则返回 V
if (Type(V) !== Reference) {
return;
}
// 使 base 为调用 GetBase(V) 的返回值
var base = GetBase(V);
// 如果 IsUnresolveableReference(V) 为 true,则抛出一个 ReferenceError 异常
if (IsUnresolveableReference(V)) {
throw Error('ReferenceError 异常');
}
// IsPropertyReference(V) 为 true
if (IsPropertyReference(V)) {
// 如果 HasPrimitiveBase(V) 是 false
if (!HasPrimitiveBase(V)) {
// 使 get 成为 base 的 [[Get]] 内部方法
base.[[Get]] = get;
// 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法。返回结果
return base.[[Get]](GetReferencedName(V));
}
}
// IsPropertyReference(V) 为 false
else {
// 如果 IsPropertyReference(V) 为 false,base 必须是一个 environment record(环境记录)
base = 'environment record';
// 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue 具体方法,返回结果
return base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
}
}
由上面可以看出,GetValue 调用内部的方法获取其 V 的参数值,而这个值,不是 Reference(引用类型) ,而是一个具体的值。
如何确定 this 值?
ECMA 第11.2.3 节:这章节说明了函数调用的情况
- 使 reference 为解释执行 MemberExpression 的结果
- 使 function 为 GetValue(reference)
- 使 argList 为解释执行 Arguments 的结果
- 如果 Type(function) 不是一个 Object 类型,抛出一个 TypeError 的异常
- 如果 IsCallable(function) 是 false,抛出一个 TypeError 的异常
- 如果 Type(reference) 为 Reference 类型,那么如果 IsPropertyReference(reference) 为 true,那么令 thisValue 为 GetBase(reference) 的 ImplictitThisValue 的具体方法的结果
- 否则,Type(reference) 不是 Reference,令 thisValue 为 undefined
第一条 MemberExpression 规范里11.2 左值表达式说明了
- PrimaryExpression:原始表达式
- FunctionExpression:函数定义表达式
- MemberExpression [Expression]:中括号属性访问表达式。obj [name]
- MemberExpression . IdentifierName:. 属性访问表达式。obj . name
- new MemberExpression Arguments:new 关键字
第六条说明了如果经过 判断后是 Reference 类型,就将 thisValue 为 GetBase(reference) 的值 第七条说明 不是 Reference 类型,就将 thisValue 设为 undefined
thisValue 就是我们平常使用的 this 值
从上面的规范可以看到,确定 thisValue 的关键在于调用函数时解释 MemberExpression 的结果是不是一个 Reference 类型 现在我们使用一些例子,在配合 ECMA规范 11 节 中的各种能调用函数的表达式,来说明可能会产生调用的场景以及 this 的结果。 以下例子无特殊情况,都是使用此代码;
// 注意 严格模式下 内部的 this 不会指向 window,方便测试不采用严格模式
var value = 'global';
var foo = {
value: 'internal',
bar: function () {
console.log(this.value);
}
}
以下例子说明的时候有时候会产生简写,此处写入注解
- lref:left reference
- lval:left Value
- rref:right reference
- rval:right Value
11.1.6 分组表达式
产生 PrimaryExpression:(Expression) 按照下面的过程执行 返回 Expression 的结果,它可能是 Reference 类型
// 分组表达式
console.log((foo.bar)()); // internal
从 11.1.6节 的描述可以看出,分组表达式的返回值,并不会特意去处理值,所以相当于执行 foo.bar()。
11.2.1 属性访问
属性访问方式有两种
// 中括号访问
MemberExpression [ Expression ]
MemberExpression [ <identifier-name-string> ]
// .属性访问
MemberExpression . IdentifierName
产生一个表达式 MemberExpression:MemberExpression [Expresssion]
- 使 baseReference 为 解释 + 执行 MemberExpression 的结果
- 使 baseValue 为 GetValue(baseReference) 执行后的结果
- 使 propertyNameReference 为 解释 + 执行 Expresssion 的结果
- 使 propertyNameValue 为 GetValue(propertyNameReference)
- 调用 CheckObjectCorercible(baseValue)
- 令 propertyNameString 为 ToString(propertyNameValue)
- 如果正在执行中的语法产生包含严格模式代码当中,使 strict 为 true,否则灵 strict 为 false
- 返回一个值类型的引用,其基值为 baseValue 且其引用名为 propertyNameString,严格模式标记为 strict
// 属性访问方式
console.log(foo.bar()); // internal
console.log(foo['bar']()); // internal
前面的步骤都是在说明通过对象来访问对象中属性的过程,第六条可以看出使用 [] 来访问属性会强制转换成字符串。第八条可以看出结果,返回一个 Reference 类型数据
11.11 二元逻辑运算符
二元逻辑运算符具备两种 产生式 LogincaANDExpression:LogincaANDExpression && BitwiseORExpression
- 使 lref 为 解释 + 执行 LogincaANDExpression 的结果
- 使 lval 为 GetValue(lref)
- 如果 ToBoolean(lval) 为 false,返回 lval
- 使 rref 为 解释 + 执行 BitwiseORExpression 的结果
- 返回 GetValue(rref)
产生式 LogincaORExpression:LogincaORExpression || LogincaANDExpression
- 使 lref 为解释 + 执行 LogincaORExpression 的结果
- 使 lval 为 GetValue(lref)
- 如果 ToBoolean(lval) 为 true,返回 lval
- 使 rref 为 解释 + 执行 LogincaANDExpression 的结果
- 返回 GetValue(rref)
由上面两个解释也可以看得出 && 和 || 自带的短路逻辑如何执行。 不管是 && 还是 || 这两种表达式都是用了 GetValue 函数作为返回值,所以,返回的是 global
// 二元逻辑运算符
console.log((true && foo.bar)()); // global
console.log((false || foo.bar)()); // global
11.12 条件运算符
产生式 ConditionalExpression:LogicalORExpression ? AssignmentExpression : AssignmentExpression 。
- 令 lref 为 解释 + 执行 LogicalORExpression 的结果
- 如果 ToBoolean(GetValue(lref)) 为 true,那么 令 trueRef 为 解释+执行第一个(也就是:左边) AssignmentExpression 的结果,返回 GetValue(trueRef)
- 否则 令 falseRef 为解释 + 执行 第二个 AssignmentExpression 的结果。返回 GetValue(falseRef)
由上述的逻辑可以看到 条件运算符也是 通过 GetValue() 进行返回的,所以打印的是 global
// 条件运算符
console.log((false ? '' : foo.bar)()); // global
console.log((true ? foo.bar : '')()); // global
11.13 赋值运算符
产生式 AssignmentExpression:LeftHandSideExpression = AssignmentExpression;
- 令 lref 为 解释 + 执行 LeftH 和 SideExpression 的结果
- 令 rref 为 解释 + 执行 AssignmentExpression 的结果
- 令 rval 为 GetValue(rref)
- 如果以下条件成立,抛出一个 SyntaxError 异常
- Type(lref) 为 Reference
- IsStrictReference(lref) 为 true
- Type(GetBase(lref)) 为 environment record(环境记录)
- GetReferencedName(lref) 为 'eval' 或 'arguments'
- 调用 PutValue(lref,rval)
- 返回 Rval
比较复杂,难懂,但是 只看 3即可,rval 为 GetValue 返回值,就与 Reference 类型数据无缘,所以为 global
注意:副作用调用与正常的, A 赋值给 B,B 调用方法不一样。
// 赋值运算符
console.log((foo.bar = foo.bar)()); // global
// 赋值运算符采用上面的情况几乎没有,一般都是下面这种情况
var obj = {
value: 'ceshi'
}
obj.bar = foo.bar;
console.log(obj.bar()); // ceshi
11.14 逗号运算符
产生式 Expression:Expression,AssignmentExpression
- 令 lref 为解释 + 执行 Expression 的结果
- Call GetValue(lref)
- 令 rref 为解释 + 执行 AssignmentExpression 的结果
- 返回 GetValue(rref)
返回 GetValue() 的值,所以指向 window
// 逗号运算符
console.log((foo.bar, foo.bar)()); // global
call、apply 和 bind 如何修改 this 值
了解 call 、apply 和 bind 的时候,我们首先了解下一个函数 [[Call]]
[[Call]]
此处有两个 [[Call]] 需要分别清楚,一个是 ECMA 8.6.2 对象内部方法 中提到的 [[Call]],一个是 ECMA 15.3.4.5.1 [[Call]]
- 8.6.2的 [[Call]]:没有代码逻辑,只有一段说明。
- [[Call]]:SpecOp(any,a List of any) - any or Reference
- 运行与此对象关联的代码。通过函数调用表达式调用。SpecOp 的参数是一个 this 对象和函数调用表达式传来的参数组成的列表。实现了这个这个内部方法的对象是可调用的。只有作为宿主对象的可调用对象才可能返回引用值。
8.6.2 的 [[Call]] 只有函数才能调用,第一个参数 any 代表所需要绑定的 this 对象, a List of any 代表执行函数时所传入的参数列表。apply 和 call,bind 都是调用此方法进行 this 环境绑定。
- 15.3.4.5.1 [[Call]]:
当调用一个用 bind 函数创建的函数对象 F 的 [[Call]] 内部方法,传入一个 this 值 和一个参数列表 ExtraArgs,采用如下步骤
- 令 boundArgs 为 F 的 [[BoundArgs]] 内部属性值
- 令 boundThis 为 F 的 [[BoundThis]] 内部属性值
- 令 target 为 F 的 [[TargetFunction]] 内部属性值
- 令 args 为一个新列表,它包含与列表 boundArgs 相同顺序相同值,后面跟着与 ExtraArgs 是相同顺序相同值
- 提供 boundThis 作为 this 值,提供 args 为参数调用 target 的 [[Call]] 内部方法,返回结果
此方法只有用 bind 函数的使用才能使用。本质上还是调用 8.6.2 里的 [[Call]]
Function.prototype.[[Call]] = function () {
var boundArgs = F.[[BoundArgs]];
var boundthis = F.[[BoundThis]];
var target = F.[[TargetFunction]];
var args = [...boundArgs, ...ExtraArgs];
target.[[Call]](boundthis, args);
}
由上可知,不管是通过 bind 函数调用的专属 [[Call]] 还是 由 apply,call 函数内部实现,都调用了 ECMA规范里 8.6.2 对象内部方法提到的 [[Call]],而看过这一节的应该知道了,[[Call]] 并不是每一个对象都有,而是特定对象才有,那么如果知道那些对象才有呢?内部提供了一个方法,用于判断那些对象具有 IsCallable 抽象操作 IsCallable 是用来判断对象是否可以调用内部的 [[Call]] 方法
类型 | 结果 |
---|---|
Undefined | false |
Null | false |
Boolean | false |
Number | false |
String | false |
Object | 如果参数对象包含一个 Call 内部方法,则返回 true,否则返回 false |
call
在 ECMA 规范 15.3.4.4 里有完善的逻辑说明 Function.prototype.call(thisArg, [,arg1[,arg2, ...]]) 当以 thisArg 和 可选的 arg1,arg2 等作为参数在一个 function(以下简称func) 对象上调用 call 方法,采用如下步骤
- 如果 IsCallable(func) 是 false,则抛出一个 TypeError 异常
- 令 argList 为一个空列表
- 如果调用这个方法的参数多于一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素
- 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果
// 用简单的代码实现上面的伪逻辑,用一些现在的语法 取巧实现
Function.prototype.call = function (thisArg, ...arg) {
// 因为调用此函数,根据文章上面所描述的 所以 this 指向调用者,又因为是函数本身调用的,所以
// this 指向函数
var func = this;
if (!IsCallable(func)) {
throw Error('类型异常')
}
// 令 argList 为一个空列表
var argList = [];
// 如果调用这个方法的参数多于一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素
if (arg instanceof Array && arg.length > 1) {
argList = arg;
}
// 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 function 的 [[Call]] 内部方法,返回结果
return func.[[Call]](thisArg, argList);
}
apply
在 ECMA 规范 15.3.4.3 里有完善的逻辑说明 Function.prototype.apply(thisArg, argArray) 当以 thisArg 和 argArray 为参数在一个 function(以下简称func) 对象上调用 apply 方法,采用如下步骤
- 如果 IsCallable(func) 是 false,则抛出一个 TypeError 异常
- 如果 argArray 是 null 或 undefined,则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果
- 如果 Type(arrArray) 不是一个 Object,则抛出一个 TypeError 异常
- 令 len 为 argArray 调用 length 的结果
- 令 n 为 ToUnit32(len)
- 令 argList 为一个空列表
- 令 index 为 0
- 只要 index < n 就重复
- 令 indexName 为 ToString(index)
- 令 nextArg 为 以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。(简单来讲就是 arrArray[indexName])
- 将 nextArg 作为最后一个元素插入到 argList 里
- 设定 index 为 index + 1
- 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果
上面 4 - 8 条看起来很多,其实只作了一件事情,将 argArray 赋值给 argList,但并不是直接赋值,而是将其里面一个个参数添加到 argList 中
// 用简单的代码实现下伪逻辑
Function.prototype.apply = function (thisArg, argArray) {
var func = this;
if (!IsCallable(func)) {
throw Error('类型异常')
}
if (Type(argArray) === 'undefined' || Type(argArray) === 'null') {
return func.[[Call]](thisArg, []);
}
if (!(Type(argArray) === 'Object' && argArray instanceof Array)) {
throw Error('类型异常')
}
var len = argArray.length;
var index = 0;
var argList = [];
while (index < len) {
var indexName = ToString(index);
var nextArg = argArray[indexName];
argList.push(nextArg);
index = index + 1;
}
return func.[[Call]](thisArg, argList);
}
bind
在 ECMA 规范 15.3.4.5 里有完善的逻辑说明 Function.prototype.bind(thisArg, [,arg1[,arg2, ...]]) bind 方法,需要一个或更多参数,thisArg 和 (可选的) arg1,arg2等,执行如下步骤返回一个新的函数对象
- 令 Target 为 this 值
- 如果 IsCallable(Target) 是 false,抛出一个 TypeError 异常
- 令 A 为一个新的参数列表,它包含按顺序的 thisArg 后面的所有参数
- 令 F 为一个新原生 ECMA 函数对象
- 依照 8.12 指定,设定 F 除了 [[Get]] 之外的所有内部方法
- 依照 15.3.5.4 指定,设计 F 的 [[Get]] 的内部属性
- 设定 F 的 [[TargetFunction]] 内部属性为 Target
- 设定 F 的 [[BoundThis]] 内部属性为 thisArg 的值
- 设定 F 的 [[BoundArgs]] 内部属性为 A
- 设定 F 的 [[Class]] 内部属性为 "Function"
- 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 的 prototype 对象
- 依照 15.3.4.5.1 描述,设定 F 的 [[Call]] 内置属性
- 依照 15.3.4.5.2 描述,设定 F 的 [[Construct]] 内置属性
- 依照 15.3.4.5.3 描述,设定 F 的 [[Haslnstance]] 内置属性
- 如果 Target 的 [[Class]] 内部属性是 "Function",则
- 令 L 为 Target 的 length 属性减去 A的长度
- 设定 F 的 length 自定属性为 0 和 L 中更大的值
- 否则设定 F 的 length 自定属性为 0
- 设定 F length 自身属性的特性为 15.3.5.1 指定的值
- 设定 F 的 [[Extensible]] 内部属性为 true
- 令 thrower 为 [[ThrowTypeError]] 函数对象
- 以 "caller",属性描述符 { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false },和 false 作为 参数调用 F 的 [[DefineOwnProperty]] 内部方法
- 以 "arguments" ,属性描述符 { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false },和 false 作为参数调用 F 的 [[DefineOwnProperty]] 内部方法
- 返回 F
Function.prototype.bind 创建的对象不包括 prototype 属性或 [[Code]],[[FormalParameters]],[[Scope]] 内部属性
// bind 的实现可谓是最复杂的了,参考了规范里的许多东西
Function.prototype.bind = function (thisArg, ...arg)
var Target = this;
if (!IsCallable(func)) {
throw Error('类型异常')
}
var A = [...arg];
var F = function () {};
// 依照 8.12 设定 F 拥有 除了 [[Get]] 之外的全部方法
F.[[GetOwnProperty]] = Object.[[GetOwnProperty]];
F.[[GetProperty]] = Object.[[GetProperty]];
F.[[CanPut]] = Object.[[CanPut]];
F.[[Put]] = Object.[[Put]];
F.[[HasProperty]] = Object.[[HasProperty]];
F.[[Delete]] = Object.[[Delete]];
F.[[DefaultValue]] = Object.[[DefaultValue]];
F.[[DefineOwnProperty]] = Object.[[DefineOwnProperty]];
// 依照15.3.5.4 设定 F 的 [[Get]]
F.[[Get]] = Function.[[Get]]
// 设定 F 的 [[TargetFunction]] 内部属性为 Target
F.[[TargetFunction]] = Target;
// 设定 F 的 [[BoundThis]] 内部属性为 thisArg 的值
F.[[BoundThis]] = thisArg;
// 设定 F 的 [[BoundArgs]] 内部属性为 A
F.[[BoundArgs]] = A;
// 设定 F 的 [[Class]] 内部属性为 "Function"
F.[[Class]] = "Function";
// 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 的 prototype 对象
F.[[Prototype]] = Function.[[Prototype]];
// 15.3.4.5.1,2,3 的描述 添加 [[Call]] [[Construct]] [[Haslnstance]]
F.[[Call]] = Function.[[Call]];
F.[[Construct]] = Function.[[Construct]];
F.[[Haslnstance]] = Function.[[Haslnstance]];
if (Target.[[Class]] === "Function") {
var L = Target.length - A.length;
F.length = Math.max(0, L);
} else {
F.length = 0;
}
// 处理 length 属性
F.[[DefineOwnProperty]])("length", {
[[Writable]]: false,
[[Enumerable]]: false,
[[Configurable]]: false
})
F.[[Extensible]] = true;
var thrower = [[ThrowTypeError]];
// 处理 "caller" 属性
F.[[DefineOwnProperty]])("caller", {
[[Get]]: thrower,
[[Set]]: thrower,
[[Enumerable]]: false,
[[Configurable]]: false
})
// 处理 "arguments" 属性
F.[[DefineOwnProperty]])("arguments", {
[[Get]]: thrower,
[[Set]]: thrower,
[[Enumerable]]: false,
[[Configurable]]: false
})
return F;
}
总结
虽然 call、apply 和 bind 三者不同,但是对于底层实现逻辑而言,三者都是调用 Object 对象的内部方法 [[Call]] 来实现绑定。三者的区别也仅仅只是使用方法上的区别
- call(thisArg, [,arg1[,arg2, ...]]):传入需要绑定的对象,和一连串参数,以逗号分割,是否返回参数由调用函数来决定
- apply(thisArg,argList):传入需要绑定的对象和一个数组,数组里可以存放函数参数,以下标分割,是否返回参数由调用函数决定
- bind(thisArg,[,arg1[,arg2, ...]]):传入需要绑定的对象,和一连串参数,以逗号分割,返回一个函数。使用返回的函数后是否返回参数由调用函数来决定
本文简单了些,只是说明了 this 的来源,以及各种使用情况下 this 的取值,最后 call、apply 和 bind 也缺少了代码实现,下次有机会,会补上。
文章有错误的地方可以提出,从语雀复制过来有格式上的错误
本文还少了一个环节,箭头函数的 this 说明,下次会补充也可以自行查看 ECMA规范ES6英文版