JS中this指向的全面解析

423 阅读6分钟

判断this

可以按照以下顺序来进行判断:

  • 函数是否在new中调用?如果是的话this绑定的是新创建的对象。
  • 函数是否通过call()、appl()或bind()绑定?如果是的话this绑定的是指定的对象。
  • 函数是否通过在某个上下文对象中调用?如果是的话this绑定的是那个上下文对象。
  • 以上规则都不满足,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
  • 自执行函数的this都指向window

调用位置

在理解this绑定过程之前,首先理解调用位置调用位置是函数在代码中被调用的位置(而不是声明的位置)。每个函数的this是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。

function baz(){
  // 当前的调用栈: baz
  console.log("baz");
  bar();   // bar的调用位置
}
function bar(){
  // 当前的调用栈: baz -- bar
  console.log("bar");
  foo();  // foo的调用位置
}
function foo(){
  // 当前的调用栈: baz -- bar -- foo
  console.log("foo");
}
baz();   // baz的调用位置

绑定规则

1.new关键字绑定

JavaScript中,包括内置对象函数(比如Number(...)Array(...))在内的所有函数都可以用new操作符来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的"构造函数",只有对函数的"构造调用"。

使用new来调用函数,或者发生构造函数调用时,会自动执行下面的操作:

  • 1.在内存中创建一个新对象。
  • 2.这个新对象会被执行[[原型]]连接,即新对象内部的__proto__特性会被赋值为"构造函数"的prototype属性。
  • 3."构造函数"内部的this被赋值为这个对象(即this指向新对象)
  • 4.如果"构造函数"返回非空对象,则返回该对象;否则,返回刚创建的新对象。
function createNew(base,...args){
  var obj = {};
  obj.__proto__ = base.prototype;
  let res = base.apply(obj, args);
  return res instanceof Object ? res : obj;
}
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function () {
  console.log(this.name)
}
const p = createNew(Person,'jackson');
console.log(p.name) // jackson
p.sayName();      // jackson

以上是在通过new调用函数时发生的操作。通过new关键字调用函数时进行了this的绑定行为,思考下面的代码:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function(){
    console.log(this.name);
  }
}

let person1 = new Person("bob",20,"Student");
let person2 = new Person("Jack", 36, "Doctor");

person1.sayName();         //bob
console.log(person1.age);  //20
person2.sayName();         //Jack
console.log(person2.job);  //Doctor

new是一种可以影响函数调用时this绑定行为的方法。使用new来调用Person(...)时,我们会创建一个新对象并把它绑定到Person(...)调用中的this上。

2.call()、apply()或bind()绑定调用

call()、apply()

var a = 11;
function foo(){
  console.log(this.a);
}
const obj = {
  a: 28
}
foo();         //11,this指向window
foo.call(obj); //28,this指向obj

通过foo.call(...),我们可以在调用foo时强制把它的this绑定到obj上。call()apply()它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用这个函数时指定这个this

fun.call(obj, arg1, arg2, arg3,...);  // call的第二个参数是数组里的元素,逐一列举
fun.apply(obj, [arg1, arg2, arg3,...]); // apply的第二个参数是一个参数数组

bind()

bind会返回一个新函数,它会把参数设置为this的上下文并调用原始函数。

function foo(num) {
  console.log(this.a, num);
  return this.a + num;
}
const obj = {
  a: 11
};
const bar = foo.bind(obj);
const b = bar(28);    // 11 28
console.log(b);       // 39

3.隐式绑定

需要考虑的是函数是否在某个上下文对象中调用,如果是的话,this绑定的是那个上下文对象。

function foo() {
  console.log(this.a);
}
const obj1 = {
  a: 11,
  foo: foo
}
const obj2 = {
  a :28,
  foo: foo
}
obj1.foo();   //11  调用foo()时this被绑定到obj1对象上
obj2.foo();   //28  调用foo()时this被绑定到obj2对象上

上面的代码中,foo函数作为引用属性被添加到obj对象中,当foo被调用时,它的落脚点就是obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

4.默认绑定

最常见的函数调用类型是独立函数调用,可以把这条规则看作是无法应用其他规则时的默认规则。通过分析调用位置来看foo是如何调用的。在以下代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能是默认绑定,无法应用其他规则。

window.a = 11;
function foo(){
  console.log(this.a);
}
foo();  // 11 调用foo时,this.a被解析成全局变量,this的默认绑定指向全局对象
window.value = 1;
function foo() {
  console.log(value);  // 1
}
function bar() {
  let value = 2;
  foo();
  // console.log(value); // 2
}
bar();

注:如果使用严格模式("use strict"),那么全局对象将无法使用默认绑定,会报错。

绑定例外

  • 如果你把null或者undefined作为this的绑定对象传入call()apply()bind(),这些值在调用时会被忽略,实际应用的是默认绑定规则。
  • 自执行函数
    var a = 1;
    (function() {
      console.log(a+this.a);    //  NaN ---- undefined转成数字是NaN---隐式转换
      // var a; 变量提升,所以a是undefined;自调用函数的this指向 window, 所以 this.a=1;
      var a = '2';
      console.log(a+this.a);  //  21 ---- string
      // a = '2';自调用函数的this指向 window,this.a = 1,隐式转换为字符串
    })()  
    
  • 箭头函数不使用以上规则,而是根据外层(函数、全局或块)作用域来决定this,且绑定后无法修改。箭头函数没有自己的this指针,调用时并不会生成自身作用域下的this,它只会从自己的作用域链的上一层继承this
    window.name = 11;
    var obj = {
        name: 28,
        say: function(){
            const foo = () => {
                return this.name;
            }
            console.log(foo()); // 28 // 直接调用者为window 但是由于箭头函数不绑定this所以取得上下文中的this即obj对象
            const bar = function(){
                return this.name;
            }
            console.log(bar()); // 11// 直接调用者为window 普通函数
            return this.name;
        }
    }
    console.log(obj.say()); // 28 // 直接调用者为obj 执行过程中的函数内上下文的this为obj对象
    
     var a = 1
      function foo () {
        var a = 2
        function inner () { 
          console.log(this.a)   // 1
        }
        inner()  
      }
     foo()  // 在全局作用域中调用,this指向window
    

普通函数和箭头函数的区别

  • 箭头函数定义更简洁,箭头函数没有prototype,所以箭头函数本身没有this
  • 箭头函数的this指向在定义的时候继承外层第一个普通函数的this,所以,箭头函数中this的指向在它被定义的时候就已经确定了,并且不会改变。
  • call、apply,bind无法改变箭头函数中this的指向。
  • 箭头函数不能作为构造器使用,用new调用时会报错。
  • 箭头函数不绑定arguments,取而代之用rest参数...代替arguments,来访问箭头函数的参数列表。
  • 箭头函数不能用作Generator函数,不能使用yield关键字。
const a = 10;
const obj = {
    a: 13,
    b: () => {
        console.log(this.a);
    },
    c: function () {
        console.log(this.a)
    },
    d: function () {
        return () => {
            console.log(this);
        }
    },
    e: function () {
        return this.b
    }
}
obj.b()     // undefined 
obj.c()     // 13
obj.e()()    //undefined
obj.d()()   // 箭头函数没有this,去外层找,此处指向obj对象
            /* {
                  a: 13,
                  b: [Function: b],
                  c: [Function: c],
                  d: [Function: d],
                  e: [Function: e]
                }
            */