美文网首页
JS入门难点解析7-this

JS入门难点解析7-this

作者: love丁酥酥 | 来源:发表于2018-01-30 22:03 被阅读62次

    (注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!)
    (注2:更多内容请查看我的目录。)

    1. 简介

    老样子,我们列一下执行上下文的三大属性:

    • 变量对象(Variable object,VO)
    • 作用域链(Scope chain)
    • this

    this是一个非常容易让人混淆的概念。首先我们思考一下JS中为什么会有this。

    2. this的作用

    看一下如下代码:

    function identify() {
        return this.name.toUpperCase();
    }
    
    function speak() {
        var greeting = "Hello, I'm " + identify.call(this);
        console.log(greeting);
    }
    
    var me = {
        name: "Kyle"
    };
    var you = {
        name: "Reader"
    };
    identify.call(me); // KYLE
    identify.call(you); // READER
    speak.call(me); // Hello, 我是 KYLE 
    speak.call( you ); // Hello, 我是 READER
    

    这段代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(), 不用针对每个对象编写不同版本的函数。

    如果不使用 this,那就需要给 identify() 和 speak() 显式传入一个上下文对象。如下所示:

    function identify(context) {
        return context.name.toUpperCase();
    }
    
    function speak(context) {
        var greeting = "Hello, I'm " + identify(context);
        console.log(greeting);
    }
    
    identify(you); // READER
    speak(me); //hello, 我是 KYLE
    

    然而,this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。后面介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象有多重要。

    3. this的两种错误解读

    this常见的错误解读有两种,下面我们来仔细分析一下。

    3.1 this指向自身

    this,字面上的理解就是“这”,大家很容易将其解读为指向这个函数自身。看一下如下代码:

    function foo(num) {
        console.log( "foo: " + num );
        // 记录 foo 被调用的次数
        this.count++; 
    }
    foo.count = 0;
    var i;
    for (i=0; i<10; i++) { 
        if (i > 5) {
            foo( i ); 
        }
    }
    // foo: 6
    // foo: 7
    // foo: 8
    // foo: 9
    // foo被调用了多少次?
    console.log( foo.count ); // 0
    

    console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然是 0。显然从字面意思来理解 this 是错误的。其实,此处this.count++创建了一个全局变量count。执行count++以后count变量成了NaN。如果不相信,大家可以在最后一行尝试输出window.count。

    这个例子说明this并不能单纯理解为指向这个函数本身。不过,既然this不是指向函数本身,我们在函数内部如何引用函数本身呢?主要有以下三个方法:

    1. 具名引用
      例如:
    function foo() {
      foo.count = 4; // foo指向它自身
    }
    

    该方法只能用于具名函数中。

    1. arguments. callee
      例如:
    setTimeout( function(){
      arguments.callee.count = 4;  // 匿名(没有名字的)函数无法指向自身
    }, 10 );
    

    但是这种写法已被废弃,不建议使用。

    1. this
      刚才不是说this不是指向函数本身么?可是现在为什又说可以呢?别急,等看完这篇文章,你自然会有答案。

    3.2 this指向其作用域

    这是this最使人混淆的地方。需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。同时,this与作用域链也不相关。

    看下面的代码:

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

    这段代码本意是想,foo在全局定义,那么this就指向全局,this.bar就可以调用全局中定义的bar,而bar执行的时候呢正好是在foo的执行上下文,所以this指向foo。其实这里对this的两处解读都是错误的。首先,this.bar()能够运行完全是一种偶然,怎样的偶然呢?你使用的是非严格模式,你是在浏览器环境运行而不是在node运行,你是独立调用的foo而正好bar在全局声明。是不是很巧合呢?是不是有点迷糊,不要紧,继续往下看。第二点,代码视图在bar里面打印foo的变量,这里是完全错误的,因为bar在运行时,this也是指向了全局(非严格模式,下面我们的讨论都是基于运行于浏览器的非严格模式)。

    3.3 this的真正解读

    this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    4. 绑定规则与函数调用方式

    this在运行时进行绑定,绑定主要有四种规则,取决于绑定时候函数的调用方式。

    4.1 默认绑定与独立调用(函数调用模式)

    独立调用是指函数作为一个普通函数来调用。当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的。对于普通的函数调用来说,函数的返回值就是调用表达式的值。

    使用函数调用模式调用函数时,非严格模式下,this被绑定到全局对象;在严格模式下,this是undefined。

    以下是四种常见的独立调用场景。

    1. 普通独立调用
    function foo(){
        console.log(this === window);
    }
    foo(); //true
    
    1. 被嵌套的函数独立调用
    //虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
    var a = 0;
    var obj = {
        a : 2,
        foo:function(){
                function test(){
                    console.log(this.a);
                }
                test();
        }
    }
    obj.foo();//0
    

    3.IIFE(立即执行函数)

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

    其实立即执行函数可以理解为立即赋值并独立调用。上面代码其实和下面效果一样:

    var a = 0;
    function foo(){
        var temp = (function test(){
            console.log(this.a);
        });
        temp();  // 独立调用
    };
    var obj = {
        a : 2,
        foo:foo
    }
    obj.foo();//0
    
    1. 闭包
    var a = 0;
    function foo(){
        function test(){
            console.log(this.a);
        }
        return test;
    };
    var obj = {
        a : 2,
        foo:foo
    }
    obj.foo()();//0
    

    4.2. 隐式绑定和方法调用(方法调用模式)

    当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被直接对象所调用时,this会被隐式绑定到该对象。如果调用表达式包含一个提取属性的动作,那么它就是被当做一个方法来调用。要记住,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

    function foo(){
        console.log(this.a);
    };
    var obj1 = {
        a:1,
        foo:foo,
        obj2:{
            a:2,
            foo:foo
        }
    }
    
    //foo()函数的直接对象是obj1,this隐式绑定到obj1
    obj1.foo();//1
    
    //foo()函数的直接对象是obj2,this隐式绑定到obj2
    obj1.obj2.foo();//2
    

    对于隐式绑定,是最容易出现错误的地方,也是面试出陷阱题最多的地方。因为很容易出现所谓的隐式丢失。隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。我们来看一下哪些情况会出现隐式丢失。

    1. 函数别名
    var a = 0;
    function foo(){
        console.log(this.a);
    };
    var obj = {
        a : 2,
        foo:foo
    }
    //把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
    var bar = obj.foo;
    bar();//0
    

    等价于

    var a = 0;
    var bar = function foo(){
        console.log(this.a);
    }
    bar();//0
    

    其实,要理解一点。就是函数在进入函数执行上下文时才会进行this绑定,也就是这个函数调用以后才会进行this绑定。而此处仅仅是做了引用赋值,然后进行了bar的独立调用。后面出现的所谓隐式丢失,其实都可以用这个道理去解释。

    1. 参数传递
    var a = 0;
    function foo(){
        console.log(this.a);
    };
    function bar(fn){
        fn();
    }
    var obj = {
        a : 2,
        foo:foo
    }
    //把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
    bar(obj.foo);//0
    

    等价于

    //等价于
    var a = 0;
    function bar(fn){
        fn();
    }
    bar(function foo(){
        console.log(this.a);
    });
    
    1. 内置函数
    var a = 0;
    function foo(){
        console.log(this.a);
    };
    var obj = {
        a : 2,
        foo:foo
    }
    setTimeout(obj.foo,100);//0
    

    等价于

    var a = 0;
    setTimeout(function foo(){
        console.log(this.a);
    },100);//0
    
    1. 间接引用
    function foo() {
        console.log( this.a );
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    //将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
    (p.foo = o.foo)(); // 2
    

    不等价于

    function foo() {
        console.log( this.a );
    }
    var a = 2;
    var o = { a: 3, foo: foo };
    var p = { a: 4 };
    o.foo(); // 3
    //将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
    p.foo = o.foo;
    p.foo();//4
    
    1. 其他情况
      在javascript引擎内部,obj和obj.foo储存在两个内存地址,简称为M1和M2。只有obj.foo()这样调用时,是从M1调用M2,因此this指向obj。但是,下面三种情况,都是直接取出M2进行运算,然后就在全局环境执行运算结果(还是M2),因此this指向全局环境。
    var a = 0;
    var obj = {
        a : 2,
        foo:foo
    };
    function foo() {
        console.log( this.a );
    };
    
    (obj.foo = obj.foo)(); // 0
    
    (false || obj.foo)(); // 0
    
    (1, obj.foo)(); // 0
    
    // 直接加括号并不会有造成隐式丢失
    (obj.foo)(); // 2
    

    总结:其实,隐式绑定只有在直接进行对象方法调用时才会出现,就是读取到属性方法以后直接在后面加括号调用,如下:

    obj.foo();
    

    如果在调用前经过了任何运算,比如“=”,“,”“||”等运算(注意"()"并不是运算符),其实都是执行了一个隐式的赋值引用,然后对被隐式赋值的函数进行了直接调用,如下:

    (obj2.foo = obj.foo)();
    (obj.foo = obj.foo)();
    (false || obj.foo)();
    (1, obj.foo)();
    ...
    

    等价于

    var temp = (obj2.foo = obj.foo);temp();
    var temp = (obj.foo = obj.foo);temp();
    var temp = (false || obj.foo);temp();
    var temp = (1, obj.foo);temp();
    ...
    

    4.3 显式绑定与间接调用(间接调用模式)

    在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

    可以通过call()、apply()、bind()方法把对象绑定到this上,这种做法叫做显式绑定。对于被调用的函数来说,叫做间接调用。

    var a = 0;
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2
    };
    foo(); // 0
    foo.call(obj); // 2
    

    obj并没有指向函数foo的属性,但却可以间接调用foo。这时候我们可以回答文章开头提出的问题了。另一种指向自身的方式,使用this进行显示绑定。

    function foo(){
        console.log(this);
    }
    foo();  // Window
    foo.call(foo);  // f foo
    

    不过,这种普通的显式绑定无法解决前面提到的隐式丢失问题。以前面所举的函数别名导致隐式丢失的代码为例。

    var a = 0;
    function foo(){
        console.log(this.a);
    };
    var obj = {
        a : 2,
        foo:foo
    }
    //把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
    var bar = obj.foo;
    bar();//0
    

    改成如下显示绑定:

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

    说明,call传入的对象改变时,隐式绑定的对象也发生了改变,this不再绑定foo的直接拥有者obj,发生了隐式丢失。那么如何防止这种隐式丢失呢?只要想办法让this始终指向其属性拥有者即可。当然我们也可以让this指向任何事先设定的对象,做到一种强制的绑定,也就是所谓的硬绑定。

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

    不管给call传入什么,最后this实际绑定的对象都是预先指定的obj。

    JavaScript中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()

    var id = 'window';
    function foo(el){
        console.log(el,this.id);
    }
    var obj = {
        id: 'fn'
    };
    [1,2,3].forEach(foo); // 1 "window" 2 "window" 3 "window"
    [1,2,3].forEach(foo,obj); // 1 "fn" 2 "fn" 3 "fn"
    

    4.4 new绑定和构造函数调用(构造函数调用模式)

    如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。要注意以下几点:

    1. 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值。
    function fn(){
        this.a = 2;
    }
    var test = new fn();
    console.log(test);  // {a:2}
    
    1. 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果。
    function fn(){
        this.a = 2;
        return 1;
    }
    var test = new fn();
    console.log(test); // {a:2}
    
    1. 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象。
    var obj = {a:1};
    function fn(){
        this.a = 2;
        return obj;
    }
    var test = new fn();
    console.log(test);  // {a:1}
    
    1. 尽管有时候构造函数看起来像一个方法调用,它依然会使用这个新对象作为this。也就是说,在表达式new o.m()中,this并不是o。
    var o = {
        m: function(){
            this.a = 1;
            return this;
        }
    }
    var obj = new o.m();
    console.log(obj, obj === o);  // {a:1} , false
    console.log(obj.a);  // 1
    console.log(o.a);  // undefined
    console.log(o.m.a);  // undefined
    console.log(obj.constructor === o.m);  // true
    

    5. this绑定优先级

    现在我们已经了解了函数调用中 this 绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?为了 解决这个问题就必须给这些规则设定优先级,这就是我们接下来要介绍的内容。

    毫无疑问,默认绑定的优先级是四条规则中最低的,所以我们可以先不考虑它。现在我们将其与另外三种规则互相比较。

    5.1 显式绑定 vs 隐式绑定

    function foo() {
        console.log( this.a );
    }
    var obj1 = {
        a: 2,
        foo: foo
    };
    var obj2 = {
        a: 3,
        foo: foo
    };
    obj1.foo(); // 2
    obj2.foo(); // 3
    //在该语句中,显式绑定call(obj2)和隐式绑定obj1.foo同时出现,最终结果为3,说明被绑定到了obj2中
    obj1.foo.call( obj2 ); // 3
    obj2.foo.call( obj1 ); // 2
    

    可以看到,显式绑定优于隐式绑定

    5.2 new绑定 vs 隐式绑定

    function foo(something) {
        this.a = something;
    }
    var obj1 = {foo: foo};
    var obj2 = {};
    
    obj1.foo( 2 );
    console.log( obj1.a ); // 2
    obj1.foo.call(obj2,3);
    console.log( obj2.a ); // 3
    
    //在下列代码中,隐式绑定obj1.foo和new绑定同时出现。最终obj1.a结果是2,而bar.a结果是4,说明this被绑定到new创建的新对象上
    var bar = new obj1.foo( 4 );
    console.log( obj1.a ); // 2
    console.log( bar.a ); // 4
    

    可以看到,new绑定优先于隐式绑定

    5.3 new绑定 vs 显式绑定

    function foo(something) {
        this.a = something;
    }
    var obj1 = {};
    
    // 先将obj1绑定到foo函数中,此时this值为obj1
    var bar = foo.bind( obj1 );
    bar( 2 );
    console.log(obj1.a); // 2
    // 通过new绑定,此时this值为baz
    var baz = new bar( 3 );
    console.log( obj1.a ); // 2
    // 说明使用new绑定时,在bar函数内,无论this指向obj1有没有生效,最终this都指向实例baz
    console.log( baz.a ); // 3
    

    6. 总结

    关于this的绑定,可以按照如下顺序判定:

    1. 是否是new绑定?如果是,this绑定的是新创建的实例对象
    var bar = new foo();   // 绑定bar
    
    1. 是否是显式绑定?如果是,this绑定的是指定的对象
    var bar = foo.call(obj2);  // 绑定obj2
    
    1. 是否是隐式绑定?如果是,this绑定的是调用的对象
    var bar = obj1.foo();   // 绑定obj1
    
    1. 如果都不是,则使用默认绑定
    var bar = foo();  // 绑定到全局对象(非严格模式)或者undefined(严格模式)
    

    参考

    深入理解this机制系列第一篇——this的4种绑定规则
    JavaScript深入之从ECMAScript规范解读this
    深入理解javascript函数系列第一篇——函数概述
    深入理解this机制系列第二篇——this绑定优先级
    BOOK-《你不知道的JavaScript》 第2部分

    相关文章

      网友评论

          本文标题:JS入门难点解析7-this

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