Revan1i

vuePress-theme-reco revan1i    2019 - 2020
Revan1i Revan1i

Choose mode

  • dark
  • auto
  • light
Home
TimeLine
Category
  • javascript
  • css
  • react
  • js
  • how
  • math
  • regexp
  • algorithm
  • feeling
Tag
Contact
  • GitHub
author-avatar

revan1i

25

文章

20

标签

Home
TimeLine
Category
  • javascript
  • css
  • react
  • js
  • how
  • math
  • regexp
  • algorithm
  • feeling
Tag
Contact
  • GitHub

Javascript基础之this

vuePress-theme-reco revan1i    2019 - 2020

Javascript基础之this


revan1i 2019-11-05 09:06:25 javascript base

本文翻译于:http://dmitrysoshnikov.com/ecmascript/chapter-3-this/

部分难以翻译的句子参考:Justin's的翻译

注意:部分示例代码中的注释不进行翻译,考虑到平时写代码习惯注释也有可能是英文(简洁或者习惯),原汁原味的英文注释可能更加容易理解。

# 前言

我们知道,当 JavaScript 引擎执行一段可执行代码(executable code)时, 会创建对应的执行上下文(execution context)。

对于每一个执行上下文,都有三个重要的属性:

  • 变量对象(Variable object, VO)
  • 作用域链(Scope chain)
  • this

这篇文章主要讲this。

# 介绍

在这篇文章中,我们将讨论执行跟上下文直接相关的更多细节。讨论的主题就是this关键词。

实践证明,这个主题很难,在不同的执行上下文中确定this的值经常发生问题。

很多程序员习惯的认为在程序语言中this关键词与面向对象程序开发紧密相关,其完全指向由构造函数新创建的对象。在 ECMAScript 中也是这样实现的,然而正如我们将看到,this 并不局限于只用来指向新创建的对象。

下面让我们更详细的了解一下,在 ECMAScript 中,this 的值到底是什么?

# 定义

this 是执行上下文中的一个属性。当一段代码执行时它是一个特殊的对象。

activeExecutionContext = {
  VO: {...},
  this: thisValue
}
1
2
3
4

这里的 VO 就是我们前一章讨论的变量对象。

this与上下文中可执行代码的类型 直接相关。this 的值在进入上下文期间确定,并且在上下文运行代码期间不会改变 this 的值。

让我们根据以下的场景去更详细的研究它。

# this 在全局代码中的值

在全局代码中,一切都很简单,this 的值始终是全局对象本身。这样就可能间接的引用到它。

// 显式定义属性到全局对象
this.a = 10; // global.a = 10
console.log(a); // 10

// 通过赋值给一个无标示符隐式定义
b = 20;
console.log(this.b); // 20

// 也是通过变量声明隐式声明
// 因为全局上下文变量对象就是全局对象自身
var c = 30;
console.log(c); // 30
1
2
3
4
5
6
7
8
9
10
11
12

# this 在函数中的值

在函数中使用 this 会更有趣。这种场景是最难的而且会导致很多问题。

this 值的第一个(或许是最主要的)特点是它没有静态绑定到一个函数中。

正如我们前面讨论那样,this的值在进入上下文时确定并且在函数代码中,this的值每一次(进入上下文时)可能完全不同。

然而,在代码运行期间,this的值是不变的。也就是说,不可能分配一个新的值给this因为this不是一个变量。(相反,在 Python 中,this明确的定义为对象本身,可以在运行期间不断的修改。)

var foo = { x: 10 };

var bar = {
  x: 20,
  test: function () {
    console.log(this === bar); // true
    console.log(this.x); // 20

    this = foo; // error, can't change this value
    console.log(this.x); // if there wasn't an error, then would be 10, not 20
  }
}

// on entering the context this value is
// determined as "bar" object; why so - will
// be discussed below in detail

bar.test();  // true, 20
foo.test = bar.test;

// however here this value will now refer
// to "foo" – even though we're calling the same function

// 不过这里this的值就是foo, 尽管调用的是相同的function
foo.test();  // error, 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

那么, 在函数代码中,什么影响了this的值发生变化?

首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的上下文(parent context)。this取决于调用函数的方式。

为了在任何情况下能准确无误的确定this的值,有必要理解和记住重要的一点:正是调用函数的方式影响了调用的上下文中this的值,没有别的什么。(我们可以在一些文章,甚至是在关于javascript的书籍中看到,他们声称:this的值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。 -- 这绝对不正确)。继续我们的话题,可以看到,即使正常的全局函数也会因为不同的调用方式被激活,这些不同的调用方式产生了不同的this值。

function foo() {
  console.log(this);
}
 
foo(); // global
 
console.log(foo === foo.prototype.constructor); // true
 
// but with another form of the call expression
// of the same function, this value is different
 
foo.prototype.constructor(); // foo.prototype
1
2
3
4
5
6
7
8
9
10
11
12

相似地,可能将函数作为某些对象的方法来调用,但是,this的值不会设置为这个对象。

var foo = {
  bar: function () {
    console.log(this);
    console.log(this === foo)
  }
}

foo.bar(); // {bar: f}即是foo对象, true

var exampleFunc = foo.bar
console.log(exampleFunc === foo.bar); // true

// again with another form of the call expression
// of the same function, we have different this value
exampleFunc(); // global, false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

那么,到底调用函数的方式如何影响this的值?为了充分理解this的值是如何确定的,有必要详细分析一个内部类型(internal type)--- 引用类型。

# 引用类型

用伪代码可以把引用类型表示为拥有两个属性的对象 -- base(即拥有属性的那个对象),和 base 中的 propertyName。

var valueOfReferenceType = {
  base: <base object>,
  propertyName。: <property name>
}
1
2
3
4

注意:从ES5开始,一个引用类型也包含了名为strict的属性 -- 标识一个引用类型是否在strict模式中处理。

'use strict'
// Access foo

foo;

//Reference for 'foo'
const fooReference = {
  base: global,
  propertyName: 'foo',
  strict: true
}
1
2
3
4
5
6
7
8
9
10
11

引用类型的值仅存在于两种情况中:

  1. 当我们处理一个标识符时;(when we deal with an identifier;)
  2. 或一个属性访问器。(or with a prototy accessor)

标识符的处理过程在[Chapter 4.Scope chain]中详细讨论;在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值(这对于this来说很重要。

标识符是变量名,函数名,函数参数名和全局对象中未识别的变量名,例如,下面标识符的值:

var foo = 10;
function bar() {};
1
2

在操作的中间结果中,引用类型对应的值如下:

var fooReference = {
  base: global,
  propertyName: 'foo'
}

var barReference = {
  base: global,
  propertyName: 'bar'
}
1
2
3
4
5
6
7
8
9

为了从引用类型中得到一个对象真正的值,在伪代码中可以用GetValue方法来来表示,如下:

function GetValue(value) {
  if (Type(value) != Reference) {
    return value;
  }

  var base = GetBase(value);

  if (base === null) {
    throw new ReferenceError;
  }

  return base.[[Get]](GetPropertyName(value));
}
1
2
3
4
5
6
7
8
9
10
11
12
13

内部的[[Get]]方法返回对象属性真正的值,包括对原型链中继承属性的分析。

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
1
2

属性访问器都应该熟悉。它有两种变体:点(.)语法(此时属性名是正确的标识符且事先知道),或括号语法([])。

foo.bar();
foo['bar']();
1
2

在计算中间的返回值中,引用类型对应的值如下:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

GetValue(fooBarReference);  // function object "bar"
1
2
3
4
5
6

那么,从最重要的意义上来说,引用类型的值与函数上下文中的this的值是如何关联起来的呢?这个关联过程是这篇文章的核心。 在一个函数上下文中确定this的值的通用规则如下:

在一个函数上下文中,this的值由调用者提供,且由调用函数的方式决定。

如果调用括号()的左边是引用类型的值,this将设为引用类型值的 base 对象。

在其他情况下(即与引用类型不同的任何其他属性),this的值一直都为null。不过,实际上不存在this的值为 null 的情况,因为当 this 的值为 null 的时候,其值会被隐式的转换为全局对象。注:第5版ECMAScript中,已经不强制转换为全局对象了,而是赋值为 undefined.

举个例子:

function foo() {
  return this;
}
foo();  // global
1
2
3
4

我们看到在调用括号的左边是一个引用类型(因为 foo 是一个标识符)。

var fooReference = {
  base: global,
  propertyName: 'foo'
}
1
2
3
4

相应地,this也设置为引用类型的 base 对象,即全局对象。

同样,使用属性访问器:

var foo = {
  bar: function() {
    console.log(this)
    return this;
  }
};
 
foo.bar(); // foo
1
2
3
4
5
6
7
8

我们再次拥有一个引用类型,其 base 是 foo 对象,在函数 bar 激活时用作 this。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
}
1
2
3
4

但是,用另一种形式激活相同的函数,我们得到其它this值。

var test = foo.bar
test() // global
1
2

因为 test 作为标识符,生成了引用类型的其他值,其 base (全局对象)作为 this 值:

var testReference = {
  base: global,
  propertyName: 'test'
}
1
2
3
4

现在,我们可以明确的告诉你,为什么用表达式的不同形式激活同一个函数会产生不同的 this 值,答案在于 引用类型不同的中间值。

function foo() {
  console.log(this);
}

foo(); // global, because

var fooReference = {
  base: global,
  propertyName: 'foo'
}

console.log(foo === foo.prototype.constructor);  // true

foo.prototype.constructor();  // foo.prototype, because

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

另外一个通过调用方式动态确定this值的经典例子:

function foo() {
  console.log(this.bar);
}

var x = {bar: 10};
var y = {bar: 20};

x.test = foo;
y.test = foo;

x.test(); //10
y.test(); //20
1
2
3
4
5
6
7
8
9
10
11
12

# 函数调用和非引用类型

因此,正如我们已经指出,如果调用括号左边不是引用类型而是其它类型,这个值自动设置为null, 结果为全局对象。

思考这种表达式

(function() {
  console.log(this); // null -> global
})()
1
2
3

在这个例子中,我们有一个函数对象但不是引用类型的对象(他不是标识符,也不是属性访问器),相应地,this的值最终设为全局对象。

更多复杂的例子:

var foo = {
  bar: function() {
    console.log(this);
  }
}

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)();  // global?
(false || foo.bar)();   // global?
(foo.bar, foo.bar)();   // global?
1
2
3
4
5
6
7
8
9
10
11
12

所以,为什么我们有一个属性访问器,它的中间值应该为引用类型的值,在某些调用中我们得到的this的值不是 base 对象(即foo),而是 global 对象?

问题在于后面三个调用,在应用一定的运算操作之后,在调用括号的左边的值不在是引用类型。

  1. 第一个例子很明显 -- 明显的引用类型,结果是,this为 base 对象,即 foo。
  2. 在第二个例子中,组运算符并不适用,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如 GetValue。相应地,在组运算的返回中 -- 我们得到仍是一个引用类型 。这也是this的值为什么再次设为 base 对象,即 foo。
  3. 第三个例子中,与组运算符不同,赋值运算符调用了GetValue方法。返回的结果是函数对象(但不是引用类型),这意味着this设为null,结果是 global 对象。
  4. 第四个和第五个例子相类似 -- 逗号运算符和逻辑运算符(OR)调用了GetValue方法,相应地,我们失去引用而得到了函数。并再次设为 global。

# 引用类型和this为null

有一种情况,如果调用方式确定了引用类型的值,不管怎样,只要this的值被设置为 null, 其最终会被隐式转换成 global。这跟引用类型的值是激活对象相关。

下面的例子中,内部函数被父函数调用,此时我们就能看到上面说的那种特殊情况,正如我们在第二章 知道的一样,局部变量,内部函数,形式参数都存储在给定的函数的激活对象中。

function foo() {
  function bar() {
    console.log(this);  // global
  }
  bar();
}
1
2
3
4
5
6

激活对象总是作为this的值返回 -- null(即伪代码AO.bar()相当于null.bar())(译者注:如果不明白可以参考下原文 )。这里我们再次回到上面描述的情况,this的值还是被设置全局对象。

有一种情况除外,在with语句中调用函数,且在 with 对象中包含函数名属性时,with语句将其对象添加到作用域的最前端,即在激活对象前面。 那么对应的,引用类型的值(通过的标识符或属性访问器),其 base 对象不再是激活对象,而是 with 语句对象。顺便提一句,这种情况不仅跟内部 函数相关,还跟全局函数相关,因为 with 对象比作用域链的最前端的对象(全局或者激活对象)还要靠前(because the with object shadows higher object (global or an activation object) of the scope chain:)。

var x = 10;

with({
  foo: function() {
    console.log(this.x);
  },
  x: 20
}){
  foo(); // 20
}

// because
var fooReference = {
  base: __withObject,
  propertyName: 'foo'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在 catch 语句的实际参数中的函数调用存在类似情况,在这种情况下,catch 对象被添加到作用域的最前端,即在激活对象或全局对象的前面。但是,这个特定 的行为被确认为是 ECMA-262-3 的一个 bug, 这个在新版本的 ECMA-262-5 被修复了,即this指向全局对象,而不是 catch 对象。

try {
  throw function () {
    console.log(this);
  };
} catch (e) {
  e(); // __catchObject - in ES3, global - fixed in ES5
}
 
// on idea
 
var eReference = {
  base: __catchObject,
  propertyName: 'e'
};
 
// but, as this is a bug
// then this value is forced to global
// null => global
 
var eReference = {
  base: global,
  propertyName: 'e'
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

同样的情况出现在命名函数 (函数的更多细节参考Chapter 5. Functions )的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。

(function foo(bar) {
 
  console.log(this);
 
  !bar && foo(1); // "should" be special object, but always (correct) global
 
})(); // global
1
2
3
4
5
6
7

# 作为构造器调用的函数中的this值

还有一个在函数上下文中与this的值相关的情况是:函数作为构造器调用时。

function A() {
  console.log(this); // newly created object, below - "a" object
  this.x = 10;
}
 
var a = new A();
console.log(a.x); // 10
1
2
3
4
5
6
7

在这个例子中,new 操作符调用A函数内部的[[Construct]]方法,接着,在对象创建后,调用内部的[[Call]]方法,所有相同 的函数A都将this的值设置为新创建的对象。

# 手动设置一个函数调用的this值

在Function.prototype中定义了两种方法(这两种方法全部函数都可以访问),允许手动设置函数调用时的this值,他们就是apply和call方法。

它们用接受的第一个参数作为this的值,这两种方法的区别不大,对于apply,第二个参数必须是数组(或者是类数组的对象,比如arguments), 相反, call方法能接受任何参数,两个方法必须的参数都是第一个 -- this的值。

例如:

var b = 10;

function a(c) {
  console.log(this.b);
  console.log(c);
}

a(20);    // this === global, this.b = 10, c = 20

a.call({b: 20}, 30);  // this === { b: 20 }, this.b == 20, c == 30
a.apply({b: 30}, [40]); // this === { b: 30 }, this.b == 30, c == 40
1
2
3
4
5
6
7
8
9
10
11

# 译者注

到这里,对原文的翻译就已经结束了,原文结论部分不是很重要就没有翻译了。整体翻译完,不禁感概翻译一篇技术文章真的不容易, 向互联网翻译技术文章的技术大牛致敬。此文大概50%参考了Justin的翻译 ,剩下根据自己的理解翻译,因为学识有限难免会有纰漏,恳请指正。这篇文章 暂时告一段落,之后有新的看法会更新,感谢~~

# 参考

  1. [JavaScript]ECMA-262-3 深入解析.第三章.this Justin
  2. ECMA-262-3 in detail. Chapter 3. This.