四. this

54 阅读11分钟

四. this

4.1. 什么是this

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值

  • 在浏览器中, 全局作用域下,我们可以认为this就是指向的window
  • 在node中, 全局作用域下,我们可以认为this就是指向的{}

但是,开发中很少直接在全局作用域下去使用this,通常都是在函数中使用。 所有的函数在被调用时,都会创建一个函数执行上下文,它有三部分内容

  • VO:GO
  • Scope Chain: VO / Parent VO
  • thisValue: 根据不同情况下绑定this

注: this 到底引用哪个对象必须到函数被调用时才能确定, this是在运行时被绑定的

4.2. 默认绑定

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

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

案例一:普通函数调用

 function foo() {
   console.log(this); // window
 }
 ​
 foo();

案例二:函数调用链(一个函数又调用另外一个函数)

  • 所有的函数调用都没有被绑定到某个对象上;
 // 2.案例二:
 function test1() {
   console.log(this); // window
   test2();
 }
 ​
 function test2() {
   console.log(this); // window
   test3()
 }
 ​
 function test3() {
   console.log(this); // window
 }
 test1();

案例三:将函数作为参数,传入到另一个函数中

 function foo(func) {
   func()
 }
 ​
 function bar() {
   console.log(this); // window
 }
 ​
 foo(bar);
 ------------------------------------------------
 function foo(func) {
     func()
 }
 var obj = {
     name : "why",
     bar: function () {
         console.log(this);   // window
     }
 }
 foo(obj.bar)

我们对案例进行一些修改,考虑一下打印结果是否会发生变化:

  • 这里的结果依然是window,为什么呢?

  • 原因非常简单,在真正函数调用的位置,并没有进行任何的对象绑定,只是一个独立函数的调用;

     function foo(func) {
       func()
     }
     ​
     var obj = {
       name: "why",
       bar: function() {
         console.log(this); // window
       }
     }
     ​
     foo(obj.bar);
    

4.3. 隐式绑定

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

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

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

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

案例一:通过对象调用函数

  • foo的调用位置是obj.foo()方式进行调用的
  • 那么foo调用时this会隐式的被绑定到obj对象上
 function foo() {
   console.log(this); // obj对象
 }
 ​
 var obj = {
   name: "why",
   foo: foo
 }
 ​
 obj.foo();

案例二:案例一的变化

  • 我们通过obj2又引用了obj1对象,再通过obj1对象调用foo函数;
  • 那么foo调用的位置上其实还是obj1被绑定了this;
 function foo() {
   console.log(this); // obj1对象
 }
 ​
 var obj1 = {
   name: "obj1",
   foo: foo
 }
 ​
 var obj2 = {
   name: "obj2",
   obj1: obj1
 }
 ​
 obj2.obj1.foo();

4.4. 隐式丢失

  • 因为foo最终被调用的位置是bar,而bar在进行调用时没有绑定任何的对象,也就没有形成隐式绑定;

  • 相当于是一种默认绑定;

     function foo() {
       console.log(this);
     }
     ​
     var obj1 = {
       name: "obj1",
       foo: foo
     }
     ​
     // 讲obj1的foo赋值给bar
     var bar = obj1.foo;
     bar();
    
  • 无论是对函数本身进行 赋值 逻辑运算 ,运算 都是会丢失this的指向

     var value = 1;
     var foo = {
         value: 2,
         bar: function () {
             return this.value;
         }
     }
     ​
     console.log(foo.bar()); // 2
     ​
     console.log((foo.bar)()); // 2 注:()()立即执行函数,相当于去foo对象,找到bar方法然后调用
     console.log((function () {
             return this.value;
         })()); // 1 注:这里就是独立函数调用了,等同默认绑定
     console.log((foo.bar = foo.bar)()); // 1 注:bar函数是独立调用,那么是默认绑定;
     console.log((false || foo.bar)()); // 1
     console.log((foo.bar, foo.bar)()); // 1
     ​
     // 其他方面一个细节
     var name="aaa"
             var obj1={
                 name:"obj1",
                 foo:function(){
                     console.log(this);
                 }
             }
             var obj2={
                 name:"obj2"
             };  // 这里没有; 会报错,原因是解析时会连在一起解析,obj2里没有bar属性的。有了;表示这行代码已经写完了
             (obj2.bar=obj1.foo)()
    
  • fn是独立执行,等同默认绑定,所以eating函数的this是window,why没有打印出来;this绑定场景和最上面例子是一样的

     var obj = {
         name: "why",
         eating: function () {
             console.log(this.name + "在吃东西");  // 在吃东西 
         }
     }
     var fn = obj.eating
     fn()
    

4.5. 显示绑定

其中call与apply比较特殊,它们在修改this的同时还会直接执行方法, 而bind只是返回一个修改完this的boundFunction并未执行, apply和call都能继承另外一个对象的方法和属性;call和apply的意思一样,只不过是参数列表不一样.

image-20220215143723801.png

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

  • JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)。

    • 它们两个的区别这里不再展开;
    • 其实非常简单,第一个参数是相同的,后面的参数,apply为数组,call为参数列表;
  • 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的。

  • 在调用这个函数时,会将this绑定到这个传入的对象上。

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

案例1: 通过call或者apply绑定this对象

  • 显示绑定后,this就会明确的指向绑定的对象
 function foo() {
   console.log(this);
 }
 ​
 foo.call(window); // window
 foo.call({name: "why"}); // {name: "why"}
 foo.call(123); // Number对象,存放时123

案例2: bind函数

  • bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
 function foo() {
   console.log(this);
 }
 ​
 var obj = {
   name: "why"
 }
 ​
 var bar = foo.bind(obj);
 ​
 bar(); // obj对象

4.6. new绑定

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

    • 在内存中创建一个新对象。
    • 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。222 第 8 章 对象、类与面向对象编程
    • 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
    • 执行构造函数内部的代码(给新对象添加属性)。
    • 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
     // 创建Person
     function Person(name) {
       console.log(this); // Person {}
       this.name = name; // Person {name: "why"}
     }
     ​
     var p = new Person("why");
     console.log(p);
    

4.7. 内置函数

传入到内置函数的回调函数this如何确定呢?

  • 某些内置的函数,我们很难确定它内部是如何调用传入的回调函数;
  • 一方面可以通过分析源码来确定,另一方面我们可以通过经验(见多识广)来确定;
  • 但是无论如何,通常都是我们之前讲过的规则来确定的;

案例一:setTimeout

  • setTimeout中会传入一个函数,这个函数中的this通常是window

  • 为什么这里是window呢?

    • 这个和setTimeout源码的内部调用有关;
    • setTimeout内部是通过apply进行绑定的this对象,并且绑定的是全局对象;
     setTimeout(function() {
       console.log(this); // window
     }, 1000);
    

案例二:数组的forEach

数组有一个高阶函数forEach,用于函数的遍历:

  • 在forEach中传入的函数打印的也是Window对象;
  • 这是因为默认情况下传入的函数是自动调用函数(默认绑定);
 var names = ["abc", "cba", "nba"];
 names.forEach(function(item) {
   console.log(this); // 三次window
 });
  • 第二个参数:用于重写回调内部 this 的值
 var names = ["abc", "cba", "nba"];
 var obj = {name: "why"};
 names.forEach(function(item) {
   console.log(this); // 三次obj对象
 }, obj);

案例三:div的点击

  • 在点击事件的回调中,this指向谁呢?box对象;

  • 这是因为在发生点击时,执行传入的回调函数被调用时,会将box对象绑定到该函数中;

     var box = document.querySelector(".box");
     box.onclick = function() {
      console.log(this); // box对象
     }
    

4.8. 箭头函数

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

  • 这里我使用setTimeout来模拟网络请求,请求到数据后如何可以存放到data中呢?

  • 我们需要拿到obj对象,设置data;

  • 但是直接拿到的this是window,我们需要在外层定义:var _this = this

  • 在setTimeout的回调函数中使用_this就代表了obj对象

     var obj = {
       data: [],
       getData: function() {
         var _this = this;
         setTimeout(function() {
           // 模拟获取到的数据
           var res = ["abc", "cba", "nba"];
           _this.data.push(...res);
         }, 1000);
       }
     }
     ​
     obj.getData();
    

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

  • 为什么在setTimeout的回调函数中可以直接使用this呢?

  • 因为箭头函数并不绑定this对象,那么this引用就会从上层作用域中找到对应的this

     var obj = {
       data: [],
       getData: function() {
         setTimeout(() => {
           // 模拟获取到的数据
           var res = ["abc", "cba", "nba"];
           this.data.push(...res);
         }, 1000);
       }
     }
     ​
     obj.getData();
    

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

  • 答案是window;

  • 依然是不断的从上层作用域找,那么找到了全局作用域;

  • 在全局作用域内,this代表的就是window

     var obj = {
       data: [],
       getData: () => {
         setTimeout(() => {
           console.log(this); // window
         }, 1000);
       }
     }
     ​
     obj.getData();
    

4.9. 优先级

  • new绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定
  • new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高

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

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

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

显示绑定和隐式绑定哪一个优先级更高呢?这个我们可以测试一下:

  • 结果是obj2,说明是显示绑定生效了
 function foo() {
   console.log(this);
 }
 ​
 var obj1 = {
   name: "obj1",
   foo: foo
 }
 ​
 var obj2 = {
   name: "obj2",
   foo: foo
 }
 ​
 // 隐式绑定
 obj1.foo(); // obj1
 obj2.foo(); // obj2
 ​
 // 隐式绑定和显示绑定同时存在
 obj1.foo.call(obj2); // obj2, 说明显式绑定优先级更高

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

  • 结果是foo,说明是new绑定生效了
 function foo() {
   console.log(this);
 }
 ​
 var obj = {
   name: "why",
   foo: foo
 }
 ​
 new obj.foo(); // foo对象, 说明new绑定优先级更高

4.new绑定优先级高于bind

  • new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
 function foo() {
   console.log(this);
 }
 ​
 var obj = {
   name: "obj"
 }
 ​
 var foo = new foo.call(obj);

4.10. this规则之外

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

     function foo() {
       console.log(this);
     }
     ​
     var obj = {
       name: "why"
     }
     ​
     foo.call(obj); // obj对象
     foo.call(null); // window
     foo.call(undefined); // window
     ​
     var bar = foo.bind(null);
     bar(); // window
    

4.11. 面试题

 var name = "window";
 var person = {
     name: "person",
     sayName: function () {
         console.log(this.name);
     }
 };
 ​
 function sayName() {
     var sss = person.sayName;
     sss(); // window
     person.sayName(); // person
     (person.sayName)(); // person
     (b = person.sayName)(); //  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 () {
         return () => {
             console.log(this.name)
         }
     }
 }
 ​
 var person2 = {
     name: 'person2'
 }
 person1.foo1();
 person1.foo1.call(person2);
 ​
 person1.foo2(); // 箭头函数不适用四种绑定,person1对象是不绑定this的,所以往上层作用域window找
 person1.foo2.call(person2); // 依然是箭头函数,不适用显示和隐式绑定
 ​
 person1.foo3()();
 person1.foo3.call(person2)(); // foo3显示绑定到person2中,但是拿到的返回函数依然是在全局下调用
 person1.foo3().call(person2);
 ​
 person1.foo4()(); // 独立函数调用
 person1.foo4.call(person2)();
 //person1显示的绑定到foo4中,然后给返回结果(箭头函数)显示绑定person2,
 //箭头函数没有this,往上层找
 person1.foo4().call(person2);
 var name = 'window'
 ​
 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.foo1.call(person2)
 ​
 // foo2函数不绑定this。往上层作用域找,不是直接找到全局,
 // 因为foo2的上层有个函数,函数是有作用域的,所以找到的是Person函数,Person的this是创建的这个对象
 person1.foo2()
 person1.foo2.call(person2)// call绑定不来this,往上层作用域找,找到的是Person函数,
 ​
 person1.foo3()()
 person1.foo3.call(person2)()
 person1.foo3().call(person2)
 ​
 // foo4返回箭头函数,找到上层person1
 person1.foo4()() 
 // foo4调用时绑定了person2,返回的函数是箭头函数,调用时,找到了上层绑定的person2
 person1.foo4.call(person2)() 
 person1.foo4().call(person2) 
 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()()
 person1.obj.foo1.call(person2)()
 person1.obj.foo1().call(person2)
 ​
 person1.obj.foo2()()
 person1.obj.foo2.call(person2)()
 person1.obj.foo2().call(person2) // call不会绑定箭头函数的this,往上层找,foo2是函数,有作用域,

4.12. 超出普遍this指向定律

 // bar是普通函数,创建上下文时候有绑定this,这里是独立函数执行,等同默认绑定
 function foo() {
     function bar(){
         console.log(this); // Window 
     }
     bar()
 }
 ​
 new foo();
 // 个人理解:构造函数不太一样,bar往上找时,找的是foo对象
 // 疑惑:上层作用域为什么是foo对象,这里没有产生闭包,foo的AO对象应该是销毁的
 function foo() { 
     let bar= ()=>{
         console.log(this); // foo {}
     }
     bar() 
 }
 ​
 new foo();
 // bar是箭头函数, 会向上级作用域找this,上层是全局,(上层不是foo的作用域,在断点处打印names会报错说没定义)
 function foo() { 
     // const names='why'
     let bar= ()=>{
         console.log(this);  // Window 
     // debugger
     }
     bar() 
 }
 ​
 foo();
 // bar是普通函数,创建上下文时候有绑定this,这里是独立函数执行,等同默认绑定
 function foo() {
     function bar(){
         console.log(this); // Window 
     } 
     bar() 
 }
 ​
 foo();