一文读懂JavaScript中的this"指针"

1,280 阅读14分钟

前言:

JavaScript这门语言是一门解释性语言,并不是编译型语言。值得注意的是,它并不是直接一行一行解释,先检查语法错误,如果没有错误则一行一行去解释执行,两者之间会有一步'预编译'的操作。

当谈及this的时候,通常会听到GO和AO的声音,心里想:wht? GO和AO是什么?

  • AO: (Activation Object => 活跃对象) 存在于局部作用域,也叫全局执行期上下文
  • GO: (Global Object => 全局对象) 存在于全局作用域中,也叫函数执行器上下文

总的来说整个步骤就是:

  1. 检查通篇代码的语法错误y
  2. 预编译(函数在执行前所要做的准备)
  3. 解释一行,执行一行

AO的理解:

预编译阶段,函数需要做的事情,首先创建一个AO对象,AO={},具体执行步骤,如下:

  1. 寻找函数的形参和变量声明
  2. 把实参赋值给形参
  3. 寻找函数声明,赋值函数体
  4. 执行函数

例子:

    function fun(a, b) {
        console.log(a);
        c = 0;
        var c;
        a = 2;
        b = 3;
        console.log(b);
        function b() {}
        function d() {}
        console.log(b);
    }
    fun(1);
    // 执行结果是:1 3 3

Tip:值得注意的是:b的打印结果,并没有因为d的函数声明而改变b的值,因为 函数声明后才去执行函数,也就是先function b() {}再 b = 3。

GO的理解:

在产生函数作用域之前,会产生一个全局的作用域,首先创建一个GO对象,GO={},具体执行步骤,如下:

  1. 寻找变量声明
  2. 寻找函数声明,并赋值函数体
  3. 执行代码

例子:

    console.log(a,b);
    function a(){}
    var b = function(){}
    // 执行结果是:ƒ a(){}   undefined

Tips:这也就是我们通常说的:函数声明(不是函数表达式)是会进行函数提升,提升至所在函数的顶部。而函数表达式没有函数提升,函数表达式执行的是变量提升。

AO和GO混合使用:

  1. 寻找变量声明
  2. 寻找函数声明,并赋值函数体
  3. 执行代码,并生成AO对象(即执行AO第一步)
  4. 实参赋值
  5. 找函数声明并赋值函数体
  6. 执行代码

例子:

function fun() {
    console.log(a);
    console.log(b);
    if (a) {
      var b = 2; //若换成 let b = 2 会存在暂时性死区,所以b值打印会报错
    }
    c = 3;
    console.log(c);
}
var a;
fun();
a = 1;
// 执行结果是:undefined   undefined   3

Tips:

  • 可能你会觉得,第一次打印a的值为什么不是1?原因就是我函数内部有a这个变量,并且有值,不管是undefined还是具体的值,都是不会去全局进行查找的。
  • 可能你会觉得,AO那里为什么不报错:Uncaught ReferenceError: b is not defined。因为在预编译阶段,是不管if语句的规则的,只要内部有变量就需要拿出来;执行的时候,是看if规则的,比如if的()中为true才会执行里面的代码。

上面的只是让我们了解this的预备动作,接下来切入正题了!

this:是执行上下文对象的一个属性,在JS中是一种类似于指针一样的存在,严格意义上来讲js中没有指针的概念,而浏览器环境下的全局变量就是window(并非全局变量就是window,比如在node中的全局变量不是window),一般情况下,this指向的就是函数执行的 上下文对象,全局变量也就是window。值得一提的是,函数执行才存在this指向的问题。

文章借鉴出处,# 想搞懂预编译,看这篇就够了!包括GO和AO→

this相关的 5 个绑定规则:

1.默认绑定:

即发生了函数的独立调用,没有绑定到某个对象上进行调用

例子:

  console.log(this); // 默认绑定,指向window
  function fun() {
    console.log(this);
  }
  fun(); //函数的独立调用,指向window

  var obj1 = {
    name: "why",
    foo: function() {
      console.log(this); //谁调用就指向谁,指向obj1
      function test() {
        console.log(this);
      }
      test(); // 函数的独立调用,指向window

      (function() {
        console.log(this); // 函数的独立调用,指向window,立即执行函数的this指向始终指向window
      })();
    }
  };
  obj1.foo();

  var obj2 = {
    name: "why",
    foo: function() {
      function test() {
        console.log(this); //指向window
      }
      return test;
    }
  };
  obj2.foo()(); //因为这个是闭包函数,有个return所以可以看成, obj.foo()() => test()函数的独立调用,指向window

  var obj3 = {
    name: "why",
    foo: function() {
      console.log(this);
    }
  };
  var bar = obj3.foo(); //这里的this指向obj3,函数的预编译先变量声明=>再函数声明=>执行代码,且是隐式调用
  var bar = obj3.foo;
  bar(); //这里的this指向window,bar持有foo的引用,且是独立调用
  
  var car = function(){
      console.log(this);
  }
  car(); // 这个car可以看成,上面bar的执行过程

Tips:上面的例子除了倒数第八行:var bar = obj3.foo(),其余全部都是函数的独立调用,也就是默认绑定,this的指向的也全是window。

2.隐式绑定:

也称弱绑定,即谁调用就指向谁

例子:

var obj1 = {
    name: "why",
    foo: function() {
      console.log(this);
    }
};
obj1.foo() // 谁调用就指向谁,指向obj1

// 只嵌套一层
var obj2 = {
    foo(){
      console.log(this);
    }
}
var obj3 ={
    bar:obj2.foo // bar持有obj2.foo的引用
}
obj3.bar() // 谁调用就指向谁,this指向obj3

// obj 再嵌套一层methods
let obj = {
    methods: {
        foo() {
            console.log(this);
        }
    }
}
obj.methods.foo() // this指向 methods

// obj2 再嵌套一层methods
var obj2 = {
    methods: {
        foo() {
            console.log(this);
        }
    }

}
var obj3 = {
    bar: obj2.methods.foo // bar持有obj2.foo的引用
}
obj3.bar() // 谁调用就指向谁,this指向obj3

Tips:在隐式绑定情况下,this指向的是调用的对象。

3.显式绑定:

也称强绑定,如call、apply和bind方法

call、apply、bind的语法,如下:

call(obj,param1,param2,param3······),参数之间用逗号隔开,是一个参数列表
applay(obj,[param1、param2、param3······]),参数使用数组包含起来,是一个数组
bind(obj,param1,param2,param3······),参数之间用逗号隔开【和call使用语法一致】
使用call、apply、bind的语法的前提是它得是个函数,比如,只有函数才有call()方法,因为Function.prototype.call()

call、apply、bind的区别

① 区别

三种方法无参的情况下,call(obj)和apply(obj)的作用是一样的。

例子:

    var obj1 = {};
    var fun = function() {
        return this;
    };
    console.log(fun() == window); // true
    console.log(fun.call(obj1) == obj1); // true
    console.log(fun.apply(obj1) == obj1); // true
    console.log(fun.bind(obj1)() == obj1); // true
    
    var n = 123;
    var obj2 = { n: 456 };
    function a() {
        console.log(this.n);
    }
    a(0); // 123
    a.call(); // 123
    a.call(null); // 123
    a.call(undefined); // 123
    a.call(window); // 123
    a.call(obj2); // 456

补充说明:call、apply、bind方法的第一个参数,是一个对象。如果它们的第一个参数为空、null和undefined,则默认传入全局对象。下面换成apply和bind结果一样,只是bind后面再加一个()让其执行。可见绑定的参数,如果是null、undefined实际绑定的是全局作用域window,如果省去绑定的参数默认是绑定全局作用域

② bind的特殊性

call和apply绑定完this会立即调用当前的函数,而bind绑定完this不会立即调用当前函数,而是将函数返回

例子:

    var obj = {
        user: "追梦子",
        fn: function(e, ee) {
            console.log(this.user);
            console.log(e + ee);
        }
    };
    var a = obj.fn;
    a(1, 10); //  undefined  11
    // 因为这个this指向的是window, 而window.obj中才有user,所以第一个为undefined

    var b = obj.fn;
    b.call(obj, 1, 10); //'追梦子'  11

    var c = obj.fn;
    c.apply(obj,[1,10]); //'追梦子'  11

    var d = obj.fn;
    d.bind(obj,1,10)(); //'追梦子'  11

补充说明:通过强绑定call、apply和bind改变this的指向,this指向的是第一个参数,而这个参数一般而言是个对象。

③ 其他用法

使用call、apply、bind并不一定是为了改变this指向

    // 先品味下官网说的这句话?如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
    function add(){
      console.log(this == window);
    }
    console.log(add());  // true , bind 函数的参数列表为空 => bind()
    console.log(add(null)); // true , thisArg是null或undefined =>bind(null)  新函数的this指向的是执行作用域的 this
    console.log(add(undefined)); // true

例子:

// 1.
let obj = { a: 1 };
function multiply(x, y, z) {
  return x * y * z;
}
let fn = multiply.bind(undefined,10)
console.log(fn(20,30)); // 6000

// 2.
console.log(Math.max.apply(null, [10,20,30])); //30, apply可以默认将数组[10,20,30]转换为参数列表(10,20,30)
console.log(Math.max(...[10,20,30]));// 同上

4.new绑定:

使用new关键字来调用函数

回顾下这个问题,new关键字做了什么(面试常问)?过程如下

  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象 例子:
    function Person(){
        this.a = 'lwx'
    }
    var lwx = new Person()
    console.log(lwx.a); // lwx

Tips:通过构造函数这个工厂函数,生成一个新的对象,而构造函数的this就指向这个新对象。

注意:特别容易混淆的点,隐式绑定和独立调用,如下:

  function foo() {
    console.log(this);
    return function() {
      console.log(this);
    };
  }
  var obj = {
    foo: foo
  };
  obj.foo() // obj ,这是foo的隐式绑定
  obj.foo()(); //obj window ,前一个'()'是函数foo()的隐式绑定执行,后一个'()'是里面return的匿名函数的独立调用执行

5.其他绑定规则:

setTimeout,setInterval、forEach、find等数组方法、箭头函数、DOM事件处理函数中的this等等规则

注意为啥要let that = this或者通过强绑定来改变函数内部的this指向?之前总是一知半解,如下:

    function fn() {
        console.log(this); //指向的是obj
        function test() {
          console.log(this); // 指向window
        }
        test();
    }
    var obj = {
        say: fn
    };
    obj.say();

说明:因为函数内部的this指向并不由函数本身决定,而是取决于调用这个函数的调用者,所以在子函数作用域并不绝对继承直系父级函数作用域的this指向,所以产生改变this的操作。

...
   // test()
   test.call(this);
...

说明:只需要改一行代码,你会发现两次打印的this结果,都是指向obj !!!

箭头函数

例子:

    const foo = ()=>{} //箭头函数
    var obj ={
        name:3,
        foo:()=>{
          console.log(this);
        }
    }
    obj.foo() // window  this指向的是父级obj所在作用域的执行上下文对象,通俗的说,就是obj同级作用域的window对象

说明:箭头函数的this的指向:箭头函数本身没有this,这个this是父级所在的作用域的执行上下文对象。

默认绑定规则 对箭头函数无效

  function foo() {
    console.log("外层的this指向", this);
    var test = () => {
      console.log("里层的this指向", this);
    };
    return test;
  }
  var obj1 = {
    name: 1,
    foo: foo
  };
  obj1.foo(); // obj1  这是执行了foo这个函数,返回了一个test函数,返回了函数test,但没有执行
  obj1.foo()(); // obj1 obj1 这里执行的是foo这个函数返回的test函数,返回了函数test,且执行了通过foo.test()

弱绑定规则 对箭头函数无效

  var obj = {
    name: 3,
    foo: () => {
      console.log('foo',this);
    },
    boo(){
      console.log('boo',this);
    }
  };
  obj.foo(); // 指向window而不是obj,可以这个规则对箭头函数无效
  obj.boo(); // obj

强绑定规则 对箭头函数无效

function foo(){
    console.log('外层的this指向',this);
    var test = ()=>{
      console.log('里层的this指向',this);
    }
    return test
}
var obj2 ={
name:2,
foo:foo
}
var bar = foo().call(obj2) // window  window

new绑定规则 对箭头函数无效,且new不能构造箭头函数,只能构造普通函数

function Person(){
    this.name = 'lwx'
    console.log(this);
    this.say = ()=>{
      console.log(this);
    }
    this.eat = function(){
      console.log(this);
    }
}
let lwx = new Person() //Person {name: 'lwx'}
lwx.say() // Person {name: 'lwx', say: ƒ, eat: ƒ}
lwx.eat() // Person {name: 'lwx', say: ƒ, eat: ƒ}
Person()  // window

var foo = ()=>{
  console.log(this);
}
let lwx = new foo()
console.log(lwx); //报错:` Uncaught TypeError: foo is not a constructor`,new不能构造箭头函数,只能构造普通函数

Tips:切记,四个规则对箭头函数的完全无效!!!不能把规则乱用!!!

setTimeout、setInterval

  setTimeout(() => {
    console.log(this); //这里指向window,因为省去了前缀window,完整写法:window.setTimeout()
  }, 500);
  var obj = {
    name: "setTimeout检测",
    // 外层函数作用域
    say() {
      console.log('1',this); // obj
      setTimeout(() => {
        console.log("箭头函数", this); //指向obj,找到父函数setTimeout()所在的作用域上下文的this指向,即setTimeout()的同级作用域
      });
    },

    eat() {
      console.log('2',this); // obj
      setTimeout(function() {
        console.log("普通函数", this); //指向window,看谁调用了setTimeout函数, 即"window.setTimeout"所以是window
      });
    }
  };
  obj.say();
  obj.eat();
    var handler = {
      init: function () {
        console.log(this); // {init: ƒ, doSomething: ƒ}
        document.addEventListener('click',
          event => {
            console.log(this); // {init: ƒ, doSomething: ƒ}
            this.doSomething(event.type)
          }, false);
      },

      doSomething: function (type) {
        console.log('Handling ' + type + ' for ' + this.id);
      }
    };
    handler.init()

Tips: 如果不是箭头函数,第2个this指向的肯定时document,使用了箭头函数后,this指向document所在作用域的上下文对象,即init的作用域内,而init()函数是handler调用的,所以init内部的this指向的是handler内部的this。这里存在一点歧义:第1个this是弱绑定规则,第2个this是箭头函数,因为箭头函数不遵循这几个规则,所以第2个this找的只是document.add...这部分代码所在作用域的同级上下文,和第1个this的绑定规则是两回事,从内向外找this的一个整体思路。

forEach、map等数组方法

仔细研究下MDN对forEach的解析

语法::arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

解释:

  • callback 为数组中每个元素执行的函数,该函数接收一至三个参数:
    • currentValue数组中正在处理的当前元素。
    • index 可选数组中正在处理的当前元素的索引。
    • array 可选forEach() 方法正在操作的数组。
  • thisArg 可选可选参数。当执行回调函数 callback 时,用作 this 的值。
  • 返回值 undefined。
    let obj = { name: "lwx" };
    let arr = [1, 2];
    // 不指明第二个参数,则在window中这个this就是window,如果是Vue环境就是Vue实例
    arr.forEach(function(item, index, arr) {
        console.log(this); // window
    });
    // 普通函数指明了第二个参数,则回调函数的this指向第二个参数
    arr.forEach(function(item, index, arr) {
        console.log(this); // obj
    },obj);
    // 箭头函数指明了第二个参数,则回调函数的this指向第二个参数
    arr.forEach((item, index, arr) => {
        console.log(this); // window
    }, obj);

说明:类似上面的其他数组迭代方法中,第二个参数thisArg就是第一个回调函数callback的this指向。

DOM事件处理函数

DOM0级: element.onclick=function(event){}
DOM2级: element.addEventListener('click',function(event){},false)冒泡
DOM3级: element.addEventListener('keyup',function(event){},false),新增的鼠标键盘事件

注意: dom1级没有涉及事件,不是没有dom1标准

...`html代码`
    <div onclick="clickOne()">按钮one</div>
    <div class="btn2">按钮Two</div>
    <div onclick="click1(params)">按钮Three</div>
...

...`js代码`
    // Dom0级事件
    // function clickOne(){
    //   console.log(this,'按钮3的点击1'); // window
    // }
    const clickOne = ()=>{
        console.log(this,'按钮3的点击2'); // window
    }
    // function click1(context){
    //   console.log(context,'按钮2的点击1');// btn的dom节点
    // }
    const click1 = (context) => {
        console.log(context, "按钮2的点击2"); // btn的dom节点
    };
    
    // Dom2级事件
    let btn2 = document.querySelector('.btn2')
    btn2.addEventListener('click',function(){
        console.log(this == btn2); //true, this指向 btn2的dom节点
    })
    btn2.addEventListener('click',()=>{
        console.log(this == btn2); //false, this指向 window
    })
...

Tips:Dom0级事件不能重复定义事件,因为JavaScript中没有重载,只有后面的方法覆盖前面的方法;而dom2级事件,相对松耦合,底层代码可以让其绑定多个事件,类似重载。其this的指向,是根据这个方法内部的封装方案决定的,所以需要看api文档如何规定this指向。

思考下,既然改变this指向有多种,那么这几种情况混用,会发生什么呢?换句话说,改变this的指向有没有优先级?答案是肯定的,存在优先级
结论:优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

例子:

      // 强绑定 > 弱绑定
      function fn() {
        console.log(this.a);
      }
      var obj1 = {
        a: 1
      };
      var obj2 = {
        a: 2,
        fn: fn
      };
      obj2.fn.call(obj1); // 1

      // new > 强绑定
      function Foo(b) {
        this.a = b;
      }
      var obj3 = {};
      var Bar = Foo.bind(obj3);
      Bar(2);
      console.log(obj3.a); // 2
      var baz = new Bar(3); // Foo(不是Bar)中的this指向的是baz,之前是Foo(不是Bar)的this指向obj3,且new的优先级比bind高,而obj3和baz并没有直接关系,不影响obj3.a的值
      console.log(obj3.a); // 2
      console.log(baz.a); // 3

      console.log(Bar.prototype); // undefined
      console.log(baz.__proto__); // {constructor:f Foo(b)}
      console.log(baz.__proto__ === Bar.prototype); // false , 这里Bar的this指向是Foo,而不是baz
      console.log(baz.__proto__ === Foo.prototype); // true , 这里的Foo的this指向的是baz

这里有个疑问:构造函数作为一个复杂数据类型是按引用访问的,为甚么baz.proto === Bar.prototype是false? wht??? 我的理解是:this指向的是原构造函数Foo,而不是Bar的构造函数,Bar构造函数只是充当一个中转站。这个疑点,待求证!!!

  function Person(){
    this.sex = '女'
  }
  let lwx = new Person()
  console.log(Person.prototype == lwx.__proto__); // true

Tips:new绑定的时候,构造函数的this指向的是这个新的实例对象。且构造函数的显式原型prototype指向的是实例对象的隐式原型__proto__。

5.Vue框架绑的this定规则:

...
data() {
    return {
      a: 1,
      b: 2,
      c: this.a,
      d: this,
    };
},
mounted() {
    console.log(this.c, "c"); // undefined
    console.log(this.d, "d"); // VueComponent实例对象
    console.log(this.d.a, "a"); // 1
},
methods: {
    getTime() {
      console.log(this, "vue的实例对象"); //this指向 vue的实例对象
    },
    // 错误写法
    // getTime :()=>{
    //  console.log(this, 'undefined'); //this指向 undefined
    //}
},
computed: {
    getVal({ a, b }) {
      return a + b;
    },
    // 复杂写法
    // getVal:{
    //     get({ a, b }){
    //       return a + b;
    //     }
    //   }
},
...
结论:
  • data中的this不能重复引用VueComponent实例对象,mounted中的this使用则是正常的。
  • methods中的箭头函数的写法,this指向undefined是因为vue默认开启了严格模式。
  • computed中的参数可以进行解构,写起来更方便。

最后来看一道关于this的面试题:

例子:

    var x = 20;
    var a = {
    x: 15,
    fn: function() {
      var x = 30;
      return function() {
        return this.x;
      };
    }
    };
    console.log(a.fn()); // function() {return this.x}
    console.log(a.fn()()); // 20
    console.log(a.fn()()); // 20
    console.log(a.fn()() == a.fn()()); // true
    console.log(a.fn().call(this)); // 20
    console.log(a.fn().call(a)); // 15

这篇文章参考了阮一峰在Es6提及this的文章,有兴趣可以研究下。

读到最后,this的指向问题就算弄懂了,赶紧收藏种草吧!!!