美文网首页浓缩解读前端系列书籍一周一章前端书
一周一章前端书·第7周:《你不知道的JavaScript(上)》

一周一章前端书·第7周:《你不知道的JavaScript(上)》

作者: 梁同学de自言自语 | 来源:发表于2017-11-11 22:49 被阅读3次

    第2章:this全面解析

    2.1 调用位置

    • 在理解this的绑定之前,首先要理解“函数调用的位置”,即函数在代码中被调用的位置。只有仔细分析了调用位置才能解释,函数中的this到底引用的是什么?
    • 寻找“函数被调用的位置”,其实并没有想象中的简单,因为JS是很灵活的语言,经常将函数也作为参数进行传递,可能会隐藏真正的调用位置。
    • 所以我们需要分析函数的调用栈。所谓调用栈,就是为了到达当前执行位置,所调用过的所有函数,所以我们可以把调用栈想象成一个函数调用链,举例说明:
    function baz(){
        //当前位置是baz
        console.log("my name is baz");
        //baz中调用bar
        bar();
    }
    
    function bar(){
        //当前位置是bar
        console.log("my name is bar");
        //bar中调用foo
        foo();
    }
    
    function foo(){
        //当前位置是foo
        console.log("my name is foo");
    }
    
    //window下调用baz
    baz();
    
    /* 
     * 所以foo的调用栈(链)就是:
     * window -> baz -> bar -> foo
     * /
    

    2.2 绑定规则

    • 通过函数的调用位置,并应用JavaScript中四条决定this绑定的规则,就能分析this的引用值了。

    2.2.1 默认绑定

    • 首先是最常用的函数调用类型:独立函数调用。所谓独立函数调用,就是没有应用其他规则的默认调用规则。举例:
    var a = 2;
    function foo(){
        console.log(this.a);   
    }
    foo();  //输出 2
    
    • 当调用foo()函数时,this.a指向了全局变量a,因为在默认绑定下,this指向全局对象。
    • 那如何辨别这里应用的是默认绑定呢?这时候,就需要运用我们前面讲的“分析函数调用位置”了,在这段代码中,foo()函数是直接调用的,不带任何修饰,也不被任何函数包含,所以可以确定是默认绑定。

    注意:如果 函数内使用严格模式(strict mode) ,是不能将全局对象用于默认绑定的,最终this会绑定到undefined上。举例说明:

    var a = 2;
    function foo(){ 
      "use strict";
      console.log(this.a);
    }
    foo();    //输出 TypeError : this is undefined
    

    在严格模式下调用函数 ,则不影响默认绑定。举例说明:

    var a = 2;
    function foo(){
      console.log(this.a);
    }
    (function(){
      "use strict";
      foo():  // 输出2
    )();
    

    由于我们可能会使用众多第三方库,所以代码中可能会混合使用strict模式和非strict模式,因此一定要注意这类的兼容性问题。

    2.2.2 隐式绑定

    • 第二条规则,就是通过函数调用位置,函数是否属于某个对象的属性。
    var obj = {
        a : 2,
        foo : foo
    }
    
    function foo(){
        console.log(this.a);
    }
    
    obj.foo();  // 输出 2
    
    • 你看foo()方法的声明方式,它是被当做引用属性添加到了obj对象中,这种情况下,obj对象拥有/包含了foo()方法。
    • 当函数有包含自己的对象时,隐式绑定规则会把this绑定到这个对象。
    • 因此,调用foo()时,this被绑定到了obj对象,在函数中this.aobj.a的引用是一样的。
    • 值得注意的是,如果是多层嵌套对象下的函数,就只在最后一层中起作用。举例:
    function foo(){
        console.log(this.a);
    }
    
    var obj1 = {
        a : 2,
        obj2 : {
            a : 42,
            foo : foo
        }
    }
    
    obj1.obj2.foo();    //输出 42
    
    • 值得注意的是,如果将obj1.obj2.foo函数的引用赋值给另一个变量,然后以默认绑定的方式调用函数,不管是自定义的函数,还是JS的内置函数,则还是会应用默认绑定规则:
    var a = 'oops,global';
    var bar = obj1.obj2.foo;
    
    function runFoo(){
        obj1.obj2.foo();
    }
    
    // 都是输出 'oops,global'
    bar();  
    runfoo();   
    setTimeout(obj1.obj2.foo,100);
    

    2.2.3 显示绑定

    • 如果不想在对象内部包含函数引用,想在某个对象上强制调用函数,该怎么做呢?
    • JavaScript中的函数都有一些特性,可以用来解决这个问题。比如函数的call()applay()方法
    • 这两个方法,传入的第一个参数是一个对象,就是留给this准备的,调用时会将其绑定到this。因为可以直接指定this的绑定对象,因此我们称之为显示绑定。
    function foo(){
        console.log(this.a);
    }
    var obj = {
        a : 2
    };
    foo.call(obj);  // 输出 2
    
    • 但如果传入的参数是原始值(字符串、布尔或者数值类型)当做this的绑定对象的话,这个原始值会被转换成它的对象形式。也就是new String()new Boolean()或者new Number(),这个过程通常叫做装箱
    • 1. 硬绑定
    function foo(){
        console.log(this.a);
    }
    
    var obj = {
        a : 2
    };
    
    var bar = function(){
        foo.call(obj);
    };
    
    bar();  // 2
    setTimeout(bar,100);
    
    //硬绑定不能再修改它的this
    bar.call(window);   // 2
    
    • 函数bar()在它内部手动调用了foo.call(obj),强制把foothis绑定到了obj。无论之后如何调用函数bar,总会手动在obj上调用foo。这种显式的强制绑定,称之为硬绑定。
    • 硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:
    function foo(something){
        console.log(this.a,something);
        return this.a + something;
    }
    var obj = {
        a : 2
    }
    var bar = function(){
        return foo.apply(obj,arguments);
    };
    
    
    var b = bar(3); // 2 3
    console.log(b); // 5
    
    • 另一种方式就是创建一个可以重复使用的辅助函数:
    function foo(something){
        cosnole.log(this.a,something);
        return this.a + something;
    }
    function bind(fn,obj){
        return function(){
            return fn.apply(obj,arguments);
        }
    }
    var obj = {
        a : 2
    }
    var bar = bind(foo,obj);
    var b = bar(3);    // 2 3
    console.log(b);    // 5
    
    • 由于硬绑定是一种非常常用的模式,所以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.log(b);    // 5
    
    • bind()会返回一个硬编码的新函数,它会把你指定的参数设置为this 的上下文,并调用原始函数。
    • 2. API调用的“上下文”
    • 许多函数都提供了一个可选的参数,其作用和bind()函数一样,确保你的回调函数使用指定的this。举例:
    function foo(el){
        console.log(el,this.id);
    }
    var obj = {
        id : 'awesome'
    }
    [1,2,3].forEach(foo,obj);
    
    • 通过call()apply()实现显示绑定,可以少写代码。

    2.2.4 new绑定

    • 在讲解最后一条this的绑定规则之前,首先要澄清一个常见的关于JavaScript中函数和对象的误解。
    • 在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类的构造函数,something = new MyClass()。然而,JavaScript中的new的机制实际上和面向类的语言完全不同。
    • 我们重新定义一些JavaScript中的“构造函数”:在JavaScript中,构造函数只是使用new操作符时被调用的函数,它们并不属于某一个对象,也不会实例化一个类。
    • 所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上,并不存在所谓的“构造函数”,只有对函数的“构造调用”。
    • 使用new来调用函数,会自动执行下面的操作:
      1. 创建一个全新的对象
      2. 这个新对象会被执行[[Prototype]]连接
      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绑定。

    2.3 优先级

    • 上文通过大篇幅讲了函数调用中,this绑定的四条规则:默认绑定、隐式绑定、显示绑定和new绑定。但如果调用应用了多条规则就必须给这些规则设定优先级了。
    • 毫无疑问,默认绑定的优先级是最低的,暂不考虑它。
    • 隐式绑定和显示绑定哪一个优先级更高?我们来测试一下:
    function foo(){
        console.log(this.a);
    }
    var obj1 = {
        a : 2,
        foo : foo
    }
    var obj2 = {
        a : 3,
        foo : foo
    }
    
    obj1.foo(); //2
    obj2.foo(); //3
    
    obj1.foo.call(obj2);    // 3
    obj2.foo.call(obj1);    // 3
    
    • 可以看到,显式绑定优先级更高。
    • 接下来,我们要测试,new绑定和隐式绑定的优先级谁高谁低:
    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
    
    var bar = new obj1.foo(4);
    console.log(obj1.a);    //2
    console.log(bar.a);    //2
    
    • 可以看到new绑定比隐式绑定的优先级更高,obj1.a的值一直没改变。
    • 那new绑定和显示绑定,谁的优先级更高呢?(由于new和call/apply无法一起使用,所以通过硬绑定来测试)
    function foo(something){
        this.a = something;
    }
    var obj1 = {};
    
    var bar = foo.bind(obj1);
    bar(2);
    console.log(obj1.a);    //2
    
    var baz = new bar(3);
    console.log(obj1.a);    // 2
    console.log(baz.a);     // 3
    
    • 观察输出的结果,bar被硬绑定到了obj1上,但new baz(3)并没有把obj1.a修改为3.
    • 话说回来,之所以在new中使用硬绑定函数,主要目的是想预先设置一些参数,这样在使用new进行初始化时就可以传入其他参数了。举例:
    function foo(p1,p2){
        this.val = p1 + p2;
    }
    
    var bar = foo.bind(null,"p1");
    var baz = new bar("p2");
    baz.val;    //p1p2
    
    • 根据优先级就能判断函数调用时应用的是哪条规则了,判断的步骤:
      1. 函数是否进行了new调用,如果是的话,this绑定的是新创建的对象;
      2. 函数是否通过callapply或者硬绑定调用,如果有的话,this绑定的是指定的对象;
      3. 函数是否在某个对象中调用,如果是的话,this绑定的是该对象;
      4. 如果都不是的话,使用默认绑定,绑定到全局对象。(严格模式下,绑定到undefined)

    2.4 绑定例外

    2.4.1 被忽略的this

    • 把null或者undefined作为this传入call、apply或者bind,这些值在函数调用时会被忽略,应用默认的绑定规则:
    function foo(){
        console.log(this.a);
    }
    var a = 2;
    foo.call(null); // 2
    
    • 什么场景下会传入null呢?比如使用apply来遍历输出一个数组,或者通过bind()进行柯里化:
    function foo(a,b){
        console.log('a:'+a+'b:'+b);
    }
    
    foo.apply(null,[2,3]); // a:2,b:3
    
    //先预先传入参数a
    var bar = foo.bind(null,2);
    //调用时再传入参数b
    bar(3); // a:2,b:3
    
    • 但这种方式可能会导致许多难以分析和追踪的bug,我们可以用更安全的方式。
    • 更安全的做法就是不传入null,而是传入一个空的对象,把this绑定到这个对象,就好像创建一个非军事区的隔离对象一样,以确保不会对你的程序产生任何副作用。
    function foo(a,b){
        console.log('a:'+a+'b:'+b);
    }
    
    var o = Object.create(null);
    foo.apply(o,[2,3]); // a:2,b:3
    
    //先预先传入参数a
    var bar = foo.bind(o,2);
    //调用时再传入参数b
    bar(3); // a:2,b:3
    
    • 我们通过Object.create(null)来创建对象,它和直接以字面量{}创建对象很相似,但前者不会创建Object.prototype的委托,所以它比{}更空。

    2.4.2 间接引用

    • 另一个需要注意的是,可能会有意无意的创建一个函数的“间接引用”,而在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生:
    function foo(){
        console.log(this.a);
    }
    
    var a = 2;
    var o = {
        a : 3,
        foo : foo
    }
    var p = { 
        a : 4
    }
    
    o.foo(); // 3
    (function(){
        p.foo = o.foo
    })();   // 2
    

    2.4.3 软绑定

    • 硬绑定可以把this强制绑定到指定的对象,以防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,硬绑定之后,就无法使用隐式绑定或者显示绑定来修改this。
    if(!Function.prototype.softbind){
        Function.prototype.softbind = function(obj){
            var fn = this;
            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;
        }
    }
    
    function foo(){
        console.log('name:' + this.name);
    }
    
    var obj = {
            name : "obj"
        },
        obj2 = {
            name : "obj2"
        },
        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
    
    

    2.5 this词法

    • 前面解说的四条规则几乎也包含所有函数,但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 , 不是 3 !
    
    • 箭头函数最常用于回调函数中,例如事件处理器或者定时器:
    function foo(){
        setTimeout(() => {
            console.log(this.a);
        },100);
    }
    var obj = {
        a : 2
    };
    foo.call(obj); // 2
    

    2.6 小结

    • 如果要判断一个运用中函数的this绑定,需要找到函数的调用位置,然后按顺序应用四条规则来判断this的绑定对象:
      1. 由new调用?绑定到新创建的对象;
      2. 由call或者apply/bind调用?绑定到指定的对象;
      3. 由对象调用?绑定到那个对象;
      4. 默认:严格模式下绑定到undefined,否则绑定到全局对象;
    • 有些调用无意中使用默认绑定规则。如果想“更安全” 地忽略this绑定,可以使用一个空的临时对象,比如o = Object.create(null),以保护全局对象。
    • ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。

    相关文章

      网友评论

        本文标题:一周一章前端书·第7周:《你不知道的JavaScript(上)》

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