JavaScript this指向

Date:

this 是 JavaScript 中的一个关键字,它指向当前执行上下文中的对象,此外还有bind、call、apply方法可以改变this指向,本文将深入探讨 this 的指向规律、bind / call / apply 的原理与手写实现。


目录

一、this 的几种常见指向总结

使用方式this 指向备注
普通函数调用全局对象(非严格模式是 window,严格模式是 undefined默认规则
方法调用(obj.fn())obj,即调用该方法的对象常见对象调用
构造函数调用(new Fn())新创建的实例对象构造函数语义
定时器(setTimeout、setInterval)全局对象(非严格模式是 window,严格模式是 undefined默认规则
显式调用(call/apply)第一个参数指定的对象显式绑定
bind 返回的函数调用永远绑定为指定的对象固定绑定
DOM 事件中的普通函数触发事件的 DOM 元素浏览器中特例
箭头函数(非普通函数)外层作用域的 this(词法作用域)没有自己的 this,会捕获定义时的外层 this

二、 有关this指向的知识点

1. 嵌套对象中的this、严格模式与非严格模式的区别

    const obj = {
      name: 'outer',
      inner: {
        name: 'inner',
        getName() {
          return this.name;
        }
      }
    };

    console.log(obj.inner.getName()); // inner
    const fn = obj.inner.getName; // 函数赋值后调用,脱离了对象,this指向全局
    console.log(fn()); // 在严格模式下,this为undefined,会报错;非严格模式下,this为window

2. 实战:找找this指向

(1) 定时器this指向

  function Normal() {
    this.name = 'Normal';
    setTimeout(function () {
      console.log(this.name); 
    }, 100);
  }

  function Arrow() {
    this.name = 'Arrow';
    setTimeout(() => {
      console.log(this.name); 
    }, 100);
  }

  new Normal();
  new Arrow();

答:

  • new Normal()中,在定时器中传入了一个普通函数,在到时间,执行函数时,相当于把函数放到全局作用域中执行,所以this指向全局, 在非严格模式下,打印undefined(即window.name);
  • new Arrow()中,传入的是箭头函数,箭头函数会捕获定义时的外一层作用域的 this,所以this指向Normal的this,所以打印Arrow;

(2) 构造函数中的this指向

  globalThis.a = 100;
  // 和 var a = 100; 效果一样, 变量提升挂到全局对象上

  function fn() {
    return {
      a: 200,
      m: function () {
        console.log(this.a);
      },
      n: () => {
        console.log(this.a);
      },
      k: function () {
        return function () {
          console.log(this.a);
        };
      }
    };
  }

  const fn0 = fn();
  fn0.m();
  fn0.n();
  fn0.k()();

  const context = {a: 300};
  const fn1 = fn.call(context);
  fn1.m();
  fn1.n();
  fn1.k()();

答:

  • fn0.m()是典型的对象方法调用,this指向fn0对象,所以打印200;
  • fn0.n()是箭头函数,this绑定到了外层this,即fn函数的this, 即globalThis.a,所以打印100;
  • fn0.k()返回了一个普通函数再执行之,相当于在全局调用普通函数,所以打印100;

  • fn1在创建时,通过call方法,显式指定this指向context对象
  • fn1.m()是对象方法调用,this指向{a,m,n,k}这个对象,所以打印200;
  • fn1.n()是箭头函数,this绑定到了外层this,即fn函数的this,即context对象{ a: 300 },所以打印300;
  • fn1.k()()同理,this指向全局,打印100;

补充:

  globalThis.a = 100
  function fn() {
      this.b = 'abc'
      return {
          a: 200,
          f: () => {
              console.log(this, ' hhha ', this.a)
          }
      }
  }
  obj0 = fn()
  obj0.f() // 全局this , 100
  console.log(obj0) // { a: 200, f: [Function] }

  const context = { a: 300 }
  obj = fn.call(context)
  obj.f() // {a: 300, b:'abc'}, 300
  console.log(obj) // { a: 200, f: [Function] }

(3) 对象方法中的this指向

给箭头函数使用 .call() / .apply() 是不合理的,因为箭头函数的 this 是词法作用域绑定的,无法通过 .call() 强制改变它的 this 指向, 对箭头函数使用 .bind() 同样没有效果。


  let name = '!window!'
  globalThis.a = 'asd'
  const person1 = {
      name: 'person1',
      foo1: function () {
          console.log(this.name);
      },
      foo2: () => {
          console.log(this, this.name);
      },
      foo3: function () {
          return function () {
              console.log(this.name);
          };
      }
  };

  const person2 = {
      name: 'person2'
  };
  person1.foo1() // 典型对象调用, person1
  person1.foo1.call(person2) //典型对象调用, person2
  person1.foo2() // 全局this !window!
  person1.foo2.call(person2) // 全局this !window! 箭头函数call你这不搞笑么
  person1.foo3()()//全局this !window!
  person1.foo3.call(person2)() //全局this !window!


(4) 给对象赋值一个方法

  let length = 10; // let不会进行变量提升,所以window.length不会被影响
  // var length = 10; // var会进行变量提升,所以window.length会被影响

  function fn() {
      return this.length + 1;
  }

  const obj = {
      length: 5,
      test1: function () {
          return fn();
      }
  };

  obj.test2 = fn // 这里给对象赋值了一个方法,这个方法的this指向obj对象(这其实就是call的原理)

  console.log(obj.test1())
  // return fn()也是先执行fn()才返回,fn()在全局执行
  // 打印 window.length + 1 应该是 0 + 1 即页面的iframe窗口数 + 1


  console.log(obj.test2()) // 5 + 1 吧?这里是对象调用了
  console.log(fn() === obj.test2()) // false 

三、call、apply、bind的原理与手写实现

call、apply、bind是三个Function.prototype上的方法,所以所有函数都可以调用这三个方法。


关于args的小知识补充:

  • args 是“收集多个参数”的方式,它把多个参数打包成一个数组(用来“传入”),而 …args 是“展开数组”的方式(用来“传出”)
  • 剩余参数语法:function (...args) {} 是剩余参数语法,它把多个参数打包成一个数组(用来“传入”),在函数体内,args 是一个数组,可以被解构

1.call

func.call(thisArg, arg1, arg2, …) call方法用于调用一个函数, 显式指定this指向和传参并立即执行该函数

  // 实现思路:
  // 在context上,把方法挂载上去,然后调用,调用完删除,就这么简单
  // 需要挂载的属性名不能重复,所以用Symbol
  Function.prototype.myCall = function(context, ...args){
    context = context || window;
    const uniqueID = Symbol();
    context[uniqueID] = this;
    
    const result = context[uniqueID](...args);
    delete context[uniqueID];
    return result;
  }

  globalThis.name = 'global';

  function sayHi(message){
    console.log(message, 'I am ', this.name);
  }

  sayHi('Good morning'); // Good morning I am global
  sayHi.myCall({name: 'John'}, 'hello'); // hello I am John
  sayHi.myCall(null, 'hello'); // hello I am global

2.apply

func.apply(thisArg, [argsArray]) apply方法用于调用一个函数, 显式指定this指向和传参并立即执行该函数 和call的区别是传参是数组

  Function.prototype.myApply = function(context, args){
    context = context || window;
    const uniqueID = Symbol();
    context[uniqueID] = this;

    const result = context[uniqueID](...args);
    delete context[uniqueID];
    return result;
  }
  

3.bind

newFunc = func.bind(thisArg, arg1, arg2, …) bind方法用于创建一个新函数,这个新函数的this指向bind的第一个参数,并且返回这个新函数,参数通过列表传入 call、apply是立即执行,而bind是返回一个新函数


  Function.prototype.myBind = function(context, ...args){
    const self = this;
    return function(...newArgs){
      return self.apply(context, args.concat(newArgs));
    }
  }

  function sayHi(message){
    console.log(message, 'I am ', this.name);
  }

  const person = {name: 'John'};
  const JohnSayHi = sayHi.myBind(person, 'hello');
  JohnSayHi(); // hello I am John