George Goodchild
George Goodchild

Reputation: 315

Value of this in a Durandal view model

I have a problem that is baffling me at the moment. I have a Durandal view model defined as below but the value of this and other variables and functions as I step through the code chops and changes all over the place.

I have commented the below as it's easier to show the various values as it moves through the code.

define(['durandal/plugins/router'], function (router) {

    var _outerRouter = router;
    var _outerSelf = this;

    var viewModel = function () {

        var _self = this;
        var _router = router;
        var _searchCriteria = ko.observable('');
        var _searchResults = ko.observableArray([]);
        var _messages = ko.observableArray([]);

        this.searchCriteria = _searchCriteria;
        this.searchResults = _searchResults;
        this.messages = _messages;

        this.search = function () {

            //
            // When called from the view bindings:
            //  > this = viewModel
            //  > _self = _outerSelf = _outerRouter = undefined
            //  > _router = defined
            //
            var that = this;

            //
            // N.B. I set callback context to the viewModel
            //
            var req = $.ajax({
                url: '../api/v1/things',
                dataType: 'json',
                context: this,  
                data: {
                    searchCriteria: this.searchCriteria()
                }
            })
            .done(function (results, status, request) {

                //
                // When called back:
                //  > this = viewModel 
                //  > _self = _outerSelf = _outerRouter = undefined
                //  > _router = defined
                //
                this.searchResults.removeAll();
                this.messages.removeAll();

                //
                // Now when calling:
                //  > doShowResult = showResult = undefined
                //  > this.showResult = defined (have to use 'this')
                //  > displayMultipleResults = defined
                //
                if (results.length == 1) {                    
                    this.showResult(results[0]);
                }
                else {
                    displayMultipleResults(results);
                }
            });

            function displayMultipleResults(results) {

                //
                // When called from ajax.done():
                //  > this = Window ?!?!
                //  > that = viewModel
                //  > _self = _outerSelf = _outerRouter = undefined
                //  > _router = defined
                //
                that.messages.push({ title: 'Found Lots of Things', body: "Please select...blah blah blah", type: 'info' });

                for (var i = 0; i < results.length; i++)
                    that.searchResults.push(results[i]);
            };
        };

        function doShowResult(result) {

            //
            // When called from ajax.done():
            //  > this = viewModel
            //  > _self = _outerSelf = _outerRouter = undefined
            //  > _router = defined
            //
            // and then
            //
            // When called from the view bindings:
            //  > this = the bound searchResult object
            //  > _self = _outerSelf = _outerRouter = undefined
            //  > _router = defined
            //
            _router.navigateTo('show-my-thing');
        }

        this.showResult = doShowResult;
    };

    return viewModel;
});

And here is the view that it's bound to:

<div>
    <div class="container-narrow">    
        <div class="row-fluid">
            <div class="span12">
                <h3>Search</h3>

                <p>Enter search criteria...</p>

                <div class="control-group">
                    <input type="text" class="input-block-level" placeholder="Criteria" data-bind="value: searchCriteria"/>
                </div>

                <div class="pull-right">
                    <button class="btn btn-primary" data-bind="click: search">
                        <i class="icon-search icon-white"></i> Search
                    </button>
                </div>

            </div>
        </div>
        <br />

        <!-- ko foreach: messages -->
        <div class="row-fluid">
            <div class="alert" data-bind="css: { 'alert-error': type == 'error', 'alert-info': type == 'info', 'alert-success': type == 'success' }">
                <strong data-bind="text: title"></strong> <span data-bind="text: body"></span>
            </div>
        </div>
        <!-- /ko -->

        <!-- ko foreach: searchResults -->
        <div class="row-fluid">
            <div class="span12 well well-small">                
                <div class="span10 search-result">
                    <label>Resut:</label> <span data-bind="{text: $data}"></span><br />
                </div>
                <div class="span2">                    
                    <button class="btn btn-mini btn-success pull-right" data-bind="click: $parent.showResult">
                        View
                    </button>
                </div>
            </div>        
        </div>    
        <!-- /ko -->

    </div>
</div>

My main question are:

  1. How come I can access _router but not _self (or any of the other variables (_searchCriteria etc.) everywhere?

  2. How when the execution is inside ajax.done() and the value of this equals the viewModel but after it's stepped into displaySearchResult this equals the Window object?

  3. When inside ajax.done() doShowResult and showResult are undefined but this.showResult works fine, surely if this is viewModel then showResult is defined?

  4. Fortunately, in this case I only need to navigate in doShowResult and _router is defined when I call from both ajax.done and from the view binding. But what if I needed to access a value from the view model - this wouldn't be available if called from the view binding - how can I change the bindings or the code to support this (preferably in a non-hacky way)?

Thanks in advance for any light anyone can shed on this.

Upvotes: 2

Views: 825

Answers (1)

Alexander Preston
Alexander Preston

Reputation: 1665

The 'this' keyword behaves very differently in JavaScript to other languages, as does the concept of visible scope.

The value of 'this' at any given time depends on the invocation context of the function you are accessing it from. There are four different ways to invoke a method and each has different implications for 'this'.

  1. Invocation as a function (a named function is invoked): this = window (i.e. global object).

    function fName() { };
    fName(); // this = window
    
  2. Invocation as a method (a property of an object is invoked): this = the object

    var o = {};
    o.whatever = function() {};
    o.whatever(); // this = o
    
  3. Invocation as a constructor (using the new keyword): this = the new object

    var Animal = function() { }; // best practice to capitalise the initial letter of function intended to be called as constructor.
    var cat = new Animal(); // this = (what will become cat)
    
  4. Invocation with the apply() and call() methods: (this = the first param)

    function fName() { };
    var obj1 = {};
    fName.apply(obj1); // this = obj1
    fName.call(obj1); // this = obj1
    

Upvotes: 4

Related Questions