美文网首页
this 指向问题

this 指向问题

作者: 五十岚色叶 | 来源:发表于2019-11-04 14:05 被阅读0次

    日期:2019 年 9 月 5 日

    this 指向问题

    介绍

    this 指向问题一直是 js 中一个令人头疼的问题,这几天得空,复习了一下以前的知识,顺便整理了有关 js 中 this 指向的知识点。

    this 绑定

    很多人对 this 指向一直都存在一个误区:this 写在谁里面就指向谁。但其实这是不对的,this 既不指向函数自身,也不指函数的词法作用域,它实际是在函数被调用时才发生的绑定,也就是说this具体指向什么,取决于你是怎么调用的函数。


    this 存在4种绑定规则:

    • 默认绑定
    • 隐式绑定
    • 显示绑定
    • new绑定

    这 4 种绑定规则的优先级是从低到高的

    1、默认绑定
    function foo(){
            console.log(this.a);
    }
    var a = 2;
    foo();  // 打印结果?
    

    打印结果:2

    因为foo()是直接调用的(独立函数调用),没有应用其他的绑定规则,这里进行了默认绑定,将全局对象( window )绑定this上,所以this.a 就解析成了全局变量中的a,即 2


    注意:在严格模式下(strict mode),全局对象将无法使用默认绑定( this 指向 undefined ),即执行会报 undefined 的错误:

    function foo(){
            “use strict”;
            console.log(this.a);
    }
    var a = 2;
    foo();  //  Uncaught  TypeError : Cannot read property 'a' of undefined
    
    

    另外,这几天正在学 Typescript,刚好也遇到了一些 this 指向的问题,这里也说道说道,看一看下面的几种情况:

    例一:

    let a = 233;
    let obj1 = {
        a: 666,
        fn: function(){
            console.log(this.a);
        }
    }
    
    obj1.fn();    //  输出: 666
    
    

    例二:

    let a = 233;
    let obj2 = {
        a: 888,
        fn: function(){
            console.log("外层:", this.a);
            return function(){
                console.log("内层:", this.a);
            }
        }
    }
    
    let ha = obj2.fn();  //  外层: 888
    ha();    //  内层: undefined
    
    

    例三:

    var a = 233;
    let obj2 = {
        a: 888,
        fn: function(){
            console.log("外层:", this.a);
            return function(){
                console.log("内层:", this.a);
            }
        }
    }
    
    let ha = obj2.fn();  //  外层: 888
    ha();    //  内层: 233
    
    

    比较之下我们可以看到,对象的 fn 属性是一个函数,对象调用这个函数,函数的执行上下文就是这个对象,所以外层的 this 指向对象没有问题;

    然后 fn 还返回了一个函数,这个函数里面的 this (内层 this)指向的是 window 而不是对象 obj2;

    有人就要问了既然指向 window,为什么例二中内层的输出不是 233 呢?这个问题问得好,这就涉及到了 ES6 与 ES5 中变量声明方面的区别了:

    • ES5声明变量只有两种方式:var 和 function
    • ES6有 let、const、import、class 再加上 ES5 的 var、function 共有六种声明变量的方式
    • 还需要了解顶层对象:浏览器环境中顶层对象是window,Node中是global对象
    • ES5中,顶层对象的属性等价于全局变量 (敲黑板了啊)
    • ES6中,有所改变:var、function声明的全局变量,依然是顶层对象的属性;let、const、class声明的全局变量不属于顶层对象的属性,也就是说ES6开始,全局变量和顶层对象的属性开始分离、脱钩

    所以ES6非严格模式下,与var声明的全局变量都会成为window的属性:

    a = 1;
    console.log(window.a);  // 1
    var b = 2;
    console.log(window.b);  // 2
    

    而使用let声明的全局变量,不会成为window的属性:

    let c = 3;
    console.log(window.c);  // undefined
    

    关于这方面问题,更多请移步原文:

    关于let声明的变量在window下无法获取的问题

    2、隐式绑定

    除了直接对函数进行调用外,有些情况是,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象

    function foo(){
            console.log(this.a);
    }
    var a = 2;
    var obj = {
            a : 3,
            foo : foo
    }
    obj.foo();    // ?
    
    

    输出结果: 3

    这里foo函数被当做引用属性,被添加到obj对象上。这里的调用过程是这样的:

    获取obj.foo属性 -> 根据引用关系找到foo函数,执行调用,所以这里对foo的调用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,所以this.a被解析成了obj.a,即 3

    当存在多层调用链时:

    function foo(){
            console.log(this.a);
    }
    var a = 2;
    var obj1 = {
            a : 4,
            foo : foo
    }
    var obj2 = {
            a : 3,
            obj1 : obj1
    }
    obj2.obj1.foo();    // ?
    
    

    输出结果:4

    同样,我们看下函数的调用过程:

    先获取obj2.obj1 -> 通过引用获取到obj1对象,再访问 obj1.foo -> 最后执行foo函数调用
    这里调用链不只一层,存在obj1、obj2两个对象,那么隐式绑定具体会绑哪个对象。这里原则是获取最后一层调用的上下文对象,即obj1,所以结果显然是 4

    3、显示绑定

    对于隐式绑定,this值在调用过程中会动态变化,可是我们就想绑定指定的对象,这时就用到了显示绑定

    显示绑定主要是通过改变对象的prototype关联对象,这里不展开讲。具体使用上,可以通过这两个方法 call() 或 apply() 来实现(大多数函数及自己创建的函数默认都提供这两个方法)

    call 与apply 是同样的作用,区别只是其他参数的设置上:

    /*apply()方法*/
    function.apply(thisObj[, argArray])
    
    /*call()方法*/
    function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);
    

    它们各自的定义:

    apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。

    call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。

    它们的共同之处:

    都“可以用来代替另一个对象调用一个方法,将一个函数的对象上下文从初始的上下文改变为由thisObj指定的新对象”。

    它们的不同之处:

    apply:最多只能有两个参数——新this对象和一个数组argArray。如果给该方法传递多个参数,则把参数都写进这个数组里面,当然,即使只有一个参数,也要写进数组里。如果argArray不是一个有效的数组或arguments对象,那么将导致一个TypeError。如果没有提供argArray和thisObj任何一个参数,那么Global对象将被用作thisObj,并且无法被传递任何参数。

    call:它可以接受多个参数,第一个参数与apply一样,后面则是一串参数列表。这个方法主要用在js对象各方法相互调用的时候,使当前this实例指针保持一致,或者在特殊情况下需要改变this指针。如果没有提供thisObj参数,那么 Global 对象被用作thisObj。 实际上,apply和call的功能是一样的,只是传入的参数列表形式不同。

    有关 apply 与 call 的比较,详情请移步原文:

    apply() 与 call() 的区别



    OK,回归正题,我们来看看显示绑定:

    function foo(){
            console.log(this.a);
    }
    var a = 2;
    var obj1 = {
            a : 3,
            foo : foo
    }
    var obj2 = {
            a : 4,
            obj1 : obj1
    }
    foo.call(obj1);    // ?
    foo.call(obj2);   // ?
    
    

    结果:

    3

    4

    这里因为显示的申明了要绑定的对象,所以this就被绑定到了 obj 上,打印的结果自然就是obj1.a 和 obj2.a

    4、new 绑定
    var a = 2;
    function foo(a){
        this.a = a;
    }
    let bar1 = new foo(3);
    console.log(bar1.a); // ?
    let bar2 = new foo(4);
    console.log(bar2.a); // ?
    

    结果:

    3

    4

    因为每次调用生成的是全新的对象,该对象又会自动绑定到this上,所以答案显而易见

    绑定规则优先级

    上面也说过,这里在重复一下。优先级是这样的,以按照下面的顺序来进行判断:

    函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象;
    函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象;
    函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象;
    如果都不是的话,使用默认绑定。如果在严格模式下,就绑定undefined,否则绑定到全局对象。


    规则例外:

    在显示绑定中,对于 null 和 undefined 的绑定将不会生效

    function foo(){
            console.log(this.a);
    }
    var a = 2;
    foo.call(null);  //  2
    foo.call(undefined);  //  2
    
    

    这种情况主要是用在不关心this的具体绑定对象(用来忽略this),而传入null实际上会进行默认绑定,导致函数中可能会使用到全局变量,与预期不符

    所以对于要忽略this的情况,可以传入一个空对象ø,该对象通过Object.create(null)创建。这里不用{}的原因是,ø是真正意义上的空对象,它不创建Object.prototype委托,{}和普通对象一样,有原型链委托关系

    箭头函数中的 this

    箭头函数的this指向:

    1. 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this;
    2. 箭头函数里的this指向无法被call,apply,bind 改变;
    var obj = {
            foo(){
                console.log(this);
            },
            bar : ()=>{
                console.log(this);
            }
    }
    obj.foo();  //  { foo: foo(), bar: bar() }
    obj.bar();  //  window
    
    

    相关文章

      网友评论

          本文标题:this 指向问题

          本文链接:https://www.haomeiwen.com/subject/jeqdbctx.html