美文网首页
全面剖析javascript中的this

全面剖析javascript中的this

作者: 沧澈 | 来源:发表于2020-12-22 10:56 被阅读0次

    1.为什么要用this

    this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

    通过代码解释为什么要使用this:

    function identify() {
        return this.name.toUpperCase();
    }
    
    function speak() {
        var greeting = 'Hello ' + identify.call(this);
        console.log(greeting);
    }
    
    var person1 = {
        name: 'Jack'
    }
    var person2 = {
        name: 'Jane'
    }
    
    identify.call(person1);
    identify.call(person2);
    
    speak.call(person1); //Hello JACK
    speak.call(person2); //Hello JANE
    

    这段代码可以在不同的上下文对象(person1和person2)中重复使用函数identify()和speak(),不用针对每个对象编写不同版本的函数。

    如果不使用this,那就需要给identify()和speak()显式传入一个上下文对象。

    function identify(context) {
        return context.name.toUpperCase();
    }
    
    function speak(context) {
        var greeting = "Hello " + identify(context);
        console.log(greeting);
    }
    
    var person1 = {
        name: 'Jack'
    }
    
    var person2 = {
        name: 'Jane'
    }
    
    identify(person1);
    identify(person2);
    
    
    speak(person1); //Hello JACK
    speak(person2); //Hello JANE
    

    2.this的常见误解

    太拘泥于“this”的字面意思就会产生一些误解。

    • 误解1:容易把this理解成指向函数自身
    function foo(num) {
        console.log('foo :' + num);
        this.count++;
    }
    
    foo.count = 0;
    
    for (var 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语句产生了4条输出,证明foo(..)确实被调用了4次,

    但是foo.count仍然是0。显然从字面意思来理解this是错误的。

    t修改his指向foo函数对象:

    function foo(num) {
        console.log('foo :' + num);
        this.count++;
    }
    
    foo.count = 0;
    
    for (var i = 0; i < 10; i++) {
        if (i > 5) {
            foo.call(foo,i);
        }
    }
    
    // foo :6
    // foo :7
    // foo :8
    // foo :9
    
    console.log(foo.count); // 4
    
    • 误解2:this指向函数的作用域

    需要明确的是,this在任何情况下都不指向函数的词法作用域。

    思考一下下面的代码,它试图(但是没有成功)跨越边界,使用this来隐式引用函数的词法作用域:

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

    这段代码中的错误不止一个。
    首先,这段代码试图通过this.bar()来引用bar()函数。

    此外,编写这段代码的开发者还试图使用this联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里的变量a。这是不可能实现的,使用this不可能在词法作用域中查到什么。


    3.this到底是什么

    this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。

    this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。

    这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。

    this就是这个记录的一个属性,会在函数执行的过程中用到。

    this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。


    4.this调用位置

    • 调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

    通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。

    最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

    function baz() {
        // 当前调用栈是baz
        // 因此当前的调用位置是全局作用域
        console.log('baz');
        bar();
    }
    
    function bar() {
    
        // 当前调用栈是baz --> bar
        // 因此当前的调用位置是在baz 中
        console.log('bar');
        foo();
    }
    
    function foo(){
         // 当前调用栈是baz --> bar --> foo
        // 因此当前的调用位置是在bar 中
        console.log('foo');
    }
    
    baz(); // baz的调用位置
    

    5.绑定规则

    • 规则1:默认绑定

    首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

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

    函数调用时应用了this的默认绑定,因此this指向全局对象。

    可以通过分析调用位置来看看foo()是如何调用的。

    在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

    如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined:

    • 规则2:隐式绑定
    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 1,
        foo: foo
    }
    
    obj.foo(); //2
    

    首先需要注意的是foo()的声明方式,及其之后是如何被当作引用属性添加到obj中的。

    但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。

    然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。

    当foo()被调用时,它的前面确实加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的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 = 'global';
    
    bar(); 
    

    虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

    一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

    function foo() {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        foo: foo
    }
    
    
    function doFoo(fn) {
        fn();
    }
    
    var bar = obj.foo;
    
    var a = 'global';
    
    doFoo(obj.foo);
    

    参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,

    所以结果和上一个例子一样。

    • 规则3:显式绑定

    可以使用函数的call(..)和apply(..)方法。

    这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。

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

    硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:

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

    • 规则4:new绑定

    首先需要澄清一个非常常见的关于JavaScript中函数和对象的误解:

    在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。通常的形式是这样的:

    something = new MyClass();
    

    首先我们重新定义一下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是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。


    6.绑定规则的优先级

    • 显示绑定和隐式绑定的优先级比较

    测试代码:

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

    可以看到,显示绑定的优先级更高

    • 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); //4
    

    可以看到new绑定比隐式绑定优先级高.

    • new绑定和显式绑定优先级比较

    new和call/apply无法一起使用,因此无法通过new foo.call(obj1)来直接进行测试。

    可以使用硬绑定来测试它俩的优先级.

    Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。

    测试代码:

    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
    
    

    new修改了硬绑定(到obj1的)调用bar(..)中的this

    总结

    根据前面优先级的比较结果,可以按照以下顺序来判断:

    1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

    var bar = new foo(); 
    

    2. 函数是否通过call、apply(显式绑定)或者硬绑定(bind)调用?如果是的话,this绑定的是指定的对象。

    var bar = foo.call(obj2);
    

    3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    var bar = obj1.foo();
    

    4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

    var bar = foo();
    

    7.绑定例外

    规则总有例外,这里也一样。

    • 如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

    注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

    function foo() {
        console.log(this.a);
    }
    var a = 2;
    
    foo.call(null);  // 书中是2 ,自己尝试是undefined
    

    什么情况下你会传入null呢?

    (1) 一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。

    (2) 使用bind(..)可以对参数进行柯里化(预先设置一些参数)。

    (3) 函数并不关心this,需要传入一个占位值。

    function foo(a, b) {
        console.log(a, b); //2,3
    }
    
    foo.apply(null, [2, 3]);
    
    var bar = foo.bind(null, 2);
    bar(3); //2,3
    

    注:ES6中可使用...代替apply展开数组, foo(...[1,2])和foo(1,2)是一样的

    然而,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果(比如修改全局对象)。

    更安全的this

    一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用。

    就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized zone,非军事区)对象——它就是一个空的非委托的对象。

    使用Object.create(null)创建一个空的对象,Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}“更空”:

    代码如下:

    function foo(a, b) {
        console.log(a, b);
    }
    var o = Object.create(null);
    
    foo.apply(o, [2, 3]); //2,3
    
    var bar = foo.bind(o, 2);
    
    bar(3); //2,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 ,自己尝试是undefined(此处同上)
    
    • 软绑定

    之前我们已经看到过,硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则。

    问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this。

    如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改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(this.name);
    }
    
    var obj = {
            name: 'obj'
        },
        obj1 = {
            name: 'obj1'
        },
        obj2 = {
            name: 'obj2'
        };
    
    
    var fooOBJ = foo.softBind(obj);
    fooOBJ(); // obj
    
    obj2.foo = foo.softBind(obj);
    obj2.foo(); //obj2
    
    fooOBJ.call(obj2);
    
    setTimeout(obj2.foo, 10);
    
    

    可以看到,软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。


    8.箭头函数

    之前介绍的四条规则已经可以包含所有正常的函数。但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数

    箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用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。由于foo()的this绑定到obj1, bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

    • 箭头函数最常用于回调函数中,例如事件处理器或者定时器:
    function foo(){
        setTimeout(() => {
            console.log(this.a);
        }, 100);
    }
    
    var obj = {
        a: 2
    }
    
    foo.call(obj); //2
    

    在ES6之前使用的一种几乎和箭头函数完全一样的模式:

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

    9.总结

    • this的两个常见误解

      1.容易把this理解成指向函数自身

      2.this指向函数的作用域

    • this调用位置
    • this的四条绑定规则

      1.规则1:默认绑定

      2.规则2:隐式绑定

      3.规则3:显式绑定

      4.规则4:new绑定

    • 如何判断函数中this的指向,就需要找到这个函数的直接调用位置。

    1.由new调用?绑定到新创建的对象。

    2.由call或者apply(或者bind)调用绑定到指定的对象。

    3.由上下文对象调用?绑定到那个上下文对象。

    4.默认:在严格模式下绑定到undefined,否则绑定到全局对象。

    • 有些调用可能在无意中使用默认绑定规则, 可以使用一个DMZ对象。
    • ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的self = this机制一样。

    来源:整理自《你不知道的Javascript 上卷》

    相关文章

      网友评论

          本文标题:全面剖析javascript中的this

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