写点什么

细说 Js 中的 this

作者:hellocoder2029
  • 2022-12-12
    浙江
  • 本文字数:7852 字

    阅读完需:约 26 分钟

为什么使用 this

先看个例子:


function identity() {    return this.name.toUpperCase();}
function speak() { return "Hello, i'm " + identity.call(this);}
var me = { name: 'rod chen'}
var you = { name: "others in Aug"}
console.log(identity.call(me)); //ROD CHENconsole.log(identity.call(you)); //OTHERS IN AUG
console.log(speak.call(me)); //Hello, i'm ROD CHEN console.log(speak.call(you)); //Hello, i'm OTHERS IN AUG
复制代码


输出的结果很明显,对于 call 的用法前面文章有提到,第一个参数就是传入到函数里的 this 的值。这段代码可以在不同的上下文对象( me 和 you )中重复使用函数 identify() 和 speak() ,如果我们不适用 this 的话,那就需要 identity 和 speak 显示传入一个上下文对象,就像下面的方式


function identity(context) {    return context.name.toUpperCase();}
function speak(context) { return "Hello, i'm " + identity(context);}
var me = { name: 'rod chen'}
var you = { name: "others in Aug"}
console.log(identity(me));console.log(identity(you));
console.log(speak(me));console.log(speak(you));
复制代码


总结:


this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样


Reference

ECMAScript 的类型分为语言类型和规范类型。​

ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的 Undefined, Null, Boolean, String, Number, 和 Object。​

而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。​

没懂?没关系,我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。


Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的


这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

组成

这段讲述了 Reference 的构成,由三个组成部分,分别是:


  • base value

  • referenced name

  • strict reference


可是这些到底是什么呢?


我们简单的理解的话:


base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。


referenced name 就是属性的名称。


var foo = 1;
// 对应的Reference是:var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false};
复制代码


var foo = {    bar: function () {        return this;    }};
foo.bar(); // foo
// bar对应的Reference是:var BarReference = { base: foo, propertyName: 'bar', strict: false};
复制代码


方法

  • GetBase:返回 reference 的 base value。

  • IsPropertyReference:简单的理解:如果 base value 是一个对象,就返回 true。

  • GetValue:返回具体的值


如何确定 this 的值

function call 步骤

  1. 令 ref 为解释执行 MemberExpression 的结果 .

  2. 令 func 为 GetValue(ref).

  3. 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表 (see 11.2.4).

  4. 如果 Type(func) is not Object ,抛出一个 TypeError 异常 .

  5. 如果 IsCallable(func) is false ,抛出一个 TypeError 异常 .

  6. 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果

  7. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.

  8. 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表

MemberExpression

  • PrimaryExpression // 原始表达式 可以参见《JavaScript 权威指南第四章》

  • FunctionExpression // 函数定义表达式

  • MemberExpression [ Expression ] // 属性访问表达式

  • MemberExpression . IdentifierName // 属性访问表达式

  • new MemberExpression Arguments // 对象创建表达式


这里说的是方法调用的左边部分。


function foo() {    console.log(this)}
foo(); // MemberExpression 是 foo
function foo() { return function() { console.log(this) }}
foo()(); // MemberExpression 是 foo()
var foo = { bar: function () { return this; }}
foo.bar(); // MemberExpression 是 foo.bar
复制代码


参考 前端面试题详细解答

判断 ref 的类型

第一步计算 ref,第七步需要判断 ref 是不是一个 reference 类型。


var value = 1;
var foo = { value: 2, bar: function () { return this.value; }}
//示例1console.log(foo.bar());//示例2console.log((foo.bar)());//示例3console.log((foo.bar = foo.bar)());//示例4console.log((false || foo.bar)());//示例5console.log((foo.bar, foo.bar)());
复制代码


foo.bar()


这个是属性访问。根据 11.2.1 property accessors 最后一步:


Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.


返回了一个 reference 类型:返回一个 reference 类型的引用,其基值为 baseValue 且其引用名为 propertyNameString, 严格模式标记为 strict


var Reference = {  base: foo,  name: 'bar',  strict: false};
复制代码


然后回到[function call](function call)第六、七步,


  • 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果

  • 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.


这里因为 foo 是一个对象,所以这里 IsPropertyReference(ref) 的值为 true。所以这里 this 的值就是 GetBase(ref)就是 foo。


(foo.bar)()


分组表达式规则如下:


  1. 返回执行 Expression 的结果,它可能是 Reference 类型


这里的结果和上面相同都是 fo。



(foo.bar = foo.bar)()


涉及到简单赋值,规则如下:


  1. 令 lref 为解释执行 LeftH 和 SideExpression 的结果 .

  2. 令 rref 为解释执行 AssignmentExpression 的结果 .

  3. 令 rval 为 GetValue(rref).

  4. 抛出一个 SyntaxError 异常,当以下条件都成立 :

  5. Type(lref) 为 Reference

  6. IsStrictReference(lref) 为 true

  7. Type(GetBase(lref)) 为环境记录项

  8. GetReferencedName(lref) 为 "eval" 或 "arguments"

  9. 调用 PutValue(lref, rval).

  10. 返回 rval.


这里的返回值为第三部,GetValue。这是一个具体的返回值。根据上面的值,得到 this 的值为 undefined,非严格模式下这里隐式装换为 window 对象。


(false || foo.bar)()和(foo.bar, foo.bar)()


这里的返回值都是去的 getValue 的值。所以 this 都和上面一样。


看一下最终的结果:


var value = 1;
var foo = { value: 2, bar: function () { return this.value; }}
//示例1console.log(foo.bar()); // 2//示例2console.log((foo.bar)()); // 2//示例3console.log((foo.bar = foo.bar)()); // 1//示例4console.log((false || foo.bar)()); // 1//示例5console.log((foo.bar, foo.bar)()); // 1
复制代码


普通函数

function foo() {    console.log(this)}
foo();
复制代码


这种属于解析标识符


The result of evaluating an identifier is always a value of type Reference with its referenced name component equal to the Identifier String.


解释执行一个标识符得到的结果必定是 引用 类型的对象,且其引用名属性的值与 Identifier 字符串相等。


那么 baseValue 是什么值呢?因为解析标识符会调用的结果是 GetIdentifierReference。


  1. 如果 lex 的值为 null,则:

  2. 返回一个类型为 引用 的对象,其基值为 undefined,引用的名称为 name,严格模式标识的值为 strict。

  3. 令 envRec 为 lex 的环境数据。

  4. 以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的结果。

  5. 如果 exists 为 true,则:

  6. 返回一个类型为 引用 的对象,其基值为 envRec,引用的名称为 name,严格模式标识的值为 strict。

  7. 否则:

  8. 令 outer 为 lex 的 外部环境引用 。

  9. 以 outer、name 和 struct 为参数,调用 GetIdentifierReference,并返回调用的结果。


这里因为是 window 对象所以这里返回的是:


var fooReference = {    base: EnvironmentRecord,  // 或许这里是undefined    name: 'foo',    strict: false};
复制代码


不管 base 的值是上面两种的哪一种,那都不是 Object。那根据上面的规则:


  • 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果

  • 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.


this 的值为 ImplicitThisValue 的值。这个值返回的一直 undefined。所以这里 this 的是 undefined。


什么是 this

说的是执行上下文的 thisbinding。


this 说的是当前函数的调用位置。这个是概念描述。下面通过上面的只是去分析各种情况下的 thisbinding 是什么东西。


this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁


并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样

函数调用

具体参照上面说的普通函数


call,apply

var person = {  name: "axuebin",  age: 25};function say(job){  console.log(this.name+":"+this.age+" "+job);}say.call(person,"FE"); // axuebin:25say.apply(person,["FE"]); // axuebin:25
复制代码

call

Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] ) 当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 方法,采用如下步骤:​

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。

  2. 令 argList 为一个空列表。

  3. 如果调用这个方法的参数多余一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素。

  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。

call 方法的 length 属性是 1。


this 的值为传入 thisArg 的值。


apply

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常

  2. 如果 argArray 是 null 或 undefined, 则

  3. 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。

  4. 如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .

  5. 令 len 为以 "length" 作为参数调用 argArray 的 [[Get]] 内部方法的结果。

  6. 令 n 为 ToUint32(len).

  7. 令 argList 为一个空列表 .

  8. 令 index 为 0.

  9. 只要 index < n 就重复

  10. 令 indexName 为 ToString(index).

  11. 令 nextArg 为以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。

  12. 将 nextArg 作为最后一个元素插入到 argList 里。

  13. 设定 index 为 index + 1.

  14. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。

apply 方法的 length 属性是 2。


注意  在外面传入的 thisArg 值会修改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。


function [[call]]

因为上面说了调用 function 的[[Call]]内部方法。


当用一个 this 值,一个参数列表调用函数对象 F 的 [[Call]] 内部方法,采用以下步骤:


  1. 用 F 的 [[FormalParameters]] 内部属性值,参数列表 args,10.4.3 描述的 this 值来建立 函数代码 的一个新执行环境,令 funcCtx 为其结果。

  2. 令 result 为 FunctionBody(也就是 F 的 [[Code]] 内部属性)解释执行的结果。如果 F 没有 [[Code]] 内部属性或其值是空的 FunctionBody,则 result 是 (normal, undefined, empty)。

  3. 退出 funcCtx 执行环境,恢复到之前的执行环境。

  4. 如果 result.type 是 throw 则抛出 result.value。

  5. 如果 result.type 是 return 则返回 result.value。

  6. 否则 result.type 必定是 normal。返回 undefined。


这里又提到了 10.4.3 的执行函数代码的规则:


当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入 函数代码 的执行环境时,执行以下步骤:


  1. 如果 函数代码 是 严格模式下的代码 ,设 this 绑定为 thisArg。

  2. 否则如果 thisArg 是 null 或 undefined,则设 this 绑定为 全局对象 。

  3. 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定为 ToObject(thisArg)。

  4. 否则设 this 绑定为 thisArg。

  5. 以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。

  6. 设词法环境为 localEnv。

  7. 设变量环境为 localEnv。

  8. 令 code 为 F 的 [[Code]] 内部属性的值。

  9. 10.5 描述的方案,使用 函数代码 code 和 argumentList 执行定义绑定初始化步骤。

bind

var person = {  name: "axuebin",  age: 25};function say(){  console.log(this.name+":"+this.age);}var f = say.bind(person);console.log(f());
复制代码


里面同样的也是讲传入的 thisArg 设置 this 的值。


箭头函数

箭头函数并不绑定 this,arguments,super(ES6),或者 new.target(ES6),这些知识沿用包含当前箭头函数的封闭的词法环境。


function foo() {   setTimeout( () => {      console.log("args:", arguments);   },100);}
foo( 2, 4, 6, 8 );// args: [2, 4, 6, 8]
复制代码

说法定义纠正

  1. 箭头函数的 this 是绑定到父函数 foo 的,其实不是,只是沿用。因为箭头函数内部没有做任何的绑定操作。

  2. 局部变量 this 下面的例子是为了说明局部变量 this


function foo() {   var self = this;   setTimeout(function() {      console.log("id:" + this.id);   },100);}
foo.call( { id: 42 } );// id:undefined
复制代码


这里为什么是 undefined,因为这里 setTimeout 是 windows 对象的属性。this 指向 window。


然后修改了一下:


function foo() {   var self = this;   setTimeout(function() {      console.log("id:" + self.id);   },100);}
foo.call( { id: 42 } );// id:42
复制代码


这里是因为 self 对于 function 的执行是一个执行上下文变量环境 outer 指向的包含当前函数的闭合函数变量环境。这里和 this 没有任何关系。


这里提一下:“因为 this 无论如何都是局部的”。this 都是函数执行的 this 绑定规则来决定的。

作为对象的一个方法

参照:上文 foo.bar 函数调用的解析

作为一个构造函数

[[construct]]


  1. 令 obj 为新创建的 ECMAScript 原生对象。

  2. 依照 8.12 设定 obj 的所有内部方法。

  3. 设定 obj 的 [[Class]] 内部方法为 "Object"。

  4. 设定 obj 的 [[Extensible]] 内部方法为 true。

  5. 令 proto 为以参数 "prototype" 调用 F 的 [[Get]] 内部属性的值。

  6. 如果 Type(proto) 是 Object,设定 obj 的 [[Prototype]] 内部属性为 proto。

  7. 如果 Type(proto) 不是 Object,设定 obj 的 [[Prototype]] 内部属性为 15.2.4 描述的标准内置的 Object 的 prototype 对象。

  8. 以 obj 为 this 值,调用 [[Construct]] 的参数列表为 args,调用 F 的 [[Call]] 内部属性,令 result 为调用结果。

  9. 如果 Type(result) 是 Object,则返回 result。

  10. 返回 obj


我们可以看到 this 绑定在当前创建的对象。


作为一个 DOM 事件处理函数

this 指向触发事件的元素,也就是始事件处理程序所绑定到的 DOM 节点。


var ele = document.getElementById("id");ele.addEventListener("click",function(e){  console.log(this);  console.log(this === e.target); // true})
复制代码


这里可能不太好知道。从文献中了解到:


The event listener is appended to target’s event listener list 


interface EventTarget {  constructor();
undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {}); undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {}); boolean dispatchEvent(Event event);};
callback interface EventListener { undefined handleEvent(Event event);};
dictionary EventListenerOptions { boolean capture = false;};
dictionary AddEventListenerOptions : EventListenerOptions { boolean passive = false; boolean once = false;};
复制代码


根据 EventTarget 的属性,可以看到 callback 是一级属性,所以 Event.callback 的执行的 this 指向的是 EventTarget。当然具体的实现没有看到


从侧面了解:还有一种写法:button.onclick = foo。这种就可以很好的了解了。


this 优先级

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:


  1. 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。


var bar = new foo()
复制代码


  1. 函数是否通过 call 、 apply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是指定的对象。


var bar = foo.call(obj2)
复制代码


  1. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上下文对象。


var bar = obj1.foo()
复制代码


  1. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到全局对象。


var bar = foo()
复制代码


结论:new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用


// 例子来源:若风 https://juejin.cn/post/6844903746984476686#heading-12var name = 'window';var person = {    name: 'person',}var doSth = function(){    console.log(this.name);    return function(){        console.log('return:', this.name);    }}var Student = {    name: 'rod',    doSth: doSth,}// 普通函数调用doSth(); // window// 对象上的函数调用Student.doSth(); // 'rod'// call、apply 调用Student.doSth.call(person); // 'person'new Student.doSth.call(person); // Uncaught TypeError: Student.doSth.call is not a constructor
复制代码


最后这一行说一下,因为. 运算符的优先级高于 new。所以这里是 Student.doSth.call 作为 new 的构造函数。但是因为 call 的方法执行的时候,执行的是 func 的[[call]]方法。想要吊用 new 的话需要调用[[Construct]] 属性


call 的调用

当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 方法,采用如下步骤:

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。

  2. 令 argList 为一个空列表。

  3. 如果调用这个方法的参数多余一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素。

  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。


用户头像

还未添加个人签名 2022-09-08 加入

还未添加个人简介

评论

发布
暂无评论
细说Js中的this_JavaScript_hellocoder2029_InfoQ写作社区