前端核心知识点1:this指向


this 指向的5个规则:


  1. 如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的对象。

  2. 如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数。

  3. 如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象。

  4. 如果一个函数作为FFI被调用,意味着这个函数不符合以上任意一种调用方式,this指向全局对象,在浏览器中,即是window。

  5. 如果出现上面对条规则的累加情况,则优先级自1至4递减,this的指向按照优先级最高的规则判断。




相关实例:


1、全局环境下的 this

function f1() {  console.log(this);}
function f2() { 'use strict'; console.log(this);}
f1(); // windowf2(); // undefine


const foo = {  bar: 10,  fn: function () {    console.log(this);    console.log(this.bar);  },};
var fn1 = foo.fn;fn1(); // window undefine


这里 this 仍然指向的是 window。虽然 fn 函数在 foo 对象中作为方法被引用,但是在赋值给 fn1 之后,fn1 的执行仍然是在 window 的全局环境中。因此输出 window 和 undefined,它们相当于:

console.log(this)console.log(window.bar)

还是上面这道题目,如果调用改变为:

const foo = {  bar: 10,  fn: function () {    console.log(this);    console.log(this.bar);  },};
foo.fn();
// { bar: 10, fn:f}// 10

因为这个时候 this 指向的是最后调用它的对象,在 foo.fn() 语句中 this 指向 foo 对象。请记住:在执行函数时,如果函数中的 this 是被上一级的对象所调用,那么 this 指向的就是上一级的对象;否则指向全局环境


2、上下文对象调用中的this


如上结论,面对下题时我们便不再困惑:

const student = {  name: 'Lucas',  fn: function () {    return this;  },};
console.log(student.fn() === student); // true

最终结果将会返回 true。
当存在更复杂的调用关系时,请看例题:

const person = {  name: 'Lucas',  brother: {    name: 'Mike',    fn: function () {      return this.name;    },  },};
console.log(person.brother.fn()); // Mike

到此,this 的上下文对象调用已经理解得比较清楚了。让我再看一道更高阶的题目:

const o1 = {  text: 'o1',  fn: function () {    return this.text;  },};
const o2 = { text: 'o2', fn: function () { return o1.fn(); },};
const o3 = { text: 'o3', fn: function () { var fn = o1.fn; return fn(); },};
console.log(o1.fn()); // o1console.log(o2.fn()); // o1console.log(o3.fn()); // undefine

答案是:o1、o1、undefined,你答对了吗?


我们来一一分析。

  • 第一个 console 最简单,o1 没有问题。难点在第二个和第三个上面,关键还是看调用 this 的那个函数。

  • 第二个 console 的 o2.fn(),最终还是调用 o1.fn(),因此答案仍然是 o1

  • 最后一个,在进行 var fn = o1.fn 赋值之后,是「裸奔」调用,因此这里的 this 指向 window,答案当然是 undefined。


如果面试者回答顺利,可以紧接着追问,如果我们需要让:

console.log(o2.fn())

输出 o2,该怎么做?


一般开发者可能会想到使用 bind/call/apply 来对 this 的指向进行干预,这确实是一种思路。但是我接着问,如果不能使用 bind/call/apply,有别的方法吗?


这样可以考察候选人基础掌握的深度以及随机应变的思维能力。答案为:

const o1 = {  text: 'o1',  fn: function () {    return this.text;  },};
const o2 = { text: 'o2', fn: o1.fn,};
console.log(o2.fn()); // o2

还是应用那个重要的结论:this 指向最后调用它的对象,在 fn 执行时,挂到 o2 对象上即可,我们提前进行了赋值操作。


3、bind/call/apply 改变this指向

const foo = {  name: 'lucas',  logName: function () {    console.log(this.name);  },};
const bar = { name: 'mike',};
foo.logName.call(bar); // mike

将会输出 mike,这不难理解。但是对 call/apply/bind 的高级考察往往会结合构造函数以及组合式实现继承。实现继承的话题,我们会单独讲到。构造函数的使用案例,我们结合接下来的例题组合进行分析。


4、构造函数和this

function Foo() {  this.bar = 'Lucas';}
const instance = new Foo();console.log(instance.bar); // Lucas

答案将会输出 Lucas。但是这样的场景往往伴随着下一个问题:new 操作符调用构造函数,具体做了什么? 以下供参考:

  • 创建一个新的对象;

  • 将构造函数的 this 指向这个新对象;

  • 为这个对象添加属性、方法等;

  • 最终返回新对象。


5、箭头函数和this


首先我们再来温习一下相关结论。

结论:箭头函数使用 this 不适用以上标准规则,而是根据外层(函数或者全局)上下文来决定。

来看题目:

const foo = {  fn: function () {    setTimeout(function () {      console.log(this);    });  },};
foo.fn(); // window

这道题中,this 出现在 setTimeout() 中的匿名函数里,因此 this 指向 window 对象。如果需要 this 指向 foo这个 object 对象,可以巧用箭头函数解决:

const foo = {  fn: function () {    setTimeout(() => {      console.log(this);    });  },};
foo.fn(); // {fn:f}