搞清楚this的几种绑定规则,将this指向一网打尽

110 阅读5分钟

JavaScript函数的 this 的值,

通常在全局执行环境中(在任何函数体外部) 都指向全局对象(浏览器中是window),在函数中取决于调用方式

下面是this的几种绑定规则,搞清楚了在日常开发中方便我们写出简洁的代码、防止因为搞不清楚this写出bug

默认绑定

默认绑定全局对象window

//普通函数被独立调用
    function bar() {
      console.log("bar:", this)
    }
    bar()  //bar:Window
//函数定义在对象中,但是被独立调用
    var obj = {
      name: "CR7",
      bar: function() {
        console.log("bar:", this)
      }
    }
    var baz = obj.bar
    baz() //bar:Window
//高阶函数的调用
 var obj = {
      name: "CR7",
      bar: function() {
        console.log("bar:", this)
      }
    }
   function highFn(fn) {
      fn()
    }
   highFn(obj.bar) //bar:Window

隐式绑定

函数被调用时,它的前面确实加上了对某个对象的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象

      function foo() {           
        console.log(this.a);       
      }       
      var obj = {          
        a: 2,            
        foo: foo      
      };       
      obj.foo(); // 2
      
      //当foo()被调用时,它的前面确实加上了对obj的引用

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,例如:

      function foo() {    
        console.log(this.a);     
      }       
      var obj2 = {    
        a: 42,         
        foo: foo    
      };      
      var obj1 = {        
        a: 2,     
        obj2: obj2    
      };    
      obj1.obj2.foo(); // 42
      //obj2作为foo调用位置的最后一层,foo的this指向obj2

隐式丢失,看似绑定了对象,实则属于默认绑定规则,例如:

//函数定义在对象中,但是被独立调用
    var obj = {
      name: "CR7",
      bar: function() {
        console.log("bar:", this)
      }
    }
    var foo = obj.bar
    foo() //bar:Window
    
    //虽然foo是obj.bar的一个引用,但是实际上,它引用的是bar函数本身,
    //因此此时的goo()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
      function foo() {
        console.log(this.a);      
      }       
      function doFoo(fn) {  
        // fn其实引用的是foo      
        fn(); // <-- 调用位置!        
      }    
      var obj = {       
        a: 2,       
        foo: foo      
      };      
      var a = "oops, global"; // a是全局对象的属性 
      doFoo(obj.foo); // "oops, global"

显示绑定

通过函数原型上的方法 callapply 实现this的显示绑定,call 和 apply区别只是传参方式不一样

            const obj = {
                name: "CR7",
            };
            function foo() {
                console.log(this);
            }
            foo.call(obj); // {name: 'CR7'}
            foo.call(123); // Number {123}
            foo.call("abc"); // String {'abc'}
            foo.call(true); // Boolean {true}
            
           //如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,
           //这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))

通过 bind 方法固定this绑定,解决把对象中的函数属性取出来单独调用导致的绑定丢失问题

           function foo(name, age) {
                console.log(this);  // {name: 'why'}
                console.log("参数:", name, age);  //参数: CR7 Messi
            }
            const obj = { name: "why" };
            const bar = foo.bind(obj, "CR7");
            bar("Messi");
            
            // foo的this固定为obj对象,无论bar怎么调用
            // 同时foo的参数是由bind函数第二个参数及以后的参数加上bar的参数组成

JavaScript中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind一样,确保你的回调函数使用指定的this。 image.png

call/apply常用于框架、组件封装

// vue2.7.8 /src/core/instance/state.ts

function initData(vm: Component) {  //vm就是当前组件vue实例
      let data: any = vm.$options.data //拿到我们写的data
      data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
      //......
}

export function getData(data: Function, vm: Component): any {
  pushTarget()
  try {
    return data.call(vm, vm) //如果data是个函数,vue调用data,绑定当前实例作为this,并传入当前实例作为参数
  } catch (e: any) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
// vue2.7.8 /src/core/observe/watcher.ts

export default class Watcher implements DepTarget {
  // ......
  constructor(
    vm: Component | null, //Vue实例
    expOrFn: string | (() => any),
    cb: Function,  //我们平时在watch中写的回调函数
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean //是否为渲染函数的观察者
  ) {
      //......
  }
      // ......
  run() {
    this.cb.call(this.vm, value, oldValue)
    //vue会call调用我们传入的回调函数,绑定当前实例为它的this,并把新值和旧值传给回调函数
  }
}

new 绑定

包括内置对象函数(比如Number(),String())在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”, ES6 的 class 可以通过new 实例化一个类,但也只是普通函数new调用的语法糖

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

1.创建(或者说构造)一个全新的对象。

2.)这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性

3.这个新对象会绑定到函数调用的this

4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

            function foo(a) {
                this.a = a;
            }
            var bar = new foo(2); //foo中的this指向a
            console.log(bar.a); //2

            class Person {
                constructor() {
                    this.name = "CR7";
                    this.age = "37";
                }
            }
            const p = new Person(); // Person类中的this指向 p
            console.log(p.name, p.age); // CR7  37

箭头函数

箭头函数的this是根据外层(函数或者全局)作用域决定的,不适用上面4条规则

箭头函数 没有自己的thisargumentssupernew.target ,箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数,不能使用new调用

           const foo = () => {
                console.log("foo的this:", this);
            };
            foo();  // foo的this: Window
            foo.apply("CR7"); // foo的this: Window
            // foo 的上层作用域是全局作用域,this指向window
            const obj = {
                name: "CR7",
                foo: () => {
                    const bar = () => {
                        console.log("bar中的this:", this); // bar中的this:Window
                    };
                    return bar;
                },
            };
            const fn = obj.foo();
            fn.apply("bbb");
            // fn等于箭头函数 bar, 执行fn.apply("bbb"),箭头函数不适用显示绑定,往上层作用域找this,
            //bar上层作用域是foo函数作用域,foo是箭头函数,继续找到foo的上层作用域全局作用域,
            //obj对象不是作用域哈
 // 下面看一个vue实际开发例子
  export default {
    data() {
      return {
        name:"CR7"
    },
    methods:{
      doSomething(){
         console.log(this.name) //CR7
      },
      onSubmit() {
        this.$refs.ruleForm.validate(valid => { //这是一个form表单校验方法,来自ant-design-vue
           /**
             在这个回调函数中要拿到vue实例中数据,比如this.name、this.doSomething,就必须使用箭头函数,
             this才能绑定到上层onSubmit函数作用域的this,onSubmit的this是绑定到当前vue实例上的
           */
        });
     },
    }
  }

上面这个例子涉及到一个参数作用域的问题:如果函数参数没有默认值,就只有一个函数作用域,如果函数参数有默认值,函数的参数就会形成一个作用域,保存参数的值,上面例子中的回调函数和validate同一个函数作用域,如果不使用箭头函数,this就不能指向vue实例

优先级

通过以上四条规则,我们已经知道默认绑定的优先级最低,下面比较一下隐式绑定、显示绑定、new绑定

            function foo() {
                console.log(this.name);
            }
            const obj = { foo, name: "CR7" };
            obj.foo(); //CR7
            obj.foo.apply({ name: "Neymar" }); //Neymar
            obj.foo.call({ name: "Messi" }); //Messi
            
            //call 、apply显示绑定优先于隐式绑定
            const obj = {
                name: "CR7",
                foo: function () {
                    console.log(this); //foo {}
                    console.log(this === obj); //false
                },
            };
            new obj.foo();
            // foo 的this不是obj
           
             const obj = {
                foo: function (name) {
                    this.name = name;
                },
            };
            obj.foo("CR7");
            console.log(obj.name); //CR7  foo的this绑定为obj
            const f = new obj.foo("Messi");
            console.log(f.name); //Messi   //foo的this绑定为新对象f  
         
          //new绑定优先于隐式绑定
         // new和call/apply无法一起使用
             function foo() {
                console.log(this);
            }
            new foo.call({ name: "CR7" }) 
            //  Uncaught TypeError: foo.call is not a constructor
           //但是new 和 bind 可以比较
            function foo() {
                console.log(this);
            }
            const bar = foo.bind({ name: "CR7" });
            bar();  // {name: 'CR7'}
            new bar(); // foo {}  //this为new创造的新对象
            // new优先级高于bind
            function foo() {
                console.log(this); //String {'aaa'}
            }
            const bar = foo.bind("aaa");
            bar.call("bbb");
          // call调用没有改变bar函数的this  

所以一般情况下优先级new > bind > call/apply >隐式绑定 > 默认绑定

this练习

            var name = "CR7";
            function Person(name) {
                this.name = name;
                this.obj = {
                    name: "Messi",
                    foo1: function () {
                        return function bar1() {
                            console.log(this.name);
                        };
                    },
                    foo2: function () {
                        return () => {
                            console.log(this.name);
                        };
                    },
                };
            }

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

            person1.obj.foo1()(); //"CR7"
            // person1.obj.foo1()返回bar1独立调用,默认绑定
            person1.obj.foo1.call(person2)(); // "CR7"
            // person1.obj.foo1.call(person2)还是返回bar1独立调用,默认绑定
            person1.obj.foo1().call(person2); //person2
            // person2绑定为返回函数bar1的this

            person1.obj.foo2()(); //Messi
            //  person1.obj.foo2()返回箭头函数,它的this由上层foo2决定,foo2隐式绑定为obj
            person1.obj.foo2.call(person2)(); //person2
            // foo2的this绑定为person2再返回箭头函数,箭头函数的this也为上层foo2的this
            person1.obj.foo2().call(person2); //Messi
            //foo2返回箭头函数,call绑定对箭头函数不管用,还是看上层foo2, foo2隐式绑定为obj
            var name = "CR7";
            function Person(name) {
                this.name = name;
                this.foo1 = function () {
                    console.log(this.name);
                };
                this.foo2 = () => console.log(this.name);
                this.foo3 = function () {
                    return function bar() {
                        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
            person1.foo1.call(person2); //person2 显示绑定person2

            person1.foo2(); //  person1
            person1.foo2.call(person2); //  person1
            // foo2是箭头函数,隐式绑定和显示绑定都不管用,找上层作用域Person,Person中的this就是person1

            person1.foo3()(); // CR7
            person1.foo3.call(person2)(); // CR7
            //foo3返回bar函数到最外层独立调用,默认绑定全局
            person1.foo3().call(person2); //  person2  foo3返回bar函数显式绑定person2

            person1.foo4()(); //person1 
            person1.foo4.call(person2)(); //   person2
            person1.foo4().call(person2); // person1
            //foo4无论如何调用,返回一个箭头函数,箭头函数无论如何调用,它的this是上层foo4函数的this
            var name = "CR7";
            var person1 = {
                name: "person1",
                foo1: function () {
                    console.log(this.name);
                },
                foo2: () => console.log(this.name),
                foo3: function () {
                    return function bar() {
                        console.log(this.name);
                    };
                },
                foo4: function () {
                    return () => {
                        console.log(this.name);
                    };
                },
            };

            var person2 = { name: "person2" };

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

            person1.foo2(); //CR7
            person1.foo2.call(person2); //CR7
            //foo2是箭头函数,无论如何调用,this由上层作用域(这里是全局,不是person1对象)

            person1.foo3()(); // 默认绑定CR7
            person1.foo3.call(person2)(); // 默认绑定 CR7
            person1.foo3().call(person2); // 显式绑定 person2
            // foo3无论怎样调用,返回函数bar,关键看bar如何调用

            person1.foo4()(); // person1  foo4隐式绑定person1
            person1.foo4.call(person2)(); // person2  foo4显示绑定person2
            person1.foo4().call(person2); // person1  foo4隐式绑定person1
            //foo4返回一个箭头函数,无论这个箭头函数如何调用,关键看上层foo4的this

参考:

《你不知道的JavaScript(上卷)》第2章

developer.mozilla.org/zh-CN/docs/…