JS中的this

解析JS中this指向

Posted by SkioFox on July 18, 2017

要理解this,就得先理解JS的函数作用域。js中采用的是词法作用域,也就是静态作用域,所以函数的作用域在函数定义的时候就决定了

    var value = 1;
    function foo() {
        console.log(value);
    }
    function bar() {
        var value = 2;
        foo();
    }
    bar();

假设JavaScript采用静态作用域,让我们分析下执行过程:

执行foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。

假设JavaScript采用动态作用域,让我们分析下执行过程:

执行foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。

前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是1。

this的指向类型

  • 由于箭头函数不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值。

    call() / apply() / bind() 方法对于箭头函数来说只是传入参数,对它的this毫无影响。 考虑到 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略

          function Person() {  
              this.age = 0;  
              setInterval(() => {
                  // 回调里面的 `this` 变量就指向了期望的那个对象了
                  // 这里this指向当前对象的实例
                  this.age++;
              }, 3000);
          }
    
          var p = new Person();
    
  • 非箭头函数

在非箭头函数下,this 指向调用其所在函数的对象,而且是离谁近就是指向谁(此对于常规对象,原型链, getter & setter等都适用)。构造函数下,this与被创建的新对象绑定;DOM事件,this指向触发事件的元素;内联事件分两种情况,bind绑定, call & apply 方法等

  • 在全局环境下,this 始终指向全局对象(window), 无论是否严格模式;
    console.log(this.document === document); // true

    // 在浏览器中,全局对象为 window 对象:
    console.log(this === window); // true

    this.a = 37;
    console.log(window.a); // 37
  • 函数上下文调用
    • 函数直接调用

      普通函数内部的this分两种情况,严格模式和非严格模式。

            // 非严格模式下,this 默认指向全局对象window
            function f1(){
                return this;
            }
            f1() === window; // true
      
            // 而严格模式下, this为undefined
            function f2(){
                "use strict"; // 这里是严格模式
                return this;
            }
            f2() === undefined; // true 
      
    • 对象中的this

      对象内部方法的this指向调用这些方法的对象

      1. 函数的定义位置不影响其this指向,this指向只和调用函数的对象有关。多层嵌套的对象,
      2. 内部方法的this指向离被调用函数最近的对象(window也是对象,其内部对象调用方法的this指向内部对象, 而非window)。
        var o = {
            prop: 37,
            f: function() {
                return this.prop;
            }
        };
        console.log(o.f());  //37  this指向o
        var a = o.f;
        console.log(a()):  //undefined // this指向windows
      
        var o = {prop: 37};
      
        function independent() {
            return this.prop;
        }
      
        o.f = independent;
      
        console.log(o.f()); // logs 37 // this指向 o
      
        //2
        o.b = {
            g: independent,
            prop: 42
        };
        console.log(o.b.g()); // logs 42  // this指向o
      
    • 原型链中this

      原型链中的方法的this仍然指向调用它的对象

            var o = {
                f : function(){ 
                    return this.a + this.b; 
                }
            };
            var p = Object.create(o);
            p.a = 1;
            p.b = 4;
            console.log(p.f()); // 5 this指向p
      

      可以看出, 在p中没有属性f,当执行p.f()时,会查找p的原型链,找到 f 函数并执行,但这与函数内部this指向对象 p 没有任何关系,只需记住谁调用指向谁。以上对于函数作为getter & setter 调用时同样适用

    • 构造函数中this

      构造函数中的this与被创建的新对象绑定。注意:当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回this。

          function C(){
              this.a = 37;
          }
    
          var o = new C();
          console.log(o.a); // logs 37
    
    
          function C2(){
              this.a = 37;
              return {a:38};
          }
    
          var b = new C2();
          console.log(b.a); // logs 38
    

    以上两个例子内部的this指向对象o/b, 看到这里的你不妨在控制台执行下以上代码,看看对象 o 和 b ,(C2函数中的this.a = 37 对整个过程完全没有影响的, 可以被忽略的)。

    • call & apply 当函数通过Function对象的原型中继承的方法 call() 和 apply() 方法调用时, 其函数内部的this值可绑定到 call() & apply() 方法指定的第一个对象上, 如果第一个参数不是对象,JavaScript内部会尝试将其转换成对象然后指向它。
          function add(c, d){
              return this.a + this.b + c + d;
          }
    
          var o = {a:1, b:3};
    
          add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
    
          add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
    
          function tt() {
              console.log(this);
          }
          tt.call(5);  // this指向Number {[[PrimitiveValue]]: 5} 
          tt.call('asd'); // this指向String {0: "a", 1: "s", 2: "d", length: 3, [[PrimitiveValue]]: "asd"}
    
    • bind 方法

      bind方法在ES5引入,在Function的原型链上,Function.prototype.bind。通过bind方法绑定后, 函数将被永远绑定在其第一个参数对象上, 而无论其在什么情况下被调用。

          function f(){
              return this.a;
          }
    
          var g = f.bind({a:"azerty"});
          console.log(g()); // azerty
    
          var o = {a:37, f:f, g:g};
          console.log(o.f(), o.g()); // 37, azerty
    
    • DOM 事件处理函数中的 this & 内联事件中的 this
      • DOM 事件处理函数中的 this
          // 被调用时,将关联的元素变成蓝色
          function bluify(e){
          //在控制台打印出所点击元素
          console.log(this);
          //阻止事件冒泡
          e.stopPropagation();
          //阻止元素的默认事件
          e.preventDefault();      
          this.style.backgroundColor = '#A5D9F3';
          }
        
          // 获取文档中的所有元素的列表
          var elements = document.getElementsByTagName('*');
        
          // 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
          for(var i=0 ; i<elements.length ; i++){
          elements[i].addEventListener('click', bluify, false);
          }
        
      • 内联事件
        • 当代码被内联处理函数调用时,它的this指向监听器所在的DOM元素
        • 当代码被包括在函数内部执行时,其this指向等同于 **函数直接调用**的情况,即在非严格模式指向全局对象window, 在严格模式指向undefined avatar
      • setTimeout & setInterval

        对于延时函数内部的回调函数的this指向全局对象window(当然我们可以通过bind方法改变其内部函数的this指向)

            //默认情况下代码
        function Person() {  
            this.age = 0;  
            setTimeout(function() {
                console.log(this);
            }, 3000);
        }
      
        var p = new Person();//3秒后返回 window 对象
        ==============================================
        //通过bind绑定
        function Person() {  
            this.age = 0;  
            setTimeout((function() {
                console.log(this);
            }).bind(this), 3000);
        }
      
        var p = new Person();//3秒后返回构造函数新生成的对象 Person{...}
      

      call(),apply(),bind()方法实现

  • 区别:

call方法,表示传入的对象参数调用call前面对象的方法,并且被调用的函数会被执行,call方法的参数是当前上下文的对象以及参数列表。

apply也是如此,只不过它传入的参数是对象和参数数组。

bind,用法与apply, call一样,但是它被对象绑定的函数不会被执行,而是返回这个函数,需要你手动去调用返回的函数,才会返回结果。

// call, apply, bind的区别
var a = {value: 1}
function getValue(name, age) {
    console.log('arguments in fn = ', arguments)
    console.log(name, age)
    console.log(this.value)
}
getValue.call(a,'yandong1', 17)
let bindFoo = getValue.bind(a, 'testBind', 45)
console.log('bindFoo = ',bindFoo)
bindFoo()
getValue.apply(a,['yandong2', 18])
var returnedFunc = getValue.bind(a,'yandong3', 19)
console.log(returnedFunc)
returnedFunc()
/*
arguments in fn =  [Arguments] { '0': 'yandong1', '1': 17 }
yandong1 17
1
bindFoo =  function () { [native code] }
arguments in fn =  [Arguments] { '0': 'testBind', '1': 45 }
testBind 45
1
arguments in fn =  [Arguments] { '0': 'yandong2', '1': 18 }
yandong2 18
1
[Function: bound getValue]
arguments in fn =  [Arguments] { '0': 'yandong3', '1': 19 }
yandong3 19
1
*/
// 我们可以看到,call, apply都是直接返回函数执行后的结果,而bind是返回一个函数,之后手动执行之后才会将结果返回。
  • 实现call()方法
    // 手写模拟call方法的思想
    /**
     * call方法思想:改变this指向,让新的对象可以执行这个方法
     * 实现思路:
     * 1、给新的对象添加一个函数(方法),并让this(也就是当前绑定的函数)指向这个函数
     * 2、执行这个函数
     * 3、执行完以后删除这个方法
     * 4、可以将执行结果返回
     */
    Function.prototype.myCall = function(funcCtx) {
        // funcCtx是当前要调用函数的对象
        console.log('funcCtx = ',funcCtx)
        // this指被调用的函数
        console.log('this = ',this)
        if(typeof this != 'function') {
            throw new TypeError('Erorr')
        }
        let ctx = funcCtx || global
        console.log('arguemnets = ', arguments)
        let args = [...arguments].slice(1)
        console.log(`args = ${args}`)

        ctx.fn = this // 为当前对象添加一个函数fn, 值为要已经定义的要调用的函数
        console.log('ctx.fn = ', ctx.fn)
        // 执行添加的函数fn
        var result = ctx.fn(...args)
        // 执行完以后删除
        delete ctx.fn
        return result
    }
    getValue.myCall(a,'test', 20)
  • 实现apply()方法
    Function.prototype.myApply = function(funcCtx) {
        console.log(this)
        if(typeof this != 'function') {
            throw new TypeError('Erorr')
        }
        let ctx = funcCtx || global

        ctx.fn = this
        console.log('arguemnets = ', arguments)
        let result
        // 判断参数是否存在
        if(arguments[1]) {
            result = ctx.fn(...arguments[1])
        } else {
            result = ctx.fn()
        }
        delete ctx.fn
        return result
    }
    getValue.myApply(a, ['eo', 50])
  • 实现bind()方法

bind() 方法相较之前的两个函数则要复杂一些。

在 MDN 中的定义是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值,并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。

    let bindFn1 = addNum.bind(obj);
    console.log(bindFn1(2,3)); // 6
    let bindFn2 = addNum.bind(obj, 2);
    console.log(bindFn2(3)); // 6
    let bindFn3 = addNum.bind(obj, 2, 3);
    console.log(bindFn3()); // 6

所以我们实现的 bind() 方法需要有以下特性:

1. 返回一个函数,该函数可以直接调用也可以通过 new 方式调用;

2. 直接调用则改变函数 this 指向,通过 new 方式调用则忽略;

3. 返回函数能接收 bind 函数传递的部分参数;
    Function.prototype.myBind = function (context) {
        if (typeof this !== 'function') throw new TypeError('Error');

        const _this = this
        const args = [...arguments].slice(1)

        return function F() {
            if (this instanceof F) {   //  通过 new 方式调用的情况
                return new _this(...args, ...arguments)
            }
            return _this.apply(context, args.concat(...arguments))
        }
    }

    // 通过 myBind() 方法来实现上述例子依旧能得到正确的返回结果
    let bindFn1 = addNum.myBind(obj);
    console.log(bindFn1(2,3)); // 6
    let bindFn2 = addNum.myBind(obj, 2);
    console.log(bindFn2(3)); // 6
    let bindFn3 = addNum.myBind(obj, 2, 3);
    console.log(bindFn3()); // 6

总结

  1. 非箭头函数的this指向由调用该函数的this决定,即与this的执行上下文相关联(调用的对象)
  2. 箭头函数由于内部没有this,期函数内部的this取决于定义箭头函数时的当前上下文(即定义时的对象)
  3. call、bind、apply方法均可以改变this指向第一个参数对象