How to create a dynamic memoize function with multiple users in javascript - javascript

I have a function that calculates bounds:
function Canvas() {
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
}
this.responsiveBounds = (f) => {
let cached;
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
and I have a function that uses this Canvas object bounds:
function Box(canvas) {
let fBounds = canvas.responsiveBounds(({ width, height }) => {
return {
width,
height
};
});
this.render = () => {
let bounds = fBounds();
console.log(bounds);
};
}
Now when canvas resize function is called, bounds will change, and I need to reflect this change (by clearing the cached variable somehow). I can't make cached global because responsiveBounds is called by multiple users.

I see two different options:
1. Register listeners to the change
function Canvas() {
let listeners = [];
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
listeners.forEach(listener => listener());
};
this.responsiveBounds = (f) => {
let cached;
listeners.push(() => cached = undefined);
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
Here each responsiveBounds execution context has its own listener registered, so it can take care of clearing its private cache.
2. Collect all caches in a Weak Map
With a (Weak) Map you can create a store for private cache variables, keyed by the function object for which they are relevant:
function Canvas() {
let cached = new WeakMap;
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
cached.clear();
};
this.responsiveBounds = (f) => {
return () => {
let result = cached.get(f);
if (result === undefined) {
cached.set(f, result = f(this));
}
return result;
};
}
}
The nice thing about Weak Maps is that if the function returned by responsiveBounds gets garbage collected, and also the corresponding f, then the corresponding WeakMap entry can be garbage collected as well.
The advantage of the first solution is that the resize method does not need to know anything about what the listeners are doing (whether they are dealing with a cache or entirely something else).

Related

Getting a debounced function to return a variable

I have a function called debouncedGetScrolledDownPercentage that I want to return the scrollPercentage. As it is debounced, it incorrectly returns undefined, presumably because the debounce function does not return anything.
How can debouncedGetScrolledDownPercentage() return the scrollPercentage, so that scrolledDownPercentage: debouncedGetScrolledDownPercentage() is not undefined? I assume a global variable accessible to all scopes could solve it, but I would prefer a different solution.
Any help would be appreciated, thank you.
For context, here is all the code:
window.onbeforeunload = function() { return "Are you sure you want to leave?"; }; // for testing, so you can click close and the browser won't close and you can still see the logs
// initial data
let bounced = true;
const sessionStartTime = new Date();
let maxScrollPosition = 0;
let totalScrollDistance = 0;
// functions
const setBouncedToFalse = () => {
bounced = false;
document.removeEventListener("click", setBouncedToFalse);
};
const calculateSessionTime = () => {
const sessionEndTime = new Date();
return sessionEndTime - sessionStartTime; // sessionTimeMs
}
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};
const debouncedGetScrolledDownPercentage = debounce(() => {
// const debouncedGetScrolledDownPercentage = debounce((callback) => {
const scrollPosition = window.scrollY;
let scrollPercentage = 0;
if (scrollPosition > maxScrollPosition) {
const scrollDistance = scrollPosition - maxScrollPosition;
totalScrollDistance += scrollDistance;
maxScrollPosition = scrollPosition;
const maxPossibleScrollDistance = document.documentElement.scrollHeight - window.innerHeight;
scrollPercentage = Math.round(totalScrollDistance / maxPossibleScrollDistance * 100);
}
return scrollPercentage;
// callback(scrollPercentage);
}, 200);
const processData = () => {
const userData = {
sessionTimeMs: calculateSessionTime(),
bounced,
scrolledDownPercentage: debouncedGetScrolledDownPercentage() // incorrectly returns undefined
};
debouncedGetScrolledDownPercentage((scrollPercentage) => {
userData.scrolledDownPercentage = scrollPercentage;
console.log('userData: ', userData);
});
}
// events
document.addEventListener("click", setBouncedToFalse); // track clicks (if user bounces)
window.addEventListener('scroll', debouncedGetScrolledDownPercentage); // needed?
window.addEventListener("beforeunload", processData); // process data before user leaves

JavaScript Object method not changing property

I am building a factory function to manage Ship objects for a battleship game. So far I have the following:
const Ship = (name, length, orientation = 'horizontal') => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = (position) => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
return {
name,
length,
orientation,
sunk,
hits,
hit,
sink,
};
};
I am testing the sink() method to change the sunk property boolean from false to true. However, whenever I run:
example.sink()
example.sunk
sunk always remains false.
Where am I going wrong?
For some reason the hit() method alters the hits propertyfine. Butsink()is not altering thesunk` property.
Thanks
You can use getters to retrieve the values:
const Ship = (name, length, orientation = 'horizontal') => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = position => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
return {
name,
length,
orientation,
hit,
sink,
// get values using getters
get sunk() { return sunk; },
get hits() { return hits; },
};
};
const someShip = Ship(`ss something`, 100, `vertical`);
someShip.sink();
console.log(someShip.sunk);
You can create a API method to access the local sunk method
const Ship = (name, length, orientation = "horizontal") => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = (position) => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
function getSunk() {
return sunk;
}
return {
name,
length,
orientation,
hits,
hit,
sink,
getSunk,
};
};
const ship = Ship("a", 10);
console.log(ship.getSunk());
ship.sink();
console.log(ship.getSunk());

Pre-load images and get progress

I have got the following script I use to pre-load images:
export default function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
});
}
return Promise.all(images.map(src => loadImage(src)));
}
I then use this within a Vue component like follows:
PreloadImages(this.images).then(() => (this.imagesPreloaded = true));
I want to be able to get progress from this however within my component so I can display for example 2/50 Images Loaded. How would I go about doing this?
Edit
This is what I ended up with:
PreloadImages.js
export default function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
});
}
return images.map(src => loadImage(src));
}
Within my component:
handlePreload() {
PreloadImages(this.images).forEach(p => p.then(() => {
this.imagesPreloaded++;
}));
},
try this:
function PreloadImages(images) {
let promises = [];
images.map(src => loadImage(src));
function loadImage(src) {
promises.push(new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
img.src = src;
}));
}
Promise.all(promises);
return promises;
}
let images = [];
let counter = 0;
let promises = PreloadImages(images);
promises.forEach(p => p.then(() => {
counter++;
console.log(counter);
}));
This is how you should approach it, don't introduce any extra looping:
var imageSrcList = [
"https://image.flaticon.com/icons/svg/565/565860.svg",
"https://image.flaticon.com/icons/svg/565/565861.svg",
"https://Error.Done.One.Purpose",
"https://image.flaticon.com/icons/svg/565/565855.svg",
];
var imageContainer = document.getElementById("loadedImages");
var loadedCount = 0;
function displayCurrentLoadStatus(){
console.log("Current load progress: ", loadedCount, " of ", imageSrcList.length);
}
function PreloadImages(images) {
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = function()
{
++loadedCount;
imageContainer.appendChild(img);
displayCurrentLoadStatus();
resolve(img);
};
img.onerror = img.onabort = function() {
reject(src);
};
});
}
// Note that I removed Promise.all, let's just return a list of promises for the images passed
return images.map((imgSrc, i)=> loadImage(imgSrc).catch((rejectedSrcError=> rejectedSrcError)));
}
// now let's create all promises for each image in images
Promise.all(PreloadImages(imageSrcList)).then( resolvedSrcList =>
{
// Use your resolved Src's here
console.log("Load complete: ",loadedCount, "/",resolvedSrcList.length, " images were loaded successfully.");
});
img{
width: 60px;
height: 60px;
display: inline-block;
}
<div id="loadedImages"></div>
... tldr ...
There will be 2 different basic ideas and 4 solutions for demonstration purpose.
The first two approaches are valid in case one can not or is not allowed to change the return value (a single promise) of PreloadImages as from the OP's originally provided example code.
The 3rd example presents the preferred approach which is to change the return value of PreloadImages to a list of (image preloading) promises, whereas the 4th example is just No.3 that in addition features a real live demo of progress-bar and statistics rendering.
1) Proxy based
One could use a Proxy object that one, in addition to the image source list, does provides to the preloadImages function.
One would use such an object in its set and validate configuration.
Choosing a Proxy based approach comes with following advantages ...
Introducing eventProxy into preloadImages makes this new implementation agnostic to later user cases.
One just needs to implement functionality for an API that provides all necessary information about image loading states in a/the most generic way.
The user specific, but in the preloadImages' scope unknown use case, that later needs to be handled, will be provided as part of the eventProxy.
/*export default */function preloadImages(imageSourceList, eventProxy) {
// - the introduction of an `eventProxy` makes this implementation
// agnostic to later user cases.
// - one just needs to implement the necessary api that provides
// information about image loading states.
// - the user specific, but in this scope unknown use case, that later
// needs to be handled, will be provided as part of the `eventProxy`.
const maxCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(image);
++successCount;
eventProxy.event = {
image,
maxCount,
successCount,
failureCount
}
};
image.onerror = image.onabort = function() {
reject(src);
++failureCount;
eventProxy.event = {
image,
maxCount,
successCount,
failureCount
}
};
image.src = src;
});
}
return Promise.all(imageSourceList.map(src => loadImage(src)));
}
function renderImageLoadProgress(evt) {
// the render functionality goes in here ...
console.log('renderImageLoadProgress :: evt :', evt);
}
function isImageLoadType(type) {
return (
type
&& ('image' in type)
&& ('maxCount' in type)
&& ('successCount' in type)
&& ('failureCount' in type)
)
}
const loadEventValidator = {
set: function(obj, key, value) {
let isSuccess = false;
if ((key === 'event') && isImageLoadType(value)) {
obj[key] = value;
isSuccess = true;
// provide render functionality as part of
// the proxy's correct setter environment.
renderImageLoadProgress(value);
}
return isSuccess;
}
}
const loadEventProxy = new Proxy({}, loadEventValidator);
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
preloadImages(imageSourceList, loadEventProxy);
.as-console-wrapper { min-height: 100%!important; top: 0; }
2) Event based
In case of not being able making use of proxies, one easily can switch the above approach to EventTarget and CustomEvent, especially since there are polyfills available that also do work in older versions of Internet Explorer. The idea behind this solution and its advantages are the same as with the former Proxy based one ...
/*export default */function preloadImages(imageSourceList, eventTarget) {
// - additionally injecting/providing an `eventTarget` makes this
// implementation agnostic to later user cases.
// - one just needs to dispatch all relevant information via the
// `details` property of a custom event.
// - the user specific, but in this scope unknown use case, will be
// handled from ouside via listening to the custom event's type.
// - there are polyfills for `EventTarget` as well as for `CustomEvent`
// that also do work in older versions of Internet Explorer.
const maxCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(image);
++successCount;
eventTarget.dispatchEvent(new CustomEvent("loadstatechange", {
detail: {
image,
maxCount,
successCount,
failureCount
}
}));
};
image.onerror = image.onabort = function() {
reject(src);
++failureCount;
eventTarget.dispatchEvent(new CustomEvent("loadstatechange", {
detail: {
image,
maxCount,
successCount,
failureCount
}
}));
};
image.src = src;
});
}
return Promise.all(imageSourceList.map(src => loadImage(src)));
}
function renderImageLoadProgress(evt) {
// the render functionality goes in here ...
console.log('renderImageLoadProgress :: evt.detail :', evt.detail);
}
const loadEventTarget = new EventTarget();
loadEventTarget.addEventListener('loadstatechange', renderImageLoadProgress);
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
preloadImages(imageSourceList, loadEventTarget);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Note
Since the 2 above given approaches did not change the return value/signature of the original implementation of PreloadImages they had to be implemented exactly each in their own way. But both together come with a lot of additional code, the proxy based approach being a little fatter than the event based one.
3) Return Array of Promises
Anyhow, if one is/was just willing to change the return value of the former PreloadImages to an array of image loading promises one could achieve a very clean and lean implementation that might look similar to the next one ...
/*export default */function getAsyncLoadingImageList(imageSourceList) {
const totalCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({
image,
counts: {
total: totalCount,
success: ++successCount,
failure: failureCount
},
success: true,
});
};
image.onerror = image.onabort = function() {
reject({
image,
counts: {
total: totalCount,
success: successCount,
failure: ++failureCount
},
success: false,
});
};
image.src = src;
});
}
// return list of *image loading* promises.
return imageSourceList.map(src => loadImage(src));
}
function renderImageLoadProgress(imageLoadData) {
// the render method.
console.log('imageLoadData : ', imageLoadData);
}
const imageSourceList = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563'
];
getAsyncLoadingImageList(imageSourceList).forEach(promise =>
promise
.then(renderImageLoadProgress)
.catch(renderImageLoadProgress)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
4) Bonus
In order to present a real progress bar demo, the 3rd example, that already had it all, was taken as is, but now features a full blown render method instead of just logging the image-loading progress-data ...
/*export default */function getAsyncLoadingImageList(imageSourceList) {
const totalCount = imageSourceList.length;
let successCount = 0;
let failureCount = 0;
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve({
image,
counts: {
total: totalCount,
success: ++successCount,
failure: failureCount
},
success: true,
});
};
image.onerror = image.onabort = function() {
reject({
image,
counts: {
total: totalCount,
success: successCount,
failure: ++failureCount
},
success: false,
});
};
image.src = src;
});
}
// return list of *image loading* promises.
return imageSourceList.map(src => loadImage(src));
}
function getImageLoadProgressRenderConfig(progressContainer) {
const elmProgress = progressContainer.querySelector('progress');
const countsContainer = progressContainer.querySelector('.image-counts');
const failuresContainer = progressContainer.querySelector('.load-failures');
const elmImageCount = countsContainer.querySelector('.count');
const elmImageTotal = countsContainer.querySelector('.total');
const elmFailureCount = failuresContainer.querySelector('.failure-count');
const elmCurrentCount = failuresContainer.querySelector('.current-count');
return {
nodes: {
progress: {
display: elmProgress,
count: elmImageCount,
total: elmImageTotal
},
failure: {
display: failuresContainer,
failureCount: elmFailureCount,
currentCount: elmCurrentCount
}
},
classNames: {
failure: 'failures-exist'
}
};
}
function renderImageLoadProgressViaBoundConfig(imageLoadData) {
const imageCounts = imageLoadData.counts;
const imageCountTotal = imageCounts.total;
const imageCountSuccess = imageCounts.success;
const imageCountFailure = imageCounts.failure;
const imageCountCurrent = (imageCountSuccess + imageCountFailure);
const renderConfig = this;
const renderNodes = renderConfig.nodes;
const progressNodes = renderNodes.progress;
const failureNodes = renderNodes.failure;
const classNameFailure = renderConfig.classNames.failure;
const isFailureOnDisplay =
failureNodes.display.classList.contains(classNameFailure);
progressNodes.display.textContent = `${ imageCountCurrent } \/ ${ imageCountTotal }`;
progressNodes.display.value = imageCountCurrent;
progressNodes.count.textContent = imageCountCurrent;
if (!imageLoadData.success) {
if (!isFailureOnDisplay) {
failureNodes.display.classList.add(classNameFailure);
}
failureNodes.failureCount.textContent = imageCountFailure;
} else if (isFailureOnDisplay) {
failureNodes.currentCount.textContent = imageCountCurrent
}
}
function preloadImagesAndRenderLoadProgress(imageSourceList, progressContainer) {
const totalImageCount = imageSourceList.length;
const renderConfig = getImageLoadProgressRenderConfig(progressContainer);
const renderNodes = renderConfig.nodes;
const progressNodes = renderNodes.progress;
const failureNodes = renderNodes.failure;
const renderImageLoadProgress =
renderImageLoadProgressViaBoundConfig.bind(renderConfig);
failureNodes.display.classList.remove(renderConfig.classNames.failure);
failureNodes.failureCount.textContent = 0;
failureNodes.currentCount.textContent = 0;
progressNodes.display.max = totalImageCount;
progressNodes.total.textContent = totalImageCount;
renderImageLoadProgress({
counts: {
success: 0,
failure: 0
},
success: true,
});
getAsyncLoadingImageList(imageSourceList).forEach(promise => promise
.then(renderImageLoadProgress)
.catch(renderImageLoadProgress)
);
}
const imageSourceListWithFailure = [
'https://picsum.photos/id/1011', // loading will fail.
'https://picsum.photos/id/1011/5472/3648',
'https://picsum.photos/id/1012/3973/2639',
'https://picsum.photos/id/1013/4256/2832',
'https://picsum.photos/id/1013', // loading will fail.
'https://picsum.photos/id/1014/6016/4000',
'https://picsum.photos/id/1015/6000/4000',
'https://picsum.photos/id/1016/3844/2563',
'https://picsum.photos/id/1018', // loading will fail.
'https://picsum.photos/id/1018/3914/2935',
'https://picsum.photos/id/1019/5472/3648',
'https://picsum.photos/id/1020/4288/2848',
'https://picsum.photos/id/1021', // loading will fail.
'https://picsum.photos/id/1021/2048/1206',
'https://picsum.photos/id/1022/6000/3376',
'https://picsum.photos/id/1023/3955/2094'
];
const imageSourceListWithoutFailure = [
'https://picsum.photos/id/1039/6945/4635',
'https://picsum.photos/id/1038/3914/5863',
'https://picsum.photos/id/1037/5760/3840',
'https://picsum.photos/id/1036/4608/3072',
'https://picsum.photos/id/1035/5854/3903',
'https://picsum.photos/id/1033/2048/1365',
'https://picsum.photos/id/1032/2880/1800',
'https://picsum.photos/id/1031/5446/3063',
'https://picsum.photos/id/1029/4887/2759',
'https://picsum.photos/id/1028/5184/3456',
'https://picsum.photos/id/1027/2848/4272',
'https://picsum.photos/id/1026/4621/3070'
];
preloadImagesAndRenderLoadProgress(
imageSourceListWithFailure,
document.querySelector('#loading-with-failure')
);
preloadImagesAndRenderLoadProgress(
imageSourceListWithoutFailure,
document.querySelector('#loading-without-failure')
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
.image-loading-progress {
margin: 20px 0;
}
.image-loading-progress,
.image-loading-progress > span,
.image-loading-progress progress {
display: block;
width: 100%;
}
.image-loading-progress > .load-failures {
display: none
}
.image-loading-progress > .load-failures.failures-exist {
display: block
}
<label class="image-loading-progress" id="loading-with-failure">
<progress value="0" max="0">0 / 0</progress>
<span class="image-counts">
...
<span class="count">0</span>
out of
<span class="total">0</span>
images finished loading ...
</span>
<span class="load-failures">
...
<span class="failure-count">0</span>
out of
<span class="current-count">0</span>
images are not available ...
</span>
</label>
<label class="image-loading-progress" id="loading-without-failure">
<progress value="0" max="0">0 / 0</progress>
<span class="image-counts">
...
<span class="count">0</span>
out of
<span class="total">0</span>
images finished loading ...
</span>
<span class="load-failures">
...
<span class="failure-count">0</span>
out of
<span class="current-count">0</span>
images are not available ...
</span>
</label>

IndexDB cursor .onsucess as a promise

First of all, I can't find a suitable title for this question - please feel free to edit.
I have the following function that reads objects from indexDb,
loadNeededParcels = property => {
const dbResults = [];
var readTransaction = this.db
.transaction("parcelData")
.objectStore("parcelData");
readTransaction.openCursor().onerror = e => {
console.log("open cursor error ", e);
};
readTransaction.openCursor().onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
dbResults.push(cursor.value);
cursor.continue();
} else {
return dbResults;
}
};
};
Now when I call this function with a simple function call, for example:
console.log(loadNeededParcels('hasData'))
The console log is undefined. I am guessing this happens because the function does not wait for the cursor to finish and return the dbResults variable?
So my question is this - how can I re-write this function as a promise, or rather to wait for the readTransaction.openCursor().onsucess to trigger?
So the expected result is for the function to actually return the values read from the database before exiting.
I am using cursors since the .getAll() the method is not supported in IE.
A simple solution that I ended up using:
loadNeededParcels = property => {
return new Promise((resolve, reject) => {
var readTransaction = this.db
.transaction("parcelData")
.objectStore("parcelData");
readTransaction.openCursor().onerror = e => {
reject(e);
};
const dbResults = [];
readTransaction.openCursor().onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
dbResults.push(cursor.value);
cursor.continue();
} else {
resolve(dbResults);
}
};
});
};
Try something like this. Do not call openCursor twice, that creates two requests.
function loadNeededParcels(db, property) {
return new Promise(function(resolve, reject) {
var results = [];
var tx = db.transaction('parcelData');
tx.onerror = function(event) {
reject(tx.error);
};
var store = tx.objectStore('parcelData');
// Open a cursor over all items
var request = store.openCursor();
request.onsuccess = function(event) {
var cursor = request.result;
if(cursor) {
var value = cursor.value;
if(value) {
// Only append defined values to the array
results.push(cursor.value);
}
cursor.continue();
} else {
resolve(results);
}
};
});
}
loadNeededParcels(db, 'hasData').then(function(results) {
console.log('Results', results);
}).catch(function(error) {
console.error(error);
});

Passing references of a class to another class and using its methods

Let's say you're making a game. You want to try and not pollute the global scope and possibly limit the user's ability to easily alter the game (doubtful with client-side). You feel like modules might be unnecessary for your purposes. Is it bad practice to pass references to a class to another class during instantiation to access its methods?
Contrived example:
//game.js
var Game = (function () {
function Game() {
this.currentLevel = null;
this.score = 0;
}
Game.prototype.addScore = function (num) {
this.score += num;
};
Game.prototype.goToLevel = function (diff) {
this.currentLevel = new Level(this, diff);
};
Game.prototype.returnHome = function (level) {
this.currentLevel = null;
};
return Game;
})();
//level.js
var Level = (function () {
function Level(game, difficulty) {
this.game = game; //reference to game
this.difficulty = difficulty;
this.entities = [];
this.load();
}
Level.prototype.load = function () {
this.addEntity({name: 'tim', power: 23, difficulty: this.difficulty});
};
Level.prototype.leave = function () {
this.game.returnHome();
};
Level.prototype.addEntity = function (options) {
this.entities.push(new Entity(this, options));
};
Level.prototype.removeEntity = function (entity) {
for(var x = 0; x < this.entities.length; x++) {
if(this.entities[x] === entity) this.entities.splice(x, 1);
}
};
return Level;
})();
//level.js
var Entity = (function () {
function Entity(level, options) {
this.level = level; //reference to level
this.options = options;
}
Entity.prototype.kill = function () {
this.level.removeEntity(this); // anti-pattern?
this.level.game.addScore(34.53); // too closely coupled?
};
return Entity;
})();
//main.js
var Main;
(function (Main) {
var game = null;
function documentIsReady() {
start(); // Start the game
}
function start() {
game = new Game();
game.goToLevel('hard');
}
return {
documentIsReady: documentIsReady
}
})(Main || (Main = {}));
$(document).ready(function () {
Main.documentIsReady();
});
Forgive the half-baked example. If you end up with many instances of the 'Entity' class, do all the references to 'Level', though the same instance, start taking more memory? Are there other pitfalls? Another method would be to implement some kind of interface that you can access that allow classes to talk to each other.

Categories