努力说清this的指向和怎么改变this的指向

1,076 阅读5分钟

JS 中的 this,总是神神叨叨的,不小心就错了。

希望我自己写完本文之后,以后也按着现在捋顺的逻辑来分析 this。

TL;DR

  • 箭头函数的this,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的 this
  • 构造函数的this,会绑定到我们 new 出来的这个对象上
  • 在不使用call/apply/bind改变this指向的时候,普通函数的this在调用的时候,绑定到调用方,和它的位置没有关系
    • 立即执行函数、setTimeout、setInterval 内部是普通函数的时候,因为其调用方是window,所以thiswindow
  • call/apply/bind 均可改变 this 指向,且均可被手写实现

箭头函数里的this

箭头函数的this,和它书写的位置密切相关,在书写阶段(即声明位置)就绑定到它父作用域的 this

因为其由父作用域决定,所以父作用域至关重要。

  • 若箭头函数的父作用域是全局作用域,则 this 始终指向window
  • 若箭头函数的父作用域是函数作用域,则指向函数作用域的this
var a = 1;
var obj = {
  a: 2,
  func2: () => {
    // 父作用域是全局,this始终都是window
    console.log(this.a);
  },
  func3: function() {
    // 等同于func3作用域的this
    () => {
      console.log(this.a);
    };
  }
};
// func1
var func1 = () => {
  //  父作用域是全局,this任何时候都是window
  console.log(this.a);
};
// func2
var func2 = obj.func2;
// func3
var func3 = obj.func3;
func1();
func2();
func3();
obj.func2();
obj.func3();

构造函数里的 this

构造函数里面的 this 会绑定到我们 new 出来的这个对象上:

function Person(name) {
  this.name = name;
  console.log(this);
}
// this是person
var person = new Person("yan");

function 定义的普通函数

function 定义的普通函数,this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反。

换言之:不管方法被书写在哪个位置,它的 this 只会跟着它的调用方走, 再大白话点:xx.fn(),点前面是谁,fn 里的 this 就是谁。没有点就是 window。

注意上面的前提是:不使用call/apply/bind改变this指向的时候。

必须严格区分 “声明位置” 与 “调用位置”!!!

上面的秘诀掌握好了,下面的例子就是 easy 了

// 声明位置
var me = {
  name: "yan",
  hello: function() {
    console.log(`你好,我是${this.name}`);
  }
};
var you = {
  name: "xiaoming",
  hello: function() {
    var targetFunc = me.hello;
    targetFunc();
  }
};
var name = "BigBear";
// 调用位置
you.hello();

还有 2 种特殊点的情景: 没有调用方,所以 this 始终都是window

  • 立即执行函数(IIFE),(function(){})()
  • setTimeout/setInterval 中传入的普通函数,setTimeout(function(){...},1000)

!!!注意,必须是 function 写的普通函数,如果箭头函数,仍遵循箭头函数的规则

先看个立即执行函数的例子,

var name = "BigBear";
var obj = {
  name: "yan",
  fn: function() {
    (function() {
      console.log(this.name);
    })();
  }
};
obj.fn();

仔细看看就是自执行函数执行,this 肯定是 window,自然就是BigBear

注意!!!换成箭头函数的话,规则就变了,因为父作用域是 fn,其 this 绑定成了 obj,所以打印的话是yan

var obj = {
  fn: function() {
    (() => {
      console.log(this);
    })();
  }
};
obj.fn();

再看下 setTimeout

var name = "BigBear";
var me = {
  name: "yan",
  hello: function() {
    setTimeout(function() {
      console.log(`你好,我是${this.name}`);
    });
  }
};
me.hello();

setTimeout 里面的函数,this 指向window,自然值是BigBear。 同理,如果将 setTimeout 里面的函数修改成箭头函数,则打印yan了。

var name = "BigBear";
var me = {
  name: "yan",
  hello: function() {
    setTimeout(() => {
      console.log(`你好,我是${this.name}`);
    });
  }
};
me.hello();

严格模式下的 this

笔者不用严格模式,真要用的时候百度下吧。

改变 this 的指向

this 的指向,要么被书写位置限制,要么被调用位置限制,很是被动。

  • 对于箭头函数,因为其this只和书写位置有关,所以一般不修改箭头函数的 this 指向。
  • 构建函数,this 就是 new 出来的实例,所以一般不修改构建函数的 this 指向
  • 于是重点!!!对于 function 定义的普通函数,修改 this,必须要显示的调用call/apply/bind

一般问的修改 this,也是指 function 定义的普通函数。

call/apply/bind三者用法及区别:

  • call/apply 改变函数的 this,且函数立即执行。但 apply 的参数是数组,call 的参数是非数组
  • bind 改变函数的 this,但函数未执行
var init = 0;

function add(num1, num2) {
  console.log(this.init + num1 + num2);
}

// 普通执行,肯定输出3
add(1, 2);

var obj = { init: 100 };

// 此时因为this变成obj,所以输出103
add.apply(obj, [1, 2]);
// 此时因为this变成obj,所以输出103
add.call(obj, 1, 2);
// 此时因为this变成obj,但需要调用一次函数才行,bind本身返回的是函数
add.bind(obj, 1)(2);

手写实现 call/apply/bind

其实细看下 call,发现 call 有以下特征:

  • call 是函数的方法,可以被函数直接调用
  • call 第一个参数是 this 绑定的对象,后面的参数是函数的参数
  • call 调用之后,函数执行,但 this 绑定到第一个参数上

推理下 call 怎么实现:

  • 是函数的方法,每个函数都可调用,可写在 Function.prototype 上
  • 参数区别第一个参数,和后面的参数
  • call 内,this 绑定到第一个参数上,函数执行

其实想想普通函数 this 的指向只和调用方有关
=> 于是 obj.fn()
=> 但是 fn 并不是 obj 的方法
=> 于是给 obj 增加 fn 这个方法不就行了
=> 但是 call 执行完,必须再将 fn 这个方法删掉就好了

Function.prototype.myCall = function(context, ...args) {
  if (context === null) {
    return fn(...args);
  }
  // 注意 这里的this就是调用call的函数
  const fn = this;
  // 先增加
  context.fn = fn;
  // 执行
  return fn(...args);
  // 在删除
  delete context.fn;
};

// 测试下,没问题,输出103
add.myCall(obj, 1, 2);

同理:

// 注意因为参数args就是数组,所以不要加...
Function.prototype.myApply = function(context, args) {
  if (context === null) {
    return fn(...args);
  }
  // 注意 这里的this就是调用myApply的函数
  const fn = this;
  context.fn = fn;
  return fn(...args);
  delete context.fn;
};
// 同理输出103
add.myApply(obj, [1, 2]);

Bind 稍微复杂点:

  • bind 是函数的方法,可以被函数直接调用
  • bind 返回一个原函数的拷贝,但 this 被指定
  • bind 也可以传原函数的部分参数

其实 bind 就是返回一个函数,函数内部执行 call~

Function.prototype.myBind = function(context, ...frontArgs) {
  const fn = this;
  return function(...behindArgs) {
    return fn.call(context, ...frontArgs, ...behindArgs);
  };
};
// this被指定,但是需要调用一次才能执行函数,输出103
add.myBind(obj, 1)(2);