this到底指向什么呢?

我们先来看一个让人困惑的问题:

  • 定义一个函数,我们采用三种不同的方式对它进行调用,它产生了三种不同的结果

这个的案例可以给我们什么样的启示呢?

  • 1.函数在调用时,JavaScript会默认给this绑定一个值;
  • 2.this的绑定和定义的位置(编写的位置)没有关系;
  • 3.this的绑定和调用方式以及调用的位置有关系;
  • 4.this是在运行时被绑定的;

那么this到底是怎么样的绑定规则呢?

  • 绑定一:默认绑定;
  • 绑定二:隐式绑定;
  • 绑定三:显式绑定;
  • 绑定四:new绑定;
// 定义函数
function foo() {
  console.log("foo函数:", this);
}

// 1.方式一:直接调用
// foo() // window对象

// 2.方式二:通过对象调用
var obj = { name: "rin" };
obj.aaa = foo;
obj.aaa(); // obj对象

// 3.方式三:call/apply调用
foo.call('abc') //String {'abc'} 对象

规则一:默认绑定

什么情况下使用默认绑定呢?独立函数调用。

  • 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

我们通过几个案例来看一下,常见的默认绑定

// "use strict" // 严格模式下,独立调用的函数中this的指向是undefined

// 1.普通的函数被独立的调用
function foo() {
  console.log("foo:", this);
}

foo(); // window 对象


// 2.函数定义在对象中,但是独立调用
var obj = {
  name: "rin",
  bar: function () {
    console.log("bar:", this);
  },
};


var baz = obj.bar;
baz(); // window 对象

// 3.高阶函数
function test(fn){
  fn()
}

test(obj.bar) // 也指向window

规则二:隐式绑定

另外一种比较常见的调用方式是通过某个对象进行调用的:

  • 也就是它的调用位置中,是通过某个对象发起的函数调用。

我们通过几个案例来看一下,常见的默认绑定

// 隐式绑定
function foo() {
  console.log("foo函数:", this);
}

var obj = {
  name: "rin",
  bar: foo,
};

  
obj.bar() // 由obj发起调用, this指向obj

规则三:new绑定

JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。

使用new关键字来调用函数是,会执行如下的操作:

  1. 创建一个全新的对象;
  2. 这个新对象会被执行prototype连接;
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
  4. 如果函数没有返回其他对象,表达式会返回这个新对象;
// new 绑定
/*
  1.创建新的空对象
  2.将this 指向这个空对象
  3.执行函数体中的代码
  4.没有显式返回非空对象时,默认返回这个对象
*/

function foo() {
  this.name = 'rin'
  console.log("foo函数:", this);
}

new foo();

规则四:显式绑定

隐式绑定有一个前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性);
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  • 正是通过这个引用,间接的将this绑定到了这个对象上;

如果我们不希望在 对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

JavaScript所有的函数都可以使用call和apply方法。

  • 第一个参数是相同的,要求传入一个对象;
    • 这个对象的作用是什么呢?就是给this准备的。
    • 在调用这个函数时,会将this绑定到这个传入的对象上。
  • 后面的参数,apply为数组,call为参数列表;

// 显式绑定
var obj = {
  name: "rin",
};

function foo() {
  console.log("foo函数:", this);
}

// 指向函数,并且函数中的 this 指向 obj 对象
/*
obj.foo = foo
obj.foo()
*/

// 执行函数,并且强制 this 就是 obj 对象
foo.call(obj); // 强制执行 obj
foo.call(123); // 包装类对象 Number {123}
foo.call("abc"); // 包装类对象 String {'abc'}

因为上面的过程,我们明确的绑定了this指向的对象,所以称之为 显式绑定。

call、apply、bind

通过call或者apply绑定this对象

  • 显式绑定后,this就会明确的指向绑定的对象
    如果我们希望一个函数总是显式的绑定到一个对象上,可以怎么做呢?
  • 使用bind方法,bind() 方法创建一个新的绑定函数(bound function,BF);
  • 绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语)
  • 在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

// call/apply
function foo(name, age, height) {
  console.log("foo函数被调用:", this);
  console.log("打印参数:", name, age, height);
}

// 直接调用
foo("Rin", 16, 1.66);

// apply
// 第一个参数: 绑定 this
// 第二个参数: 传入额外的实参,以数组的形式
foo.apply("apply", ["Min", 16, 1.86]);

// call
// 第一个参数: 绑定 this
// 第二个参数: 传入额外的实参,以参数列表的形式
foo.call("call", "Kin", 16, 1.67);
function foo(name, age, height,address) {
  console.log("foo:", this);
  console.log("参数:", name, age, height);
}

var obj = { name: "why" };

// 需求: 调用foo 时, 总是绑定 obj 对象上(但是不希望obj对象身上有函数)
// 1.bind函数的基本使用
var bar = foo.bind(obj);

bar(); // this -> obj

// 2.bind函数的其他参数
var bar = foo.bind(obj, "rin", 16, 1.66);
bar('东京');
bar();

内置函数的绑定思考

有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库中的内置函数。

  • 这些内置函数会要求我们传入另外一个函数;
  • 我们自己并不会显式的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
  • 这些函数中的this又是如何绑定的呢?

setTimeout、数组的forEach、div的点击

      // 内置函数(第三方库): 根据一些经验去判断this
      // 1.定时器
      setTimeout(function () {
        console.log("定时器函数:", this); // window
      }, 1000);

      // 2.按钮的点击监听
      var btnEl = document.querySelector("button");
      btnEl.onclick = function () {
        console.log("btn的点击:", this); // this就是当前点击的按钮
      };

      btnEl.addEventListener("click", function () {
        console.log("btn的点击:", this);
      });

      // 3.forEach
      var names = ["abc", "cba", "nba"];
      names.forEach(function (item) {
        console.log("forEach:", this);
      }, "aaa"); // 第二个参数可以手动传入this

规则优先级

学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?

1.默认规则的优先级最低

  • 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this

2.显式绑定优先级高于隐式绑定

	  function foo() {
        console.log("foo:", this);
      }

      // 比较优先级
      // 1.1显示绑定的优先级: apply绑定高于默认绑定
      var obj = { foo: foo };
      obj.foo.apply("abc"); // 显式绑定 String {'abc'} 高于隐式 obj.foo()
      obj.foo.call("abc");
      
      // 1.2 测试bind高于默认绑定
      var bar = foo.bind("aaa");
      var obj2 = {
        name: "rin",
        baz: bar,
      };
      obj2.baz(); // String {'aaa'}

3.new绑定优先级高于隐式绑定

      var obj = {
        name: "rin",
        foo: function () {
          console.log("foo:", this);
          console.log("foo:", this === obj);
        },
      };
      
      new obj.foo();// foo: foo {}  foo: false

4.new绑定优先级高于bind

  • new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
  • new绑定可以和bind一起使用,new绑定优先级更高
	 // 3.new/显示
     // 3.1 new 不可以和apply/call一起使用
     // 3.2 new 优先级高于bind
      function foo() {
        console.log("foo:", this);
      }

      var bindFn = foo.bind("aaa");

      new bindFn(); // foo: foo {}
      
      // 4.bind/apply
      // bind优先级高于apply/call
      function foo() {
        console.log("foo:", this);
      }
      
      var bindFn = foo.bind("aaa");
      foo.apply('bbb')

this规则之外 – 忽略显式绑定

我们讲到的规则已经足以应付平时的开发,但是总有一些语法,超出了我们的规则之外。(神话故事和动漫中总是有类似这样的人物)

情况一:如果在显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,使用默认规则:

严格模式

      "use strict";
      function foo() {
        console.log("foo:", this);
      }

      foo.apply("abc"); // foo: abc

      foo.apply(null); // foo: null
      foo.apply(undefined); // foo: undefined

this规则之外 - 间接函数引用

情况二:创建一个函数的 间接引用,这种情况使用默认绑定规则。

  • 赋值(obj2.foo = obj1.foo)的结果是foo函数;
  • foo函数被直接调用,那么是默认绑定;

箭头函数 arrow function

箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:

  • 箭头函数不会绑定this、arguments属性;
  • 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误);

箭头函数如何编写呢?

  • (): 函数的参数
  • {}: 函数的执行体

	 // 1.之前的方式
      function foo1() {}
      var foo2 = function (name, age) {
        console.log("函数体代码", this, arguments);
        console.log(name, age);
      };

      // 2.箭头函数完整写法
      var foo3 = (name, age) => {
        console.log("箭头函数体代码");
        console.log(name, age);
      };

      // 3.箭头函数的练习
      // 3.1 forEach
      var names = ["abc", "cba", "nba"];
      names.forEach((item, index, arr) => {
        console.log(item, index, arr);
      });

      // 3.2 setTimeout
      setTimeout(() => {
        console.log("setTimeout");
      }, 3000);

箭头函数的编写优化

优化一: 如果只有一个参数()可以省略

  • nums.forEach(item => {})

优化二: 如果函数执行体中只有一行代码, 那么可以省略大括号

  • 并且这行代码的返回值会作为整个函数的返回值
    优化三: 如果函数执行体只有返回一个对象, 那么需要给这个对象加上()
	 // var names = ["abc", "cba", "nba"];
     // var nums = [20, 30, 11, 15, 111];
     
      // 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
      /*       
      names.forEach((item) => {
        console.log(item);
      });

      var newNums = nums.filter((item) => {
        return item % 2 === 0;
      }); */

      // 2.优化二: 如果函数体中只有一行代码,那么大括号可以省略
      /*  names.forEach((item) => console.log(item));
      var newNums = nums.filter((item) => {
        return item % 2 === 0;
      }); */

      // 3.优化三: 只有一行代码时,这行代码的表达式结果就会作为函数的返回值默认返回
      /* var newNums = nums.filter((item) => {
        return item % 2 === 0;
      });
      var newNums = nums.filter((item) => item % 2 === 0);
	 */

      // 4.优化四: 如果默认返回值是一个对象,那么这个对象必须加()
      // var arrFn = () => ['abc','cba']
      // var arrFn = () => {name:'rin'}  这里是代码执行体
      /*       
      var arrFn = () => ({ name: "rin" }); //这样才表示返回一个对象
      console.log(arrFn());
       */
       
      // 箭头函数实现所有偶数平方的和
      var nums = [20, 30, 11, 15, 111];
      var result = nums
        .filter((item) => item % 2 === 0)
        .map((item) => item * item)
        .reduce((prevValue, item) => prevValue + item);
      console.log(result);

this规则之外 – ES6箭头函数

箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。

我们来看一个模拟网络请求的案例:

  • 这里我使用 setTimeout 来模拟网络请求,请求到数据后如何可以存放到data中呢?
  • 我们需要拿到 obj 对象,设置 data;
  • 但是直接拿到的 this 是 window,我们需要在外层定义:var _this = this
  • setTimeout 的回调函数中使用_this就代表了obj对象

      /* function foo() {
        console.log("foo:", this);
      }
      foo();
      foo.apply("aaa"); */

      // 2.箭头函数中,压根没有this
      var bar = () => {
        console.log("bar:", this);
      };
      bar();
      
      // 通过 apply 调用,也是没有 this 的
      bar.apply("aaaa"); // 根本没有this

      // console.log("全局this:", this);

      // 3.this的查找规则
      var obj = {
        name: "obj",
        foo: () => {
          var bar = () => {
            console.log("bar:", this);
          };
          return bar;
        },
      };
      
      var fn = obj.foo();
      fn.apply("bbb");
	// 网络请求的工具函数
      function request(url, callbackFn) {
        var results = ["abc", "cba", "nba"];
        callbackFn(results);
      }

      // 实际操作的位置(业务)
      var obj = {
        names: [],
        network: function () {
          // 1.早期的写法
          /*  var _this = this;
          request("/names", function (res) {
            _this.names = [].concat(res);
          }); */
          
          // 2.箭头函数
          request("/names",(res)=>{
            this.names = [].concat(res)
          })
        },
      };

      obj.network();
      console.log(obj);

ES6箭头函数this

之前的代码在ES6之前是我们最常用的方式,从ES6开始,我们会使用箭头函数:

  • 为什么在setTimeout的回调函数中可以直接使用this呢?
  • 因为箭头函数并不绑定this对象,那么this引用就会从上层作用于中找到对应的this

思考:如果getData也是一个箭头函数,那么setTimeout中的回调函数中的this指向谁呢?

面试题一

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  },
};

function sayName() {
  var sss = person.sayName;
  sss(); // 绑定: 默认绑定, window -> window

  person.sayName(); // 绑定: 隐式绑定, person -> person

  (person.sayName)(); // 绑定: 隐式绑定,person -> person

  (b = person.sayName)(); // 术语:间接函数引用 window -> window
}

sayName();

面试题二

var name = "window";

// {} => 对象
// {} => 代码块

var person1 = {
  name: "person1",
  foo1: function () {
    console.log(this.name);
  },

  foo2: () => console.log(this.name),

  foo3: function () {
    return function () {
      console.log(this.name);
    };
  },

  foo4: function () {
    // console.log(this) // 第一个表达式 this -> person1
    // console.log(this) // 第二个表达式 this -> person2
    // console.log(this); // 第三个表达式this -> person1
    return () => {
      console.log(this.name);
    };
  },
};

var person2 = { name: "person2" };

person1.foo1(); // 隐式绑定: person1
person1.foo1.call(person2); // 显示绑定: person2

person1.foo2(); // 上层作用域: window
person1.foo2.call(person2); // 上层作用域: window

person1.foo3()(); // 默认绑定: window
person1.foo3.call(person2)(); // 默认绑定: window
person1.foo3().call(person2); // 显式绑定: person2

person1.foo4()(); // person1
person1.foo4.call(person2)(); // person2
person1.foo4().call(person2);

面试题三

var name = "window";
/*
  1.创建空对象
  2.将这个空的对象赋值给this
  3.执行函数体中代码
  4.将这个新的对象默认返回
*/

function Person(name) {
  this.name = name;
  (this.foo1 = function () {
    console.log(this.name);
  }),
    (this.foo2 = () => console.log(this.name)),
    (this.foo3 = function () {
      return function () {
        console.log(this.name);
      };
    }),
    (this.foo4 = function () {
      return () => {
        console.log(this.name);
      };
    });
}

var person1 = new Person("person1");
var person2 = new Person("person2");

person1.foo1(); // 隐式绑定: person1
person1.foo1.call(person2); // 显示绑定: person2

person1.foo2(); // 上层作用域: person1
person1.foo2.call(person2); // call对箭头函数无意义,继续去上层作用域找: person1

person1.foo3()(); // 默认绑定: window
person1.foo3.call(person2)(); // 默认绑定: window
person1.foo3().call(person2); // 显示绑定: person2

person1.foo4()(); // 上层作用域: person1(隐式绑定)
person1.foo4.call(person2)(); // 上层作用域: person2(显式绑定)
person1.foo4().call(person2); // 上层作用域: person1(隐式绑定)

面试题四

var name = 'window'

function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },

    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()  // 默认绑定: window
person1.obj.foo1.call(person2)() // 默认绑定: window
person1.obj.foo1().call(person2)  // 显示绑定: person2

person1.obj.foo2()() // 上层作用域查找: obj(隐式绑定)
person1.obj.foo2.call(person2)() // 上层作用域查找: person2(显式绑定)
person1.obj.foo2().call(person2) // // 上层作用域查找: obj(隐式绑定)

Q.E.D.