Javascript 中的 this

当我们学习 Javascript 中的 this 时,非常容易陷入一种困境,一种似懂非懂的困境。在某些情况下,我们看了一些文章和解释,将其应用到一些简单的情况,发现,嗯,确实这么运作了。而在另一些更为复杂的情况下,我们发现又懵逼了,什么情况?这篇文章的目的,就是要完全搞懂并掌握 Javascript 中的 this。为什么我们很难完全掌握 this?在我看来,原因是 this 的解释太过抽象,在理论上是这样,到了实际应用时,却无法直接应用。同时,一些写 this 的文章可能并未覆盖全面所有 this 的情况,如果缺乏理论和实际的互相印证,以及一些深入揭示 this 原理的实例分析,那么 this 确实是很难完全掌握的。还好,有这篇文章,看完这篇文章后,你将完全掌握 Javascript 中的 this。

确定 this 的规则

this 是 Javascript 当前执行上下文中 ThisBinding 的值,所以,可以理解为,它是一个变量。现在的关键问题是,ThisBinding 是什么?ThisBinding 是 Javascript 解释器在求值(evaluate)js 代码时维护的一个变量,它是一个指向一个对象的引用,有点像一个特殊的 CPU 寄存器。解释器在建立一个执行上下文的时候会更新 ThisBinding。
当一个函数被执行时,它的 this 值被赋值。一个函数的 this 的值是由它的调用位置决定的。但是找到调用位置,需要跟踪函数的调用链,这有时候是非常复杂和困难的。有一个更简单的确定 this 的方式,就是直接找到调用该函数的对象。Arnav Aggarwal 提出了一个简单的能确定 this 值的规则,优先级按文中的先后顺序:

1. 构造函数(constructor)中的 this,通过 new 操作符来调用一个函数时,这个函数就变成为构造函数。new 操作符创建了一个新的对象,并将其通过 this 传给构造函数。

function ConstructorExample() {
    console.log(this);
    this.value = 10;
    console.log(this);
}
new ConstructorExample();
// -> {}
// -> { value: 10 }

new 操作符在 Javascript 中的实现大致如下:

function newOperator(Constr, arrayWithArgs) {
    var thisValue = Object.create(Constr.prototype);
    Constr.apply(thisValue, arrayWithArgs);
    return thisValue;
}

2. 如果 apply,call 或者 bind 用于调用、创建一个函数,函数中的 this 是作为参数传入这些方法的参数

function fn() {
    console.log(this);
}
var obj = {
    value: 5
};
var boundFn = fn.bind(obj);
boundFn();     // -> { value: 5 }
fn.call(obj);  // -> { value: 5 }
fn.apply(obj); // -> { value: 5 }

3. 当函数作为对象里的方法被调用时,函数内的this是调用该函数的对象。比如当obj.method()被调用时,函数内的 this 将绑定到obj对象

var obj = {
    method: function () {
        console.log(this === obj); // true
    }
}
obj.method();

4. 如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下this的值指向window对象,但是在严格模式下('use strict'),this的值为undefined

  • sloppy mode:
function sloppyFunc() {
    console.log(this === window); // true
}
sloppyFunc();
  • strict mode:
function strictFunc() {
    'use strict';
    console.log(this === undefined); // true
}
strictFunc();

需要注意的是,以下情况下,默认为 strict mode:

  • Module code is always strict mode code.
  • All parts of a ClassDeclaration or a ClassExpression are strict mode code.

第二个需要注意的点是,在 nodejs 的 module 中,this 指向 module.exports:

// `global` (not `window`) refers to global object:
console.log(Math === global.Math); // true

// `this` doesn’t refer to the global object:
console.log(this !== global); // true
// `this` refers to a module’s exports:
console.log(this === module.exports); // true

5. 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定this的值

6. 如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this被设置为它被创建时的上下文

const obj = {
   value: 'abc',
   createArrowFn: function() {
       return () => console.log(this);
   }
};
const arrowFn = obj.createArrowFn();
arrowFn(); // -> { value: 'abc', createArrowFn: ƒ }

一些进阶情况下的 this

以上确定 this 的规则,能满足大部分简单情况下 this 的确定,然而,在一些进阶情况下,我们还是难以确定 this,下面将分析一些进阶情况下的 this:

闭包中的函数中的 this

看下面这个例子,用上一节的确定 this 规则,可以看到 inner 函数中的 this 符合第四条规则,在 strict 模式下,this 为 undefined。这是因为 inner 函数有自己的 this,可以这么理解:每个非箭头函数其实都有其自己的隐式的 this 参数,而这里 inner 函数并没有明确的调用其的对象,也没有被 apply、call 或 bind,其隐式的 this 在 strict 模式下则为 undefined,在宽松模式下则为 window 对象。

function outer() {
    'use strict';
    function inner() {
        console.log(this); // undefined
    }

    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

一个类似的例子,在构造函数中调用外部函数:

'use strict';

function getThis() {
  console.log(this); // undefined
}

function Dog(saying) {
  this.saying = saying;
  getThis();
  console.log(this); // Dog {saying: "wang wang"}
}

new Dog('wang wang');

另一个例子,通过 this 调用构造函数中的方法,this 是 new 操作符创建的一个新对象,getThis 是该对象中的一个方法,this.getThis,符合规则 3,对象中调用对象的方法,所以 getThis 中的 this 为其调用其的对象,即 Dog {saying: "wang wang"}:

function Dog(saying) {
  this.saying = saying;
  this.getThis = function() {
    console.log(this); // Dog {saying: "wang wang"}
  };
  this.getThis();
  console.log(this); // Dog {saying: "wang wang"}
}

new Dog('wang wang');

回调函数中的 this

回调函数和闭包的情况类似,看下面例子:

'use strict';
function getThis() {
  console.log(this);
}

function higherOrder(callback) {
  console.log(this);
  callback();
}

higherOrder(getThis);

higherOrder.call({ a: 1 }, getThis);

// undefined
// undefined
// {a: 1}
// undefined

用 new 来调用回调函数的情况,满足规则1,this 为新创建的对象:

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

function callbackAsConstructor(callback) {
  new callback();
}

callbackAsConstructor(getThis);

// getThis {}

原生 js 提供的 api 中的回调函数中的 this

setTimeout 中回调函数中的 this,在浏览器中是 window 对象,在 node 环境下为 Timeout 对象

'use strict';
function getThis() {
  console.log(this);
}
setTimeout(getThis, 0);

// window or Timeout {_called: true, _idleTimeout: 1, _idlePrev: null, _idleNext: null, _idleStart: 338, …}

dom 事件回调函数,包含 html 中的内联回调函数和 js 中的事件回调函数。当内联回调函数直接在定义在内联代码中或者在内联代码中被调用,它们的 this 还是 window。而如果在内联代码中直接使用 this,则 this 指向对应的 dom 元素。在 js 中监听事件,回调函数中的 this 指向对应的 dom 元素。

<h3>Using `this` "directly" inside event handler or event property</h3>
<button id="button1">click() "assigned" using addEventListner()</button><br />
<button id="button2">click() "assigned" using click()</button><br />
<button id="button3" onclick="alert(this+ ' : ' + this.tagName + ' : ' + this.id);">
  used `this` directly in click event property
</button>

<h3>Using `this` "indirectly" inside event handler or event property</h3>
<button onclick="alert((function(){return this + ' : ' + this.tagName + ' : ' + this.id;})());">
  `this` used indirectly, inside function <br />
  defined & called inside event property</button
><br />

<button id="button4" onclick="clickedMe()">
  `this` used indirectly, inside function <br />
  called inside event property
</button>
<br />

<script>
  function clickedMe() {
    alert(this + ' : ' + this.tagName + ' : ' + this.id);
  }
  document.getElementById('button1').addEventListener('click', clickedMe, false);
  document.getElementById('button2').onclick = clickedMe;
</script>

eval 中的 this

eval 可以被直接或间接调用,当 eval 被间接调用时,其 this 为 global 对象。当 eval 被直接调用时,this 和它被包围处的 this 一致:

(0, eval)('this === window') // true

// Real functions
function sloppyFunc() {
    console.log(eval('this') === window); // true
}
sloppyFunc();

function strictFunc() {
    'use strict';
    console.log(eval('this') === undefined); // true
}
strictFunc();

// Constructors
var savedThis;
function Constr() {
    savedThis = eval('this');
}
var inst = new Constr();
console.log(savedThis === inst); // true

// Methods
var obj = {
    method: function () {
        console.log(eval('this') === obj); // true
    }
}
obj.method();

this 实例

  • 经典例子,函数中的 this 取决于调用它的对象
var obj = {
    value: 'hi',
    printThis: function() {
        console.log(this);
    }
};
var print = obj.printThis;
obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}
  • 多条规则组合
var obj1 = {
    value: 'hi',
    print: function() {
        console.log(this);
    },
};
var obj2 = { value: 17 };

obj1.print.call(obj2); // -> { value: 17 }
new obj1.print(); // -> {}
  • 陷阱:忘记使用 new 操作符
function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p = Point(7, 5); // we forgot new!
console.log(p === undefined); // true

// Global variables have been created:
console.log(x); // 7
console.log(y); // 5

// strict mode
function Point(x, y) {
    'use strict';
    this.x = x;
    this.y = y;
}
var p = Point(7, 5);
// TypeError: Cannot set property 'x' of undefined
  • 陷阱:回调函数中的 this,宽松模式下指向 window 对象
function callIt(func) {
    func();
}
var counter = {
    count: 0,
    // Sloppy-mode method
    inc: function () {
        this.count++;
    }
}

callIt(counter.inc);

// Didn’t work:
console.log(counter.count); // 0

// Instead, a global variable has been created
// (NaN is result of applying ++ to undefined):
console.log(count);  // NaN

修复方式:

callIt(counter.inc.bind(counter));
  • 陷阱:this 被屏蔽
var obj = {
    name: 'Jane',
    friends: [ 'Tarzan', 'Cheeta' ],
    loop: function () {
        'use strict';
        this.friends.forEach(
            function (friend) {
                console.log(this.name+' knows '+friend);
            }
        );
    }
};
obj.loop();
// TypeError: Cannot read property 'name' of undefined

修复方式有很多,个人比较喜欢的方式是直接用箭头函数:

var obj = {
    name: 'Jane',
    friends: [ 'Tarzan', 'Cheeta' ],
    loop: function () {
        'use strict';
        this.friends.forEach(
             (friend) => {
                console.log(this.name+' knows '+friend);
            }
        );
    }
};
obj.loop();
  • 陷阱:callback(Promises) 中获取 this 的问题
// Inside a class or an object literal:
performCleanup() {
    cleanupAsync()
    .then(function () {
        this.logStatus('Done'); // 这里会失败
    });
}

// 修复
// Inside a class or an object literal:
performCleanup() {
    cleanupAsync()
    .then(() => {
        this.logStatus('Done');
    });
}
  • eval 例子,myFun 没有明确调用对象,满足规则 4
function myFun() {
    return this; // window
}
var obj = {
    myMethod: function () {
        eval("myFun()");
    }
};
  • 复杂例子,你能梳理清楚吗?
const throttle = (fn, time) => {
  let last;
  let timerId;
  return function(...args) {
    const now = Date.now();
    if (last && now - last < time) {
      clearTimeout(timerId);
      timerId = setTimeout(() => {
        last = now;
        fn.apply(this, args);
      }, time);
    } else {
      last = now;
      fn.apply(this, args);
    }
  };
};

const hi = new func();

hi.deGetA();

const timerId = setInterval(hi.deGetA, 100);

setTimeout(() => {
  clearInterval(timerId);
}, 1000);

this 最佳实践

  • 使用箭头函数,箭头函数的 this 被设定为在其创建时的上下文,而不是被调用时确定,这样可以避免很多问题。
  • 将函数作为回调函数传入另一个函数中的时候要特别注意,如果发现不对,可以考虑下使用 bind 方法,使得回调函数的 this 始终指向你想要的 this
  • 在函数内调用回调函数时,如果希望回调函数和外部函数的 this 一致,可以考虑使用 apply,call 或 bind,合理使用这些方法,可以降低使用者在使用你的函数时可能发生的错误

参考文献

上一篇:深入理解 react/redux 数据流并基于其优化前端性能


下一篇:ES6 iterator 和 generator