细说Js中的this

为什么使用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 CHEN
console.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;
  }
}

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.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第六、七步,

  • 如果 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 异常,当以下条件都成立 :

    • Type(lref) 为 Reference
    • IsStrictReference(lref) 为 true
    • Type(GetBase(lref)) 为环境记录项
    • GetReferencedName(lref) 为 "eval" 或 "arguments"
  5. 调用 PutValue(lref, rval).
  6. 返回 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;
  }
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.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,则:

    1. 返回一个类型为 引用 的对象,其基值为 undefined,引用的名称为 name,严格模式标识的值为 strict。
  2. 令 envRec 为 lex 的环境数据。
  3. 以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的结果。
  4. 如果 exists 为 true,则:

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

    1. 令 outer 为 lex 的 外部环境引用 。
    2. 以 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:25
say.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, 则

    1. 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。
  3. 如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
  4. 令 len 为以 "length" 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
  5. 令 n 为 ToUint32(len).
  6. 令 argList 为一个空列表 .
  7. 令 index 为 0.
  8. 只要 index < n 就重复

    1. 令 indexName 为 ToString(index).
    2. 令 nextArg 为以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。
    3. 将 nextArg 作为最后一个元素插入到 argList 里。
    4. 设定 index 为 index + 1.
  9. 提供 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-12
var 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]] 内部方法,返回结果。

你可能感兴趣的:(javascript)