美文网首页技术干货JavaScript 进阶营程序员
简析JavaScript中的最复杂的机制之一——this

简析JavaScript中的最复杂的机制之一——this

作者: GeniusFunny | 来源:发表于2017-10-05 23:52 被阅读0次

    this是JavaScript中最复杂的机制之一,被自动定义在所有函数的作用域中。“When a function of an object was called , the object will be passed to the execution context as 'this' value.”当一个函数被调用时,拥有这个函数的对象会作为this传入,所以this可以是全局对象,当前对象乃至任何对象
    例如:

    function f(){
        var name = "Funny";
        console.log(this.name);  //undefined
        console.log(this);   //window
    }
    f();    //f();与window.f();效果相同
    

    调用函数f,因为函数f是最外层的函数,所以它拥有全局作用域,换句话说,它是全局对象(window)的一个方法(全局变量是全局对象的属性或方法)。console.log(this.name),程序执行到这里时,会对全局作用域进行RHS查询名为name的变量,由于不存在,所以控制台输出‘undefined’,console.log(this),输出全局对象window。
     为什么要用this这个关键字?倘若我们不用this:

    var me = {
        name: "Kyle"
    };
    var you = {
        name: "Reader"
    };
    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,I'm KYLE
    

    如果不用this来传递上下文对象,而采用显式传递上下文对象,这会让代码变得越来越混乱,尤其当使用模式越来越复杂的时候(上面的例子代码特别简单)。函数自动引用合适的上下文对象十分重要:

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

    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
    

    按道理,count是计算函数foo调用的次数,foo.count = 0的确为函数对象foo添加一个名为count的属性,但是foo函数内部的this.count中的this并非指向foo函数对象,而是指向window(全局对象)。在非严格模式下,this.count++;,会对count进行LHS查询,然而并没有在全局作用域中找到,所以就创建一个名为count的全局变量(严格模式下,程序会抛出引用异常);随着函数foo的调用,全局对象的属性count就递增,而函数对象的属性count则不变化,当然,console.log(count);会输出4。
    如果要从函数对象内部引用它本身,那么只使用this是不够的。我们可以强制this指向foo函数本身:

    function foo(num){
        console.log("foo: " + num);
        this.count++;
    }
    foo.count = 0;
    var i;
    for(i = 0; i < 10; i++){
        if(i > 5){
            foo.call(foo,i);
        }
    }
    console.log(foo.count);     //4
    

    大多数情况下this并非指向函数的作用域
    this在任何情况下都不会指向函数的词法作用域,作用域和对象类似,可见的标识符都是它的属性,但是作用域无法通过JS代码访问,它存在于JS引擎内部。下面是个经典的错误例子:

    function foo(){
        var a = 2;
        this.bar();
    }
    function bar(){
        console.log(this.a);
    }
    foo();  //ReferenceError: a is not defined
    

    这段代码首先通过this.bar()来引用函数bar(意外的成功了),通常省略前面的this,此外,开发者还试图用this联通foo()和bar()的词法作用域,使得bar()可以访问foo()作用域中的变量a,当然这是不可能的。至于为什么抛出这样一个异常,这里就不再赘述了。
    this到底是一样什么样的机制???
    this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。(动态作用域的定义瞬间浮现在脑海里)。当一个函数被调用时,会创建一个活动记录(有时也叫执行上下文)。这个纪录会包含函数在哪里被调用(调用栈)、函数调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

    调用位置

    要理解this的绑定过程,首先要理解调用位置。


    D86C4F31-669D-4A41-BFFF-8A7A4647B25D.png

    下面的例子中的注释就分析了调用栈,显然易懂,不再赘述。

    function baz(){
        //当前调用栈:baz
       //当前调用位置:全局作用域
       console.log("baz");
       bar();// <-- bar的调用位置
    }
    function bar(){
        //当前调用栈:baz->bar
        //当前调用位置:baz
        console.log("bar");
        foo(); <--foo的调用位置
    }
    function foo(){
        //当前调用栈:baz->bar->foo
        //当前调用位置:bar
        console.log("foo");
    }
    baz(); <-- baz的调用位置
    

    绑定规则

    首先找到调用位置,然后判断需要按照哪条绑定规则进行应用,如果多条规则都适用,则按优先级。

    默认绑定

    最常见的函数调用类型:独立函数调用

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

    函数foo调用时应用了默认绑定,this指向了全局对象window。foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能应用默认绑定严格模式下,则不能将全局对象用于默认绑定,因此this会被绑定到undefined

    隐式绑定

    调用位置是否有上下文对象?是否被某个对象拥有或者包含? function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); //2 无论直接在obj中定义还是先定义在添加为引用属性,函数foo严格上来说都不属于这个对象,但是在函数被调用的时候,可以认为obj拥有或者包含这个函数,调用位置会使用obj上下文来引用函数。当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象。所以调用foo()时,this被绑定到obj,因而这里的this.a和obj.a就是一样的。对象属性引用链只有上一层在调用位置起作用
    被隐式绑定的函数会丢失绑定对象,然后它应用默认绑定(非严格模式下)。

    function foo(){
        console.log(this.a);
    }
    function doFoo(fn){
        //fn其实引用的是foo
        fn();
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = "oops,global";
    doFoo(obj.foo); //“oops,global"
    

    参数传递是一种隐式赋值,所以这种会造成隐式丢失(显式赋值也会造成隐式丢失)。

    显式绑定

    使用函数的call(...)和apply(...)方法。它们的第一个参数是对象,显然是为this准备的,在调用函数时,将它绑定到this。

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

    显式绑定一样无法解决之前的丢失绑定问题,但显式绑定的变种可以解决这个问题。

    硬绑定
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a;2
    };
    var bar = function(){
        foo.call(obj);
    };
    bar();  //2
    setTimeout(bar,100);    //2
    //硬绑定的bar是无法再修改它的this
    bar.call(window);   //2
    

    首先我们创建了一个函数bar(),并在它的内部调用了foo.call(obj),所以我们强制把foo的this绑定到了obj。之后无论怎么调用函数bar,它都会再一次手动地在obj上调用foo。硬绑定是一种非常常用的内置方法。ES5提供了内置的方法Function.prototype.bind:

    function foo(something){
        console.log(this.a,something);
        return this.a + something;
    }
    var obj = {
        a: 2
    };
    var bar = foo.bind(obj);
    var b =  bar(3);    //2 3;
    console.(b);    //5
    
    new绑定

    使用new来调用函数,会自动执行如下操作:
    1、构造一个新对象
    2、这个新对象会被执行[[Protorype]]连接
    3、这个新对象会被绑定到函数调用的this
    4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

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

    使用new来调用函数foo,我们会构造一个新对象并把它绑定到foo调用中的this上。

    优先级

    正常情况:new绑定>显式绑定>隐式绑定>默认绑定
    特殊情况:当把null或者undefined作为绑定对象传入call、apply、bind,这些操作会被忽略,最后应用默认绑定。创建一个函数的“间接引用”,即前面提的绑定丢失,最终也是应用默认绑定。

    软绑定

    给默认绑定指定一个全局对象和undefined以外的值,那么就可以实现和硬绑定一样的效果,同时保留隐式绑定或显示绑定修改this的能力。

    if(!Function.prototype.softBind){
        Function.prototype.softBind = function(obj){
            var fn = this;
            //捕获所以curried参数
            var curried = [].slice.call(arguments,1);
            var bound = function(){
                return fn.apply(
                    (!this || this === (window || global))?
                        obj:this,
                    curried.concat.apply(curried,arguments)
                );
            };
            bound.prototype = Object.create(fn.prototype);
            return bound;
        };
    }
    

    首先检查调用时的this,如果this绑定到全局对象或undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。

    ES6中的新玩法

    箭头函数不是用function关键字定义的,而是用操作赋 => 定义的,这是ES6中新增的语法糖之一。箭头函数不适用于this的绑定规则,而是根据外层作用域来决定this,无论最外层绑定到了什么,它都会继承下来。
    首先箭头函数的词法作用域:

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

    foo()内部的箭头函数会捕获调用foo()时的this,这里是obj1,所以bar的this绑定到了obj1,箭头函数的绑定无法修改。
    箭头函数常用于回调函数中,例如事件处理器或定时器:

    function foo(){
        setTimeout( () => { //这里的this在此法上继承自foo()
            console.log(this.a);
        },100);
    }
    var obj = {
        a:2
    };
    foo.call(obj);  //2
    

    相关文章

      网友评论

        本文标题:简析JavaScript中的最复杂的机制之一——this

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