`

[转载]JavaScript’s “this”: how it works, where it can trip you up

 
阅读更多

JavaScript’s “this”: how it works, where it can trip you up

In JavaScript, the special variable this is relatively complicated, because it is available everywhere, not just in object-oriented settings. This blog post explains how this works and where it can cause problems, concluding with best practices.

To understand this, it is best to partition the locations where it can be used into three categories:

  • In functions: this is an extra, often implicit, parameter.
  • Outside functions (in the top-level scope): this refers to the global object in browsers and to a module’s exports in Node.js.
  • In a string passed to eval(): eval() either picks up the current value of this or sets it to the global object, depending on whether it is called directly or indirectly.

Let’s examine each of these categories.

this in functions

That’s the most common way of using this, because functions represent all callable constructs in JavaScript, by playing three different roles:

  • Real functions (this is the global object in sloppy mode, undefined in strict mode)
  • Constructors (this refers to the newly created instance)
  • Methods (this refers to the receiver of the method call)

In functions, this can be thought of as an extra, often implicit, parameter.

this in real functions

In real functions, the value of this depends on the mode one is in:

  • Sloppy mode: this refers to the global object (window in browsers).
        function sloppyFunc() {
            console.log(this === window); // true
        }
        sloppyFunc();
    
  • Strict mode: this has the value undefined.
        function strictFunc() {
            'use strict';
            console.log(this === undefined); // true
        }
        strictFunc();
    

That is, this is an implicit parameter that is set to a default value (window or undefined). You can, however, make a function call via call() or apply() and specify the value of this explicitly:

    function func(arg1, arg2) {
        console.log(this); // a
        console.log(arg1); // b
        console.log(arg2); // c
    }
    func.call('a', 'b', 'c'); // (this, arg1, arg2)
    func.apply('a', ['b', 'c']); // (this, arrayWithArgs)

this in constructors

Functions become constructors if you invoke them via the new operator. That operator creates a new object and passes it to the constructor via this:

    var savedThis;
    function Constr() {
        savedThis = this;
    }
    var inst = new Constr();
    console.log(savedThis === inst); // true

Implemented in JavaScript, the new operator looks roughly as follows (a more accurate implementation is slightly more complex):

    function newOperator(Constr, arrayWithArgs) {
        var thisValue = Object.create(Constr.prototype);
        Constr.apply(thisValue, arrayWithArgs);
        return thisValue;
    }

this in methods

In methods, things are similar to more traditional object-oriented languages: this refers to the receiver, the object on which the method has been invoked.

    var obj = {
        method: function () {
            console.log(this === obj); // true
        }
    }
    obj.method();

this in the top-level scope

In browsers, the top-level scope is the global scope and this refers to the global object (like window does):

    <script>
        console.log(this === window); // true
    </script>

In Node.js, you normally execute code in modules. Therefore, the top-level scope is a special module scope:

    // `global` (not `window`) refers to global object:
    console.log(Math === global.Math); // true

    // `this` doesn’t refer to the global object:
    console.log(this !== global); // true
    // `this` refers to a module’s exports:
    console.log(this === module.exports); // true

this in eval()

eval() can be called either directly (via a real function call) or indirectly (via some other means). The details are explained here.

If eval() is called indirectly, this refers to the global object:

    > (0,eval)('this === window')
    true

Otherwise, if eval() is called directly, this remains the same as in the surroundings of eval(). For example:

    // Real functions
    function sloppyFunc() {
        console.log(eval('this') === window); // true
    }
    sloppyFunc();

    function strictFunc() {
        'use strict';
        console.log(eval('this') === undefined); // true
    }
    strictFunc();

    // Constructors
    var savedThis;
    function Constr() {
        savedThis = eval('this');
    }
    var inst = new Constr();
    console.log(savedThis === inst); // true

    // Methods
    var obj = {
        method: function () {
            console.log(eval('this') === obj); // true
        }
    }
    obj.method();

this-related pitfalls

There are three this-related pitfalls that you should be aware of. Note that in each case, strict mode makes things safer, because this is undefined in real functions and you get warnings when things go wrong.

Pitfall: forgetting new

If you invoke a constructor and forget the new operator, you are accidently using it as a real function. Hence, this does not have the correct value. In sloppy mode, this is window and you’ll create global variables:

    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    var p = Point(7, 5); // we forgot new!
    console.log(p === undefined); // true

    // Global variables have been created:
    console.log(x); // 7
    console.log(y); // 5

Thankfully, you get a warning in strict mode (this === undefined):

    function Point(x, y) {
        'use strict';
        this.x = x;
        this.y = y;
    }
    var p = Point(7, 5);
    // TypeError: Cannot set property 'x' of undefined

Pitfall: extracting methods improperly

If you retrieve the value of a method (instead of invoking it), you turn the method into a function. Calling the value results in a function call, not a method call. This kind of extraction can happen when you pass a method as an argument for a function or method call. Real-world examples include setTimeout() and registering event handlers. I’ll use the function callIt() to simulate this use case synchronously:

    /** Similar to setTimeout() and setImmediate() */
    function callIt(func) {
        func();
    }

If you call a sloppy-mode method as a function, this refers to the global object and global variables will be created:

    var counter = {
        count: 0,
        // Sloppy-mode method
        inc: function () {
            this.count++;
        }
    }

    callIt(counter.inc);

    // Didn’t work:
    console.log(counter.count); // 0

    // Instead, a global variable has been created
    // (NaN is result of applying ++ to undefined):
    console.log(count);  // NaN

If you call a strict-mode method as a function, this is undefined. Things don’t work, either. But at least you get a warning:

    var counter = {
        count: 0,
        // Strict-mode method
        inc: function () {
            'use strict';
            this.count++;
        }
    }

    callIt(counter.inc);

    // TypeError: Cannot read property 'count' of undefined
    console.log(counter.count);

The fix is to use bind():

    var counter = {
        count: 0,
        inc: function () {
            this.count++;
        }
    }

    callIt(counter.inc.bind(counter));

    // It worked!
    console.log(counter.count); // 1

bind() created a new function that always receives a this whose value is counter.

Pitfall: shadowing this

When you use a real function inside a method, it is easy to forget that the former has its own this (even though it has no need for it). Therefore, you can’t refer from the former to the method’s this, because it is shadowed. Let’s look at an example where things go wrong:

    var obj = {
        name: 'Jane',
        friends: [ 'Tarzan', 'Cheeta' ],
        loop: function () {
            'use strict';
            this.friends.forEach(
                function (friend) {
                    console.log(this.name+' knows '+friend);
                }
            );
        }
    };
    obj.loop();
    // TypeError: Cannot read property 'name' of undefined

In the previous example, this.name fails, because the function’s this is undefined, it is not the same as the this of the method loop(). There are three ways to fix … this.

Fix 1: that = this. Assign this to a variable that isn’t shadowed (another popular name is self) and use that one.

    loop: function () {
        'use strict';
        var that = this;
        this.friends.forEach(function (friend) {
            console.log(that.name+' knows '+friend);
        });
    }

Fix 2: bind(). Use bind() to create a function whose this always has the correct value (the method’s this in the following example).

    loop: function () {
        'use strict';
        this.friends.forEach(function (friend) {
            console.log(this.name+' knows '+friend);
        }.bind(this));
    }

Fix 3: forEach’s second parameter. This method has a second parameter whose value is passed to the callback as this.

    loop: function () {
        'use strict';
        this.friends.forEach(function (friend) {
            console.log(this.name+' knows '+friend);
        }, this);
    }

Best practices

Conceptually, I think of real functions as not having their own this and think of the aforementioned fixes as keeping up that illusion. ECMAScript 6 supports this approach via arrow functions – functions without their own this. Inside such functions, you can freely use this, because there is no shadowing:

    loop: function () {
        'use strict';
        // The parameter of forEach() is an arrow function
        this.friends.forEach(friend => {
            // `this` is loop’s `this`
            console.log(this.name+' knows '+friend);
        });
    }

I don’t like APIs that use this as an additional parameter of real functions:

    beforeEach(function () {  
        this.addMatchers({  
            toBeInRange: function (start, end) {  
                ...
            }  
        });  
    });  

Turning such an implicit parameter into an explicit one makes things more obvious and is compatible with arrow functions.

    beforeEach(api => {
        api.addMatchers({
            toBeInRange(start, end) {
                ...
            }
        });
    });
分享到:
评论

相关推荐

    Beginning JavaScript 3d Edition

    This book aims to teach you all you need to know to start experimenting with JavaScript: what it is, how it works, and what you can do with it. Starting from the basic syntax, you’ll move on to learn...

    WordPress 2.7 Cookbook.pdf

    How it works 217 There's more... 217 Managing who Sees Ads 218 Getting ready 218 How to do it 219 How it works 220 There's more... 220 Advanced conditions 221 Inserting ads in your RSS feeds ...

    Effective JavaScript: 68 Specific Ways to Harness the Power of JavaScript[EPUB版]

    No matter how long you’ve been writing JavaScript code, Effective JavaScript will help deepen your understanding of this powerful language, so you can build more predictable, reliable, and ...

    JavaScript Functional Programming for JavaScript Developers (PDF, EPUB, MOBI)

    Discover what functional programming is, why it's effective, and how it's used in JavaScript Understand and optimize JavaScript's hidden potential as a true functional language Who This Book Is For If...

    Beginning JavaScript with DOM Scripting and Ajax: Second Editon

    This completely updated second edition covers everything you need to know to get up-to-speed with JavaScript development and add dynamic enhancements to web pages, right from the basics. As well as ...

    Learning [removed] JavaScript programming Vol 1: The language core

    You will learn how to write JavaScript code that not only works, but that you can be proud of. The latest, completely revised edition takes into account the most current JavaScript language version. ...

    Building Blockchain Projects -Packt Publishing((2017).pdf )

    This book will teach you what Blockchain is, how it maintains data integrity, and how to create real- world Blockchain projects using Ethereum. With interesting real-world projects, you will know ...

    JavaScript.Object.Programming.148421

    This brief book explains the advantages of the object model, inheritance, both classical and prototypical, and shows how these concepts can be implemented in JavaScript. It also shows how object ...

    JavaScript Security(PACKT,2014)

    This book starts off with an introduction to JavaScript security and gives you an overview of the basic functions JavaScript can perform on the Web, both on the client side and the server side. It ...

    JavaScript: Moving to ES2015

    JavaScript: Moving to ES2015 by Ved Antani English | 24 Mar. 2017 | ASIN: B06XWDKLS8 | 1194 Pages | AZW3 | 9.08 MB Explore and master modern JavaScript techniques with ES2015 in order to build large-...

    JavaScript for Dummies 4th Edition

    Shows users how to "team up" JavaScript with Java, C++, OpenDoc, and Common Gateway Interface (CGI) to create powerful multimedia applications Describes how to build a quickie Web page using ...

    JavaScript Best Practice

    This book presents modern JavaScript best practice, utilizing the features now available in the language that enable you to write more powerful code that is clean, performant, maintainable, and ...

    how-webpack-works.pdf

    This big file can then be sent by the server to a client's browser. Remember the browser and server don't care that this big file was generated using Webpack it just treats it like any other file.

    英文原版-JavaScript for NET Developers 1st Edition

    Unlock the potential of evergreen browsers and increase the efficiency of your ASP.NET applications by learning how to write JavaScriptIf you want to improve responsiveness or the UX in your ASP.NET ...

    Test-Driven JavaScript Development

    If you’re an experienced programmer with no prior experience with JavaScript, this part should help you understand where JavaScript differs from other languages, especially less dynamic ones, and ...

    Javascript.Object.Oriented.Programming.pdf

    This course is a comprehensive guide where each chapter consists of best practices, constructive advice, and few easy-to-follow examples that will build up your skills as you advance through the book....

    JavaScript: The Good Parts

    With JavaScript: The Good Parts, you can release this elegant programming language from its old shell, and create more maintainable, extensible, and efficient code., The book's topics include:, * ...

    深入浅出javascript

    Who is this book for? Who should probably back away from this book? If you can answer “yes” to all of these: We’ll help you learn how to ...check it out if you want to brush up on your HTML

    JavaScript Concurrency pdf 无水印 0分

    Understand exactly how JavaScript works in a web browser environment and how these mechanisms power our event-driven JavaScript code Use promises to turn complex synchronization scenarios into ...

Global site tag (gtag.js) - Google Analytics