this

作者: Bai_白 | 来源:发表于2018-11-28 11:29 被阅读0次

    参考《你不知道的JavaScript上卷》中的this部分
    参考文章 关于JavaScript中this的软绑定

    1. JavaScript中的this

    1.1 关于this的误解
    1. this是指向函数自身的
    function foo(num) {
        console.log("foo: " + num);
        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
    console.log(foo.count); //0
    console.log(count); //NaN
    

           执行foo.count=0时,的确向函数对象foo添加了一个属性count,但由于foo函数内部代码中的this不是指向函数自身的,所以this.count不等于foo.count,虽然函数名相同,根对象并不相同。而函数内部的this.count会创建一个全局变量,且值为NaN。

    1. this指向函数的作用域
    function foo(){
        var a=2;
        this.bar();
    }
    function bar(){
        console.log(this.a);
    }
    
    foo(); //undefined
    

           bar()无法访问foo()作用域里的变量a,使用this不可能在词法作用域中查到什么。

    总结:
    1.this既不指向函数自身也不指向函数的词法作用域;
    2.this是在函数被调用时发生的绑定,它的绑定和函数声明的位置没有关系,它指向什么完全取决于函数在哪里被调用。


    1.2 调用位置

           调用位置就是函数在代码中被调用的位置,而不是声明的位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就是当前正在执行的函数的前一个调用中。

    function first(){
        //当前调用栈是:first
        //当前调用位置是全局作用域
        
        second();
    }
    
    function second(){
        //当前调用栈是:first -> second
        //当前调用位置是first
    }
    
    first();
    

    1.3 绑定规则
    1. 默认绑定
             独立函数调用。(可把这条规则看作是无法应用其他规则时的默认规则)
             在foo()函数内部中,如果使用严格模式,则默认绑定不能绑定到全局对象,而绑定到undefined。
             在严格模式下调用foo()则不影响绑定。
    //在foo()函数内部中,如果使用严格模式
    function foo(){
        "use strict";
        
        console.log(this.a);
    }
    var a=2;
    foo(); //TypeError: Cannot read property 'a' of undefined
    
    //在严格模式下调用foo()
    function foo(){
        console.log(this.a);
    }
    var a=2;
    (function(){
        "use strict";
        foo(); //2
    })(); //将脚本文件包裹在一个自执行的匿名函数中,这样相当于所有语句都在一个严格模式的函数中执行
    
    1. 隐式绑定
             当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。(在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象中)
             对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
    function foo(){
        console.log(this.a); //因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
    }
    var obj = {
        a:2,
        foo:foo
    };
    
    obj.foo(); //2
    
    function foo(){
        console.log(this.a);
    }
    var obj2 = {
        a:42,
        foo:foo
    };
    var obj1 = {
        a:2,
        obj2:obj2
    };
    
    obj1.obj2.foo(); //42
    
    • 一个常见的this绑定问题是隐式绑定的函数会丢失绑定对象,从而应用默认绑定。(即隐式丢失)
    //bar是obj.foo的一个引用,但是它引用的只是foo函数本身,即只是把foo函数内容赋值给bar。
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2,
        foo:foo
    };
    var bar=obj.foo;
    var a="global";
    bar(); //global
    
    //回调函数
    //参数传递其实就是一种隐式赋值,即fn = obj.foo,同理只是把foo函数内容赋值给fn
    function foo(){
        console.log(this.a);
    }
    function doFoo(fn){
        fn();
    }
    var obj = {
        a:2,
        foo:foo
    };
    var a="global";
    doFoo(obj.foo); //global
    
    //把函数传入语言内置的函数,结果一样
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2,
        foo:foo
    };
    var a="global";
    setTimeout(obj.foo, 100); //global
    
    1. 显式绑定
             使用call()和apply()方法:第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2
    };
    foo.call(obj); //2
    //通过foo.call(),可以在调用foo时强制把它的this绑定到obj上。
    
    • 通过硬绑定(显式绑定的一个变种)解决丢失绑定问题
      硬绑定的bar不可能再修改它的this
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2
    };
    var bar=function(){
        foo.call(obj); //强制把foo的this绑定到了obj
    }
    
    bar(); //2
    setTimeout(bar, 100); //2
    
    bar.call(window); //2,没办法修改this
    

    硬绑定的主要使用方法是:
    1.创建一个包裹函数,负责接收参数并返回值;
    2.创建一个可以重复使用的辅助函数
           由于硬绑定是一种常用的模式,所以ES5提供了内置的方法bind(),会把指定的参数设置为this的上下文并调用函数使用指定的this。

    1. new绑定
             使用new来调用函数,会自动执行下面的操作:
      1.创建一个全新的对象。
      2.这个新对象会被执行[[Prototype]]连接。
      3.这个新对象会绑定到函数调用的this。
      4.如果函数没有返回其他对象,则自动返回这个新对象。
    function foo(num){
        this.a=num;
    }
    var bar=new foo(2); //这里会构造一个新对象并把它绑定到foo()调用中的this上
    console.log(bar.a); //2
    

    如果函数里面返回一个对象,则this会绑定到返回的这个对象中。若返回一个非对象类型数据,则不会出现这样的情况,this仍会绑定到新对象。

    function foo(num){
        this.a=num;
        return {a:200};
    }
    var bar=new foo(2); 
    console.log(bar.a);  //200
    
    • this绑定的四条规则的优先级
      1.new绑定:this绑定的是新创建的对象(var bar = new foo()
      2.显式绑定:this绑定的是指定的对象(var bar = foo.call(obj2)
      3.隐式绑定:this绑定的是那个上下文对象(var bar = obj1.foo()
      4.默认绑定:严格模式下绑定到undefined,否则绑定到全局对象

           在new中使用硬绑定,主要目的是预先设置函数的一些函数,这样在使用new进行初始化时就可以只传入其余的参数。

    function foo(p1,p2,p3){
        this.val=p1+p2+p3;
    }
    var bar=foo.bind(null, "p1"); //使用null是因为无需关心硬绑定的this是什么,因为使用new时this会被修改
    var baz=new bar("p2","p3"); //通过new把this绑定到baz上
    console.log(baz.val); //"p1p2p3"
    

    1.4 绑定例外
    • 把null和undefined作为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则
             常用的做法是使用apply()来展开一个数组并当做参数传入一个函数、bind()对参数进行柯里化(预先设置一些参数)。
             这些方法都需要传入一个参数作为this的绑定对象,如果函数不关心this的话,可以传入null作为一个占位值。
    function foo(a,b){
        console.log("a: "+a+", b: "+b);
    }
    
    //把数组展开成参数
    foo.apply(null, [2,3]); //a: 2, b: 3
    
    //使用bind()进行柯里化
    var bar=foo.bind(null, 2);
    bar(3); //a: 2, b: 3
    

           但总是使用null来忽略this绑定可能会产生一些副作用,因此更安全的做法是传入一个特殊的对象,比如创建一个DMZ对象(即一个空的非委托的对象)。
    创建一个空对象最简单的方法是Object.create(null)
           Object.create(null){}很像,但是并不会创建Object.prototype这个委托,因此比{}更空。

    • 间接引用,会应用默认绑定规则
    function foo(){
        console.log(this.a);
    }
    var a=2;
    var o={a:3, foo:foo};
    var p={a:4};
    
    o.foo(); //3
    (p.foo=o.foo)(); //2
    

           从1.3的隐式丢失问题中可以了解到,赋值表达式p.foo=o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),因此会应用默认绑定。

    • 软绑定(给默认绑定指定一个全局对象和undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力)
             下面的代码,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外这段代码还支持可选的柯里化。
    if(!Function.prototype.softBind){
        Function.prototype.softBind = function(obj){
            var fn=this;
            
            var args=[].slice.call(arguments, 1);
            var bound=function(){
                return fn.apply((!this||this===(window||global))?obj:this, args.concat.apply(args,arguments));
            };
            bound.prototype=Object.create(fn.prototype);
            return bound;
        };
    }
    
    
    function foo(){
        console.log("name: "+this.name);
    }
    
    var obj={name: "obj"};
    var obj2={name: "obj2"};
    var obj3={name: "obj3"};
    
    var fooOBJ=foo.softBind(obj);
    fooOBJ(); //name: obj
    
    obj2.foo=foo.softBind(obj);
    obj2.foo(); //name: obj2
    
    fooOBJ.call(obj3); //name: obj3
    
    setTimeout(obj2.foo, 10); //name: obj
    
    1. 首先先看[].slice.call(arguments, 1)这个写法。因为arguments是一个对象,不是真正的数组对象,只是与数组类似,所以不具有slice()方法。所以这条语句的过程就是先将传入进来的第一个参数转为数组,再调用slice()方法(即通过call显式绑定来实现arguments变相有slice()方法)。
    2. 再看(! this || this === (window || global)) ? obj : this,这条语句的作用是判断函数的调用位置,即它的this指向,将this绑定到现在正在指向的函数(即隐式绑定或显式绑定)。
             对于fooOBJ();,调用时此处的this绑定到全局对象,则将this绑定到obj中,所以输出的结果是name: obj
             对于obj2.foo();,调用时此处的this绑定到obj2中,所以this不变,输出name: obj2,此处相当于隐式绑定。
             同理,对于fooOBJ.call(obj3);,调用时此处的this绑定到obj3,相当于显式绑定。
             对于setTimeout(obj2.foo, 10);,由1.3的隐式丢失问题中可以了解到回调函数相当于一个隐式的传参,如果没有软绑定的话,这里将会应用默认绑定将this绑定到全局环境上,但有软绑定,所以这里this还是指向obj。
    3. 可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。
    4. 此代码还支持柯里化。apply()传入的第二个参数args.concat.apply(args,arguments)就是运行 foo 所需要的参数,由上面的args(外部参数)和内部的arguments(内部参数)连接成。下面代码就是实现柯里化的例子。
    function add(a,b){
        return this.num+a+b;
    }
    var obj={num: 1};
    var func=add.softBind(obj, 2);
    func(3); //6
    

    2. ES6中的this

           ES6的箭头函数无法使用之前所说的四条规则,它是根据外层(函数或者全局)作用域来决定this。具体来说,箭头函数会继承外层函数调用的this绑定。

    function foo(){
        return (a)=>{
            console.log(this.a);
        };
    }
    var obj1={a:1};
    var obj2={a:2};
    
    var bar=foo.call(obj1);
    bar.call(obj2); //1
    

           foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,且箭头函数的绑定无法被修改,new也不行,所以bar.call(obj2);语句并没有改变bar的this绑定。

           箭头函数最常用于回调函数,例如事件处理器或者定时器,下面是不使用箭头函数和使用箭头函数的结果。

    var a=200;
    function foo(){
        setTimeout(function display(){
            console.log(this.a); //回调函数丢失this绑定
        }, 100);
    }
    var obj={a:2};
    foo.call(obj); //200
    
    var a=200;
    function foo(){
        setTimeout(()=>{
            console.log(this.a); //this在词法上继承自foo(),foo()的this指向obj。
        }, 100);
    }
    var obj={a:2};
    foo.call(obj); //2
    

    相关文章

      网友评论

          本文标题:this

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