JavaScript 中的 this

92 阅读3分钟

为什么要使用 this

函数间使用标识符,需要考虑上下文对象,我们可以在每个函数中都设置一个参数,通过这个表示上下文对象的参数,来使用对应的值。

然而,this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。

        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

        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

this 的机制

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

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录的一个属性,会在函数执行的过程中用到。

调用位置

在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是被声明的位置)。

默认绑定、隐式绑定和显式绑定

当代码中的函数是直接使用不带任何修饰的函数引用进行调用的,就只能使用默认绑定,无法应用其他规则。默认绑定指的是this默认指向调用这个方法的上下文环境。

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

        foo(); // 2

当函数引用由上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

        function foo() {
            console.log(this.a);
        }

        var obj2 = {
            a: 42,
            foo: foo
        };

        var obj1 = {
            a: 2,
            obj2: obj2
        };

        obj1.obj2.foo(); // 42
        obj1.foo(); // 2

隐式绑定会有许多种意想不到的“特殊”情况,例如:声明对象内部函数,却在全局中调用声明变量时,对象内部函数的 this 会指向全局上下文;回调函数中调用的外部方法,外部方法中的 this 指向的却是回调函数内部的上下文。

        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"

我们也可以使用 fn.call(ctx) 或者 fn.apply(ctx) 实现显式绑定。显示绑定会将fn函数内部的this指针强制指向ctx上下文

        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

通常情况下,我们可以使用 fn.call(null)来取消上下文的指定,但是使用null还是不太安全,因为null并不是完全的“空”。我们可以通过 Object.create(null)来创建一个完全的空,Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}“更空”

在多个call或者apply嵌套调用的情况下,ctx使用的是最内层的call或者apply所赋予的上下文

        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!