简体   繁体   中英

Use an IIFE approach and pass a variable from one file to another

One of my previous questions was how to organize the code between multiple .js files. Now I have a problem.

I have a map in d3.js divided by countries. When the user double-clicks on a country, I would like to pass a variable to another js file.

This is my html file, index.hbs :

<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
        <script src='https://d3js.org/topojson.v2.min.js'></script>
        <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>

        <link href='/css/all.css' rel='stylesheet'/>
    </head>

    <body>
        <div id='map'></div>

        <script> 
            var viewData = {};  
            viewData.nuts0 = JSON.parse('{{json nuts0}}'.replace(/&quot;/g, '"').replace(/&lt;/, ''));
            viewData.CONFIG = JSON.parse('{{json CONFIG}}'.replace(/&quot;/g, '"').replace(/&lt;/, '')); 
        </script>

        <script src='/script/map.js' rel='script'/><{{!}}/script>
        <script src='/script/other.js' rel='script'/><{{!}}/script>
    </body>
</html>

map.js :

var NAME=(function map() {

    var my = {};

    var CONFIG = viewData.CONFIG;
    var nuts0 = viewData.nuts0;

    // paths
    var countries;

    // width and height of svg map container
    var width = CONFIG.bubbleMap.width;
    var height = CONFIG.bubbleMap.height;

    // to check if user clicks or double click
    var dblclick_timer = false;

    // create Hammer projection
    var projectionCurrent = d3.geoHammer() 
        .scale(1) 
        .translate([width/2, height/2]); 

    var projectionBase = d3.geoHammer()
        .scale(1) 
        .translate([width/2, height/2]);

    // creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
    var path = d3.geoPath().projection(projectionCurrent);

    // creates the svg element that contains the map
    var map = d3.select('#map');

    var mapSvg = map.append('svg')
        .attr('id', 'map-svg')
        .attr('width', width)
        .attr('height', height);

    var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');

    countries = topojson.feature(nuts0, nuts0.objects.nuts0);
    projectionCurrent.fitSize([width, height], countries);

    var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
        .data(countries.features)
        .enter()
        .append('path');

    mapSvgGCountryPath.attr('class', 'country')
        .attr('fill', 'tomato')
        .style('stroke', 'white')
        .style('stroke-width', 1) 
        .attr('d', path)
        .attr('id', function(c) {
            return 'country' + c.properties.nuts_id;
        })
        .on('click', clickOrDoubleCountry);

    function clickOrDoubleCountry(d, i) {
        if(dblclick_timer) { // double click
            clearTimeout(dblclick_timer);
            dblclick_timer = false;
            my.countryDoubleClicked = d.country; // <-- variable to pass
        }
        else { // single click
            dblclick_timer = setTimeout(function() {
                dblclick_timer = false;
            }, 250)
        } 
    } 

    return my;

}());

other.js :

(function other(NAME) {
    console.log('my:', NAME.my); // undefined
    console.log('my:', NAME.countryDoubleClicked); // undefined
})(NAME);

I would like to be able to read the my object created in map.js in the other.js file and then be able to access the my.countryDoubleClicked variable from other.js .

This code doesn't work, I get TypeError: NAME.my is undefined .

There are a few things going on:

Revealing Variables

First you're not revealing the my variable to show up as NAME.my in map.js :

var NAME = (function map() {
    var my = {};
    //...
    return my;
}());

This sets NAME to my , instead of setting NAME.my to my . If you do want to do this, you can do something like this:

var NAME = (function map() {
    var my = {};
    //...
    return {
      my: my
    };
}());

You can read more about this technique, called the "Revealing Module Pattern", from articles like this one: http://jargon.js.org/_glossary/REVEALING_MODULE_PATTERN.md

Using functions to run code later

Second, as others have mentioned and as you've realized, since the code in other.js runs immediately, it'll run that code before the user has any chance to click on a country. Instead, you want code that can run on demand, (in this case when the user double clicks on something). In JavaScript, this is traditionally done by assigning or passing a function. For simplicity, we can assign something to my.doubleClickHandler and then invoke that function in clickOrDoubleCountry . For this I've made the country an argument passed to the handler, in addition to assigning it to NAME.my.countryDoubleClicked , but you'll probably only need to use one of them.

function clickOrDoubleCountry(d, i) {
    if(dblclick_timer) { // double click
        clearTimeout(dblclick_timer);
        dblclick_timer = false;
        my.countryDoubleClicked = d.country; // <-- variable to pass
        if (my.doubleClickHandler) {
            my.doubleClickHandler(d.country);
        }
    }
    // ...
} 

Then in other.js , you'd assign the function you want to run to NAME.my.doubleClickHandler :

(function other(NAME) {
    NAME.my.doubleClickHandler = function (country) {
        // now this code runs whenever the user double clicks on something
        console.log('exposed variable', NAME.my.countryDoubleClicked); // should be the country
        console.log('argument', country); // should be the same country
    });
})(NAME);

So in addition to the modified other.js above, this is complete modified map.js :

var NAME=(function map() {

    var my = {};

    var CONFIG = viewData.CONFIG;
    var nuts0 = viewData.nuts0;

    // paths
    var countries;

    // width and height of svg map container
    var width = CONFIG.bubbleMap.width;
    var height = CONFIG.bubbleMap.height;

    // to check if user clicks or double click
    var dblclick_timer = false;

    // create Hammer projection
    var projectionCurrent = d3.geoHammer() 
        .scale(1) 
        .translate([width/2, height/2]); 

    var projectionBase = d3.geoHammer()
        .scale(1) 
        .translate([width/2, height/2]);

    // creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
    var path = d3.geoPath().projection(projectionCurrent);

    // creates the svg element that contains the map
    var map = d3.select('#map');

    var mapSvg = map.append('svg')
        .attr('id', 'map-svg')
        .attr('width', width)
        .attr('height', height);

    var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');

    countries = topojson.feature(nuts0, nuts0.objects.nuts0);
    projectionCurrent.fitSize([width, height], countries);

    var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
        .data(countries.features)
        .enter()
        .append('path');

    mapSvgGCountryPath.attr('class', 'country')
        .attr('fill', 'tomato')
        .style('stroke', 'white')
        .style('stroke-width', 1) 
        .attr('d', path)
        .attr('id', function(c) {
            return 'country' + c.properties.nuts_id;
        })
        .on('click', clickOrDoubleCountry);

    function clickOrDoubleCountry(d, i) {
        if(dblclick_timer) { // double click
            clearTimeout(dblclick_timer);
            dblclick_timer = false;
            my.countryDoubleClicked = d.country; // <-- variable to pass
            if (my.doubleClickHandler) {
                my.doubleClickHandler(d.country);
            }
        }
        else { // single click
            dblclick_timer = setTimeout(function() {
                dblclick_timer = false;
            }, 250)
        } 
    } 

    return {
        my: my
    };

}());

If you don't want to use NAME.my for everything and want methods and variables accessible directly from NAME (eg NAME.countryDoubleClicked instead of NAME.my.countryDoubleClicked ), you can use the original return statement return my; , just bear in mind that there will be no variable named NAME.my .

You need to set explicit fields... for example:

let x = (function(){
    let obj = {}; // the "namespace"
    let private_var = 0;
    function foo() {
        return private_var++; // Access private vars freely
    };
    obj.foo = foo; // "publish" the function
    console.log(foo()); // you can use unqualified foo here
    return obj;
})();

// outputs 0 from the console log call inside the "constructor"

console.log(x.private_var); // undefined, it's not published
console.log(x.foo()); // outputs 1, the function was published
console.log(x.foo()); // 2
console.log(x.foo()); // 3

Local variables or local functions of a Javascript function are not implicitly published anywhere. If you want to access them then you need to set up an object field.

I prefer the Original Module pattern above the Revealing Module pattern, mainly because of the benefits of loose augmentation; In short, it allows to break up a module into parts which can be loaded asynchronously, read more here .

Via window.NAME = window.NAME || {} window.NAME = window.NAME || {} in the code below a custom namespace with name NAME gets declared if it doesn't exist yet. Module 1 declares a variable my into it and module 2 a variable other ; whether module 1 runs before or after module 2 doesn't matter.

When the main module gets executed (after module 1 and 2) it can access the variables defined in both. Notice that these can be accessed in 3 different ways.

 // Module1.js. (function(NAME) { NAME.my = "foo" // Replace with your map instance. })(window.NAME = window.NAME || {}); // Module2.js. (function(namespace) { namespace.other = "bar" })(window.NAME = window.NAME || {}); // Main module using what is defined in the 2 modules above. (function(namespace) { console.log(NAME.my); console.log(namespace.my) console.log(window.NAME.my); console.log(NAME.other); console.log(namespace.other) console.log(window.NAME.other) })(window.NAME = window.NAME || {}); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM