Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript 中的 this #22

Open
shaozj opened this issue Jul 23, 2019 · 4 comments
Open

Javascript 中的 this #22

shaozj opened this issue Jul 23, 2019 · 4 comments
Assignees

Comments

@shaozj
Copy link
Owner

shaozj commented Jul 23, 2019

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,合理使用这些方法,可以降低使用者在使用你的函数时可能发生的错误

参考文献

@Littlegrace111
Copy link

我居然看完了

@shaozj
Copy link
Owner Author

shaozj commented Aug 23, 2019

我居然看完了

然后呢?

@Littlegrace111
Copy link

我居然看完了

然后呢?

居然把blog写在issue里,为啥不发到ata啊?

@shaozj
Copy link
Owner Author

shaozj commented Aug 24, 2019

我居然看完了

然后呢?

居然把blog写在issue里,为啥不发到ata啊?

主要考虑是不是太基础了,哈哈

@shaozj shaozj self-assigned this Aug 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants