你的浏览器不支持canvas

Enjoy life!

javascript - this

Date: Author: JM

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。

一、 this 定义

  • this 是 “执行上下文”的属性。
  • this与上下文的可执行代码类型有关。
  • this 的值在进入执行上下文时就已经决定了,而且在执行代码阶段中是不能改变的。
activeExecutionContext = {
  VO: {...},
  this: thisValue
};

二、全局作用域下的this

  • 全局作用域下的 this 为 全局对象自己,即 window对象。 即:this === window, true
  • 点击打开demo
this.a = 10; // 等价 window.a = 10;
console.log(a); // 10

b = 20; 
console.log(this.b); // 20

var c = 30;
console.log(this.c); // 30

三、 函数作用域下的this

3.1 介绍

  • 函数作用域下的 this的第一个特性(最主要的特征):this并不是静态绑定到函数上的。
  • 就像前面所讲的,this 的值决定于所进入的上下文,因此它在函数代码中的话,其值是可以不同的。
  • 但是,只要进入了执行代码阶段,this的值就不可变了。
    • 例如:我们根本不可能给this赋一个新值,因为 this 根本就不是变量。
  • 点击打开demo
var foo = {x: 10};
 
var bar = {
  x: 20,
  test: function () {
 
    console.log(this === bar); // true
    console.log(this.x); // 20
     
    this = foo; // 报错,不可以改变 this 的值
  
    console.log(this.x); // 如果没有报错的话,这里是 10 而不是 20.
  }
};

// 在进入上下文,this的值决定于 bar 对象
bar.test(); // true, 20

3.2 函数代码内影响this值变化的原因

// bar 的代码参考 3.1

// 将 bar.test 的引用 赋给 foo.test ,此时 this 的值变为 foo
foo.test = bar.test;
foo.test(); // false, 10

  • 在一般的函数调用中,this的值是由 激活上下文代码的调用者 决定的,例:调用函数的外层上下文
  • this 的值是由 调用表达式的形式 决定的。
  • 一些关于 JavaScript 的文章和书籍中指出的“this的值取决于函数的定义方式,如果是全局函数,则 this 的值就会设置为全局对象,如果是某个对象的方法,则 this 的值就会设置为该对象”,这个说法其实是错误的。请看以下例子:
function foo () {
  console.log(this);
}

foo(); // window,因为

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

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

foo.prototype.constructor(); // {constructor: f} -- foo.prototype, 因为

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
}
  • 从上述代码可以看出,foo 即使是全局函数,但它 被不同的调用表达式激活,即:foo() 以及 foo.prototype.constructor(),其this的值也会受到影响 — 即: this 值不同。上述代码的结果展示:
  • 点击打开demo

relationship-map


  • 在对象中定义的方法,其 this 值并不全是这个对象。请看下面代码:
  • 点击打开demo
var foo = {
    bar: function () {
      console.log(this,this === foo);
    }
};

foo.bar(); // foo, true

// 将 foo.bar 复制给全局变量 exampleFunc,使用另外一种形式的调用表达式
// foo.bar 和 exampleFunc 两个函数是相等的,但是其 this值是不一样的。

var exampleFunc = foo.bar;

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

exampleFunc(); // window, false

3.3 引用类型(reference type)

  • 引用类型 与 this 的值是哪一个有着很大的关系,我们可以通过引用类型回答 3.2 展示的例子 “会输出这些结果的原因”的问题。
  • 通过使用伪代码,引用类型的值可以表示为具有两个属性的对象:
    • base:属性所属的对象。
    • propertyNamebase对象中的属性名。
var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
}

  • 引用类型的值又是如何影响函数上下文中this的值呢?
  • 在函数上下文中,this 的值由执行者提供以及由调用表达式的当前形式决定
  • 如果在调用括号(…)的左侧,有引用类型的值,那么this的值可设置此引用类型的 base 对象(即:伪代码中base的值)
  • 在所有其他情况下(即非引用类型),this 的值始终为 null 。 但是由于 null 对于 this 来说没有任何意义,因此隐式转换为全局对象,global ===》 window

  • 根据上述观点,我们可以判断this的取值了。不过,获取引用类型的值只能是以下两种情况:
  • 1.当我们处理一个标识符时 【 标识符可以是变量名、函数名、形参以及全局对象的未受限属性;并且关于标识符的算法总是返回一个引用类型的值】。请看下面代码:
  • 点击打开demo
var foo = 10;
function bar () {
  console.log(this);
}

bar(); // window
  • 伪代码表示:
var fooReference = {
  base: global,
  propertyName: 'foo'
};

var barReference = {
  base: global,
  propertyName: 'bar'
}
  • 可以看到,调用bar函数后,输出的this值为window。原因:调用括号的左侧是引用类型的值(bar是标识符),伪代码中的base值为global,即 windowthis就会设置为引用类型值的base对象window

为了从引用类型的值中获取对象真正的值需使用GetValue方法,使用伪代码描述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"

  • 2.当我们通过属性访问 【包括点表示法和方括号表示法】,请看以下代码:
  • 点击打开demo
var foo = {
    bar: function () {
      console.log(this);
    }
};

foo.bar(); // foo
foo['bar'](); // foo
  • 伪代码
var fooBarReference = {
  base: foo,
  propertyName: bar
};
  • 可以看到,调用bar函数后,输出的this值为foo,原因:其base对象是foo对象,激活bar函数时,this就设为foo对象了。

  • 不要忘记:即使是同一个函数,但是调用表达式的形式不同,其 this的值也是不同的。原因:处理过程中,是不同的引用类型的值。请看下面代码:
  • 点击打开demo
var foo = {
    bar: function () {
      console.log(this);
    }
};

var test = foo.bar;

test(); // window
  • 伪代码
var testReference = {
  base: global,
  propertyName: 'test'
};
  • test 即使和 foo.bar 共享同一个函数,但是调用 test 后输出的 this 值是window 而不是 foo。原因:test是标识符,就会产生另外的引用类型的值,其中base对象(window)就是this的值。

  • 以下是另外一种(典型的)利用调用表达式形式来动态决定this值的例子:
  • 点击打开demo
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

3.4 函数调用和非引用类型

  • 如果在调用括号的左边不是引用类型而是其他类型时,this 会自动设置为null,并最终会指向 window 对象。
(function() {
  console.log(this); // null ===》 window
})()
  • 在上述代码中,我们有函数对象但这个对象并不是引用类型(因为它不是标识符也不通过属性访问),因此,this最终被设为window对象。
  • 再看以下例子:
  • 点击打开demo

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

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

(foo.bar = foo.bar)(); // window
(false || foo.bar)(); // window
(foo.bar,foo.bar)(); // window
  • 从上述代码我们可以看到:都是通过属性访问,但是其输出的this值不全是foo,也有window,这是为什么?
  • 事实上,最后的三个调用,在添加了特定的操作后,调用括号左侧的值就不再是引用类型。
  • 第一个调用,非常明确,是引用类型,最终 this 的值设为 base 对象 foo
  • 第二个调用,有一个 组操作符(grouping operator ,该操作符不会触发调用获取引用类型实际值的方法,例如:GetValue方法。如果处理组操作符中间过程中——获得的仍然是一个引用类型的值,那么this值仍为base object,即foo
  • 第三个调用,赋值操作符(assignment operator 不像组操作符,它会调用GetValue方法。它最后返回的是函数对象而不是引用类型的值,所以this会设为null,即:window
  • 第四、五个调用,逗号(comma)操作符 和 逻辑 OR 操作符都会调用 GetValue方法,于是我们失去了原来的引用类型的值,变成了函数类型,因此,this的值为window

3.5 引用类型 和 null

  • 有一种情况:当调用表达式在调用括号左侧是引用类型的值时,但是this被设置为null,结果是指向window。发生这种情况的条件是当引用类型的base对象是活动对象(activation object)有关。
  • 当内部子函数在父函数中被调用就会发生这种情况。我们知道,局部变量、嵌套函数和形参都可以保存在指定函数的活动对象中。
function foo () {
  function bar () {
    console.log(this); // window
  }
  bar(); // 与 AO.bar() 一样
}
  • 活动对象经常返回的 this 值为 null,即:伪代码的 AO.bar() 等价于 null.bar()。因此,this的值仍然是window

  • with
  • 当函数调用在with代码块中,并且with对象包含函数属性时,就会出现例外。
  • with语句会将这个对象添加到作用域链的最前面,在活动对象之前。
  • 相应地,在引用类型的值(标识符或者属性访问)情况下,base对象不是活动对象而是with语句里的对象。
  • 它不仅仅只针对内部函数,全局函数也是如此。
    • 原因:with对象掩盖了作用域链中更高层的对象。(全局对象或活跃对象)。
  • 点击打开demo
var x = 10;

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

// 因为
var fooReference = {
  base: __withObject,
  propertyName: 'foo'
}

  • try-catch
  • 当调用的函数是catch子句的参数时,也有类似的情况:catch对象也会被添加到作用域链的前端,在活动对象或者全局对象之前。
  • 然而,这个行为在 ECMA-262-3 中被指出是个 bug,并且已经在 ECMA-262-5 中修正了;因此,在这种情况下,this 的值应该设置为全局对象,而不是 catch 对象
  • 点击打开demo
try {
  throw function () {
    console.log(`this:` + this);
  }
} catch (e) {
  e(); // __catchObject - in ES3, global - fixed in ES5
}

var eReference = {
  base: __catchObjec,
  propertyName: 'e'
}

// 然而,既然这是个bug
// 那就应该强制设置为全局对象
// null => global
 
var eReference = {
  base: global,
  propertyName: 'e'
};

  • 相同的情况也会发生在 递归调用 一个 非匿名函数
  • 在第一次调用时,base 对象是外层的活动对象或者全局对象。
  • 在接下来的递归调用,base 对象应该是一个存储了可选函数表达式名的特殊对象。
  • 然而,事实却是:在这种情况下,this 的值永远是全局对象。
  • 点击打开demo
(function foo (bar) {
  console.log(this);
  
  !bar&&foo(1);
})();

3.6 函数作为构造器被调用时this的值

function A () {
  console.log(this); // 新创建的对象 - 下面的`a`对象
  this.x = 10;
}
var a = new A();
console.log(a.x); // 10
  • 在这种情况下,new 操作符会调用“A”函数的内部 [[Construct]]。 在对象创建之后,会调用内部的 [[Call]] 函数,然后所有“A”函数中 this 的值会设置为新创建的对象。
  • 点击打开demo

3.6 手动设置函数调用时this的值


对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。