美文网首页程序员
你不知道的JavaScript(二)|this和对象原型

你不知道的JavaScript(二)|this和对象原型

作者: xpwei | 来源:发表于2017-09-29 11:19 被阅读28次

    this全面解析
    在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

    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 = 2;
    foo(); // 2
    

    在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
    如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this会绑定到undefined:

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

    这里有一个微妙但是非常重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;严格模式下与foo()的调用位置无关:

    function foo() {
        console.log(this.a);
    }
    var a = 2;
    (function () {
        "use strict";
        foo(); // 2
    })();
    
    • 隐式绑定
      另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。
    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    obj.foo(); // 2
    

    当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
    对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

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

    隐式丢失
    一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var bar = obj.foo; // 函数别名!
    var a = "oops, global"; // a 是全局对象的属性
    bar(); // "oops, global"
    

    虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
    一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

    function foo() {
        console.log(this.a);
    }
    function doFoo(fn) {
        // fn 其实引用的是foo
        fn(); // <-- 调用位置!
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = "oops, global"; // a 是全局对象的属性
    doFoo(obj.foo); // "oops, global"
    

    显示绑定
    用call和apply

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

    通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。
    如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通常被称为“装箱”。
    显示绑定仍然无法解决我们之前提出的丢失绑定问题。但是显示绑定的一个变种可以解决这个问题。

    • 硬绑定
    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。这种绑定是一种显示的强制绑定,因此我们称之为硬绑定。
    硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接受到的所有值:

    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) {
        console.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的上下文并调用原始函数。

    • API调用的“上下文”
      第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数指定的this。
    function foo(el) {
        console.log(el, this.id);
    }
    var obj = {
        id: "awesome"
    };
    // 调用foo(..) 时把this 绑定到obj
    [1, 2, 3].forEach(foo, obj);
        // 1 awesome 2 awesome 3 awesome
    

    这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样可以少写一些代码。

    new绑定
    包括内置对象函数(比如Number(..))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
    使用new来调用函数,或者说发生构造函数低啊用时,会自动执行下面的操作:

    • 创建(构建)一个全新的对象。
    • 这个新对象会被执行[[原型]]连接。
    • 这个新对象会绑定到函数调用的this。
    • 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
    function foo(a) {
        this.a = a;
    }
    var bar = new foo(2);
    console.log(bar.a); // 2
    

    以上代码使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。

    优先级
    显示绑定优先级高于隐式绑定
    new绑定比隐式绑定优先级高
    new绑定比硬绑定高
    为什么要在new中使用硬绑定函数呢?直接使用普通函数不是更简单吗?
    之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”):

    function foo(p1, p2) {
        this.val = p1 + p2;
    }
    // 之所以使用null 是因为在本例中我们并不关心硬绑定的this 是什么
    // 反正使用new 时this 会被修改
    var bar = foo.bind(null, "p1");
    var baz = new bar("p2");
    baz.val; // p1p2
    

    判断this
    根据优先级来判断函数在某个调用位置应用的是哪条规则:
    1、函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
    2、 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。var bar = foo.call(obj2)
    3、函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo()
    4、如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。var bar = foo()

    被忽略的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
    // 使用 bind(..) 进行柯里化
    var bar = foo.bind(null, 2);
    bar(3); // a:2, b:3
    

    这两种方法都需要传入一个参数当做this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择,就像代码所示的那样。
    在ES6中,可以用...操作符代替apply(..)来“展开”数组,foo(..[1,2])和foo(1,2)是一样的,这样可以避免不必要的this绑定。可惜,在ES6中没有柯里化的相关语法,因此还是需要使用bind(..)。

    更安全的this
    使用null来忽略this绑定可能产生一些副作用。一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用。在JavaScript中创建一个空对象最简单的方法都是Object.create(null)。Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}"更空":

    function foo(a, b) {
        console.log("a:" + a + ", b:" + b);
    }
    // 我们的DMZ 空对象
    var ø = Object.create(null);
    // 把数组展开成参数
    foo.apply(ø, [2, 3]); // a:2, b:3
    // 使用bind(..) 进行柯里化
    var bar = foo.bind(ø, 2);
    bar(3); // a:2, b:3
    

    间接引用
    另一个需要注意的是,你有可能会创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

    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
    

    赋值表达式p.foo=0.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者0.foo()。根据我们之前说过的,这里会应用默认绑定。

    this词法

    function foo() {
        // 返回一个箭头函数
        return (a) => {
            //this 继承自foo()
            console.log(this.a);
        };
    }
    var obj1 = {
        a: 2
    };
    var obj2 = {
        a: 3
    };
    var bar = foo.call(obj1);
    bar.call(obj2); // 2, 不是3 !
    

    foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)
    箭头函数最常用于回调函数中,例如事件处理器或者定时器:

    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/gyvuextx.html