JavaScript 的 this 机制并没有那么复杂
为什么会有 this?
在如何使用 this 之前, 我们要搞清楚一个问题, 为什么要使用 this.
下面的代码尝试去说明 this 的使用动机:
- function identify() {
- return this.name.toUpperCase();
- }
- function speak() {
- var greeting = "Hello, I'm " + identify.call( this );
- console.log( greeting );
- }
- var me = {
- name: "Kyle"
- };
- var you = {
- name: "Reader"
- };
- identify.call( me ); // KYLE
- identify.call( you ); // READER
- speak.call( me ); // Hello, I'm KYLE
- speak.call( you ); // Hello, I'm READER
这段代码使得函数 identify() 和 speak() 可以在多个上下文 (me 和 you) 对象中重用, 不用给每个对象分别创建函数.
如果不用 this, 你也可以将上下文对象直接传入函数:
- function identify(context) {
- return context.name.toUpperCase();
- }
- function speak(context) {
- var greeting = "Hello, I'm " + identify( context );
- console.log( greeting );
- }
- identify( you ); // READER
- speak( me ); // Hello, I'm KYLE
然而 this 机制可以隐式地传递一个对象引用, 使得 API 设计得更简洁和更容易复用.
你的使用模式越复杂, 你就能更加明白, 显式传递一个参数经常比传递 this 上下文还混乱.
困惑
在解释 this 如何工作之前, 必须要先摒弃错误的概念. 开发者们总是太过依赖 this 的字面意思.
引用自身 Itself
一种普遍的错误是认为 this 指代这个函数自身.
为什么你会想从一个函数内部引用它自己呢, 通常的原因是递归, 或者事件回调函数在被调用之后解除绑定.
JS 新手会认为将函数作为对象引用可以在函数调用期间存储状态(属性的值). 这确实是可以的但是用处有限, 后面会介绍其它模式, 除了函数对象本身还有更好的存储状态的地方.
下面的代码会说明, this 并不会像我们以为的那样让函数得到对自身的引用:
- function foo(num) {
- console.log( "foo:" + num );
- // keep track of how many times `foo` is called
- this.count++;
- }
- foo.count = 0;
- var i;
- for (i=0; i<10; i++) {
- if (i> 5) {
- foo( i );
- }
- }
- // foo: 6
- // foo: 7
- // foo: 8
- // foo: 9
- // how many times was `foo` called?
- console.log( foo.count ); // 0 -- WTF?
foo.count 还是 0 , 循环确实执行了 4 次, console.log 也确实被调用了 4 次.
foo.count = 0 执行之后, 实际上给函数对象 foo 添加了一个属性 count.
但是在函数内部的 this.count 中, this 实际上并不指向这个函数对象, 即使这个属性名字是一样的, 但属性所在的对象是不同的.
如果 foo 的属性 count 的值没有改变, 那么我们改变的究竟是什么. 实际上, 如果你再深究一下, 就会发现, 这段代码意外地创建了一个全局变量 count, 而且当时会有一个值 NaN(具体看这个系列的第二节).
很多开发者就会通过别的方式避免这个问题, 比如创建另外一个对象储存这个属性 coun:
- function foo(num) {
- console.log( "foo:" + num );
- // keep track of how many times `foo` is called
- data.count++;
- }
- var data = {
- count: 0
- };
- var i;
- for (i=0; i<10; i++) {
- if (i> 5) {
- foo( i );
- }
- }
- // foo: 6
- // foo: 7
- // foo: 8
- // foo: 9
- // how many times was `foo` called?
- console.log( data.count ); // 4
这确实解决了问题, 但是很遗憾这是忽略了真正的问题 -- 不理解 this 的含义和用法, 只是回到熟悉的词法作用域机制.
如果想在一个函数对象内部引用自身, this 是不够的, 你需要一个标识符:
- function foo() {
- foo.count = 4; // `foo` refers to itself
- }
- setTimeout( function(){
- // anonymous function (no name), cannot
- // refer to itself
- }, 10 );
在第一个函数中, 函数被命名为 foo, 这个标识符 foo 就可以用来指代这个函数对象自身.
但在第二段中, 回调函数没有名字, 所以没办法引用自己.
注: 老派的已经被废弃的 arguments.callee 在函数中可以用来指代正在执行的函数对象. 这是在匿名函数内部访问函数对象的唯一方式.
当然最好的方式还是避免匿名函数的使用.
另外一种解决办法就是使用 foo 标识符, 不使用 this:
- function foo(num) {
- console.log( "foo:" + num );
- // keep track of how many times `foo` is called
- foo.count++;
- }
- foo.count = 0;
- var i;
- for (i=0; i<10; i++) {
- if (i> 5) {
- foo( i );
- }
- }
- // foo: 6
- // foo: 7
- // foo: 8
- // foo: 9
- // how many times was `foo` called?
- console.log( foo.count ); // 4
然而这种方法同样回避了对 this 的理解.
另外一种解决这个问题的方式是, 将 this 强制绑定到 foo 这个函数对象上:
- function foo(num) {
- console.log( "foo:" + num );
- // keep track of how many times `foo` is called
- // Note: `this` IS actually `foo` now, based on
- // how `foo` is called (see below)
- this.count++;
- }
- foo.count = 0;
- var i;
- for (i=0; i<10; i++) {
- if (i> 5) {
- // using `call(..)`, we ensure the `this`
- // points at the function object (`foo`) itself
- foo.call( foo, i );
- }
- }
- // foo: 6
- // foo: 7
- // foo: 8
- // foo: 9
- // how many times was `foo` called?
- console.log( foo.count ); // 4
作用域的引用 Its Scope
第二个常见的关于 this 的错误理解是, this 指向这个函数的作用域. 这是一个有点狡猾的问题, 因为在某种意义上这种说法是有些正确的, 但在另一种意义上, 这又是被误导的.
首先, this 并没有指向函数的词法作用域. 作用域确实就像是一个包含所有标识符属性的对象, 但是这个作用域 "对象" 是无法被代码直接访问的, 这是引擎内部实现的.
所以下面的代码是错误的:
- function foo() {
- var a = 2;
- this.bar();
- }
- function bar() {
- console.log( this.a );
- }
- foo(); //undefined
你可能觉得这段代码很做作, 但这是摘自一些帮助论坛里的真实代码.
首先, 这段代码试图通过 this.bar() 引用函数 bar(), 能运行起来也是巧合. 调用 bar() 最自然的方式就是直接使用标识符引用, 去掉前面的 this.
然而, 写这段代码的开发者其实是想让 bar() 访问 foo() 内部的变量 a, 但 this 不能被用来查询词法作用域的.
this 到底是什么
前面讲到过, this 是在运行时绑定的, 它的上下文环境取决于函数调用的条件. this 的绑定和函数声明的位置没有关系, 和函数调用的位置有关.
当一个函数被调用时, 一个执行上下文被创建. 这个上下文记录包含函数调用的位置, 函数调用的方式以及传入的参数这些信息. this 的引用就是在这个时候决定的.
在下一节中, 会介绍根据一个函数的调用位置确定它执行过程中将如何绑定 this.
小结:
this 既不指代函数本身, 也不指代函数的词法作用域.
this 是在函数调用的时候绑定的, 它引用的内容完全取决于函数调用的位置.
来源: https://www.cnblogs.com/xiyouchen/p/10320667.html