Javascript中this的取值

this是javascript中非常基础的一个知识点,也是一个令很多初学者迷惑的知识点。

Ecmascript中对其描述如下:

There is a this value associated with every active execution context. The this value depends on the caller and the type of code being executed and is determined when control enters the execution context. The this value associated with an execution context is immutable.

this的取值跟executable code的类型直接相关,execute code有三种Global code,Function code 和Eval code。跟this取值有关的是前两种。

this在global code中的取值

这种情况下问题比较简单,this的值就是global对象本身

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

// implicit property definition of the global object
b = 20;
alert(this.b); // 20

// also implicit via variable declaration
// in global context: this =  global = VO
var c = 30;
alert(this.c); // 30

demo中的b没有通过var 声明,它实际上不是一个variable,而是window(global)的一个property。变量和属性的一个显著区别就是变量具有{ DontDelete },而后者没有。因此经常说的

不用var能直接声明全局变量

的说法是错误的,事实上它是为windows添加了一个属性。


var a =5;
b = 4;  // equal to windows.b = 4
alert(delete a); // false
alert(delete b); // true

this在function code中的取值

在function code中this的值不像在global code中那么简单:

  • this的值不是与对应的函数绑定的,也就是说不是在函数创建时决定。
  • this的值在进入上下文时决定,并且在执行过程中不可变。ie.不能像变量赋值一样为this赋值。
var foo = {x: 10};

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

    this = foo; // error, can't change this value
    alert(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;
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的取值呢?在有些文章和书中有这样的说法:

this的取值由函数的定义方式决定,如果函数被定义成全局函数,则对应的this是global;如果函数是一个对象的方法(method),则this的值是对应的对象。

这种说法是错误的。来看一个例子:

Is the definition of function determing this

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

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

baz虽然和foo.bar的值一样,也就是指向同一个函数。但是调用结果不一样,因此this的取值不是由函数的定义决定的。

事实上,this的值由调用者提供(例如调用函数的父上下文),由函数的调用方式决定。

为什么this的取值由函数的调用方式决定?

要回答这个问题需要先看下ECMA中的一个内部类型(internal type) -- Reference type

Reference type

Refference是一个内部类型,没有任何函数能返回Reference类型的值,无论是built-in函数还是用户定义的函数。

规范中关于Reference描述如下:

The internal Reference type is not a language data type. It is defined by this specification purely for expository purposes. However, a value of type Reference is used only as an intermediate result of expression evaluation and cannot be stored as the value of a variable or property.

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

other use of the Reference type is to explain the determination of the this value for a function call.

A Reference is a reference to a property of an object. A Reference consists of two components, the base object and the property name.

The following abstract operations are used in this specification to access the components of references:

  1. GetBase(V). Returns the base object component of the reference V.
  2. GetPropertyName(V). Returns the property name component of the reference V.

Reference的结构可以描述如下:


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

根据规范,仅在以下两种情况时会返回Reference类型的值:

  • 标识符解析(identifiers resolution),标识符包括变量名,函数名,函数参数名,还有就是上面提到的不用var声明,直接赋值的变量。
  • 属性访问(Property access),属性访问有两种方式,通过[]或者.,如foo.barfoo['bar']
// 标识符解析
var foo = 10;
function bar() {}

// 对应的Reference的值如下
var fooReference = {
    base: global,
    propertyName: 'foo'
};

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

// 属性访问
foo.bar();
foo['bar']();

// 对应的Reference type值
var fooBarReference = {
    base: foo,
    propertyName: 'bar'
};

Reference是一个中间值,要获取real value,需要用到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]]函数会返回对象属性的值,该函数同时会考虑原型链上的继承属性。

综上,关于this的取值:

  • 函数中this的取值由调用者提供,由函数的当前调用方式决定
  • 在函数调用括号()的左边如果是一个Reference类型的值,那么this的值将被设置为该值的base属性的值
  • 所有其它情况(比如()左边的值不是Reference类型),this都将被设置为null,因为null没有任何意义,它们将会被隐式的转换成global object
// eg1
function foo() {
    return this;
}

foo(); // global

// 对应的Reference值
var fooReference = {
    base: global,
    propertyName: 'foo'
};

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

foo.bar(); // foo

// 对应的Reference值
var fooBarReference = {
    base: foo,
    propertyName: 'bar'
};

// eg3
// 我们换一种形式调用foo.bar
var test = foo.bar;
test(); // global

// 因为对应的Reference值变成下面这样
var testReference = {
    base: global,
    propertyName: 'test'
};

在上面的demo里

  • eg1中调用foo()时,根据上面提到的原则,foo是一个变量,经过标识符解析会返回一个Reference type的值fooReference,this被设置成fooReference对象的base属性的值--global
  • 在eg2中调用foo.bar()时,bar作为foo的一个属性,经过属性读取(property access)会返回一个Reference类型的值fooBarReference,this被设置成base属性的值--foo
  • 在eg3中我们把foo.bar赋值给了test,test作为标识符,在调用test的时候产生了testReference,因此返回的就是global

现在理解了同一个函数,使用不同的方式调用,为什么返回值不同了。

来看两个问题,考虑下执行结果将是怎样的:

// 思考题一:
function foo () {
    console.log(this);
}

foo(); // ?
foo.prototype.constructor(); // ?


// 思考题二:
function foo() {
    console.log(this.bar);
}

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

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

x.test(); // ?
y.test(); // ?

Function call and non-Reference type

上面提到过,当函数调用()的左侧不是Reference类型值的时候,this将被设置为null,继而被隐式的转换成global。

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

上面例子中()的左侧是一个函数对象,既不是标识符也不是属性访问,因此不会返回Reference值,this将被设置为null,继而被转为global。

再来看几个复杂点的例子:

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?

Reference

comments powered by Disqus