再也不用担心javascript的this---从代码去理解(第一篇)---深析this原理

介绍

在这篇文章里,我们将讨论跟执行上下文直接相关的更多细节。讨论的主题就是this关键字。
(如果在本篇有不懂的地方,一定要看第二篇)

定义

this是执行上下文中的一个属性:

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

这里VO是我们在执行上下文讨论的变量对象。

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

this在全局代码中的值

在全局代码中,this始终是全局对象本身。这样就有可能间接的引用到它了。

// explicit property definition of
// the global object
this.a = 10; // global.a = 10
alert(a); // 10

// implicit definition via assigning
// to unqualified identifier
b = 20;
alert(this.b); // 20

// also implicit via variable declaration
// because variable object of the global context
// is the global object itself
var c = 30;
alert(this.c); // 30

this在函数代码中的值

在这种类型的代码中,this值的首要(也许是最主要的)特点是它没有静态绑定到一个函数。

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

不管怎样,在代码运行期间,this的值是不变的,也就是说,因为this不是一个变量,所以不可能为其分配一个新值。

var foo = {x: 10};

var bar = {
  x: 20,
  test: function () {

    alert(this === bar); // true
    alert(this.x); // 20

    this = foo; // error

    alert(this.x); // if there wasn't an error then 20, not 10

  }

};

// 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

foo.test(); // false, 10

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

The value of this in a function context is provided by the caller and determined by the current form of a call expression (how the function call is written syntactically).

中文的意思就是:在函数上下文中this的值是由函数的调用者以及这个调用者的语法形式决定的。(即这个调用者是怎么书写的)

在有些javascript书籍中它们声称:“this的值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。–——这绝对不正确。
如:

function foo() {
  alert(this);
}

foo(); // global

alert(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

有时可能将函数作为某些对象的一个方法来调用,此时this的值不会设置为这个对象。

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};

foo.bar(); // foo, true

var exampleFunc = foo.bar;

alert(exampleFunc === foo.bar); // true

// again with another form of the call expression
// of the same function, we have different this value

exampleFunc(); // global, false

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

引用类型

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

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

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

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

标示符的处理过程将在其它章节中讨论,在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值。

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

var foo = 10;
function bar() {}

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

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

var barReference = {
  base: global,
  propertyName: 'bar'
};

为了从引用类型中得到一个对象真正的值,在伪代码中可以用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));

}

内部的[[Get]]方法返回对象属性真正的值,

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

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

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

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

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

GetValue(fooBarReference); // function object "bar"

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

在一个函数上下文中,this的值由调用者提供,且由调用者的语法形式决定。如果调用括号()的左边是引用类型的值,this将设为这个引用类型值的base对象,在其他情况下(与引用类型不同的任何其它属性),this的值都为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。

下面让我们看个例子:

function foo() {
  return this;
}

foo(); // global

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

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

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

同样,使用属性访问器:

var foo = {
  bar: function () {
    return this;
  }
};

foo.bar(); // foo

同样,我们拥有一个引用类型的值,其base是foo对象,在函数bar调用时将base设置给this。

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

但是,如果用另一种方式调用相同的函数,this的值将不同。

var test = foo.bar;
test(); // global

因为test作为标识符,产生了其他引用类型的值,该值的base(全局对象)被设置为this的值。

var testReference = {
  base: global,
  propertyName: 'test'
};

现在,我们可以很明确的说明,为什么用不同的形式激活同一个函数会产生不同的this,答案在于不同的引用类型(type Reference)的中间值。

function foo() {
  alert(this);
}

foo(); // global, because

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

alert(foo === foo.prototype.constructor); // true

// another form of the call expression

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

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

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

function foo() {
  alert(this.bar);
}

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

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

x.test(); // 10
y.test(); // 20

函数调用和非引用类型

那么,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,this的值自动设置为null,实际最终this的值被隐式转换为全局对象。

让我们思考下面这种函数表达式:

(function  () {
  alert(this); // null => global
})();

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

更多复杂的例子:

var foo = {
  bar: function () {
    alert(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

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

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

第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。

在第二个例子中,分组操作符(译者注:这里的分组操作符就是指foo.bar外面的括号”()”)没有实际意义,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue (参考11.1.6)。相应的,在分组操作的返回值中———我们得到的仍是一个引用类型。这就是this的值为什么再次被设为base对象,即 foo。

第三个例子中,与分组操作符不同,赋值操作符调用了GetValue方法(参考11.13.1的第三步)。返回的结果已经是函数对象(不是引用类型),这意味着this的值被设为null,实际最终结果是被设置为global对象。

第四个和第五个也是一样——逗号操作符和逻辑操作符(OR)调用了GetValue 方法,相应地,我们失去了引用类型的值而得到了函数类型的值,所以this的值再次被设为global对象。

引用类型和this为null

有一种情况,如果调用方式确定了引用类型的值,不管怎样,只要this的值被设置为null,其最终就会被隐式转换成global。当引用类型值的base对象是激活对象时(即A O / VO),就会导致这种情况。

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

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // the same as AO.bar()
}

激活对象总是作为this的值返回——null(即伪代码AO.bar()相当于null.bar()),this的值最终还是被设置为全局对象。

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

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

function A() {
  alert(this); // newly created object, below - "a" object
  this.x = 10;
}

var a = new A();
alert(a.x); // 10

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

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

在Function.prototype中定义了两个方法允许手动设置函数调用时this的值,它们是.apply和.call方法(所有的函数都可以访问它们)。它们用接受的第一个参数作为this的值,this在调用的作用域中使用。这两个方法的区别不大,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,相反,.call能接受任何参数。两个方法必须的参数都是第一个——this。

例如:

var b = 10;

function a(c) {
  alert(this.b);
  alert(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

结论

在这篇文章中,我们讨论了JavaScript中this关键字的特征,希望对你有所帮助,如果在此篇不懂的内容,在下一篇里会详细讲述。


下一篇:再也不用担心javascript的this—从代码去理解(第二篇)–引用类型

本文参考自:

中文版 :[javascript]ECMA-262-3深入解析 第三章 this

英文版:ECMA-262-3 in detail. Chapter 3. This.

发布了32 篇原创文章 · 获赞 23 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章