В чем разница между "var webApp = { .. }" и "var webApp = function (){ .. }"
В последнее время я много экспериментировал с модульным JS, и мне все еще интересно, правильно ли я пишу.
Например, если у меня есть страница, на которой есть входные данные и кнопки отправки, которые должны отображать данные после отправки (например, таблицу и график), то я пишу свой код в IFFE, чтобы ничто не могло получить к нему доступ, но используя переменную объекта, подобную этой:
var webApp = { ... }
и внутри него я кеширую элементы из DOM, добавляю события привязки и другие полезные функции.
Это реальный код, который я использую для формы, которая должна отображать график, таблицу и индикатор выполнения при загрузке данных, и все управляется в одном объекте. qwData
:
(function() {
const qwData = {
// Initialize functions
init: function() {
this.cacheDom();
this.bindEvents();
},
// Cache vars
cacheDom: function() {
this.dataDisplayed = false;
this.countUsers = <?php echo $_SESSION['all_users_count_real']; ?>;
this.customerMachines = <?php echo $_SESSION['customer_statistics']['total_machines']; ?>;
this.$form = $('#frm');
this.$alistToolbar = this.$form.find('.alist-toolbar');
this.start_date = this.$form[0][9].value;
this.end_date = this.$form[0][10].value;
this.dateCount = this.countDays(this.start_date, this.end_date);
this.show = document.querySelector('#btn-show');
this.downloadBtn = document.querySelector('#download_summary_button');
this.$dataContainer = $('#qw-data-container');
this.$qwTable = $('#qwtable');
this.$qwTbody = this.$qwTable.find('tbody');
this.$qwTd = this.$qwTbody.find('td');
this.qwChart = echarts.init(document.getElementById('main-chart'));
this.progressBar = document.querySelector('.progress-bar');
Object.defineProperty(this, "progress", {
get: () => {
return this.progressPrecent || 0;
},
set: (value) => {
if( value != this.progressPrecent ) {
this.progressPrecent = value;
// this.setQwChartProgress(value);
this.setProgressBarValue(value);
this.setProgressButton(value);
}
},
configurable: true
});
this.qwChartProgress = this.progress;
},
// Bind click events (or any events..)
bindEvents: function() {
var that = this;
// On click "Show" BTN
this.show.onclick = this.sendData.bind(this);
// On Change inputs
this.$form.change(function(){
that.updateDatesInputs(this);
});
// downloadQw
this.downloadBtn.onclick = this.downloadQw.bind(this);
},
downloadQw: function(e){
e.preventDefault();
var customer = "<?php echo $_SESSION['company_name']; ?>";
var filename = customer + "qws_"+ this.start_date + "-" + this.end_date + ".zip";
$.ajax({
url: "/aaa/api/download_csv.php",
method: "GET",
dataType : "json",
data: {
customer: customer,
filename: filename
},
success:function(result){
if(result.status){
window.location.href="/aaa/api/download_csv.php?customer="+customer+"&filename="+filename+"&download=1";
}
},
error:function(){
}
})
},
setQwChartProgress: function(value){
if (value != 0) {
// Show Chart Loading
this.qwChart.showLoading({
color: (value == 99) ? '#00b0f0' : '#fff',
text: value + '%'
});
}
},
setProgressButton: function(value){
if ( value >= 100 || value == 0 ){
this.show.value = 'Show';
}
else {
this.show.value = value +'%';
// this.show.disabled = true;
this.disableShow();
}
},
resetShowButton: function(){
this.show.value = 'Show';
this.disableShow();
},
disableShow: function(){
// this.show.style.color = "grey";
// this.show.disabled = true;
this.show.classList.add("isDisabled");
},
enableShow: function(){
// this.show.style.color = "#66aa66";
// this.show.disabled = false;
this.show.classList.remove("isDisabled");
},
updateDatesInputs: function(){
this.start_date = this.$form[0][9].value;
this.end_date = this.$form[0][11].value;
this.dateCount = this.countDays(this.start_date,this.end_date);
// this.show.disabled = false;
this.enableShow();
this.removeError();
},
removeError: function(){
if (this.errors) {
this.errors.remove();
delete this.errors;
}
},
countDays: function(date1, date2){
// First we split the values to arrays date1[0] is the year, [1] the month and [2] the day
var date1 = date1.split('-');
var date2 = date2.split('-');
// Now we convert the array to a Date object, which has several helpful methods
date1 = new Date(date1[0], date1[1], date1[2]);
date2 = new Date(date2[0], date2[1], date2[2]);
// We use the getTime() method and get the unixtime (in milliseconds, but we want seconds, therefore we divide it through 1000)
var date1_unixtime = parseInt(date1.getTime() / 1000);
var date2_unixtime = parseInt(date2.getTime() / 1000);
// This is the calculated difference in seconds
var timeDifference = date2_unixtime - date1_unixtime;
// in Hours
var timeDifferenceInHours = timeDifference / 60 / 60;
// and finaly, in days :)
var timeDifferenceInDays = timeDifferenceInHours / 24;
if (timeDifferenceInDays > 0){
return timeDifferenceInDays;
} else {
// alert('Error: The date are invalid.');
}
},
// Get data, prevent submit defaults and submit.
sendData: function(e) {
e.preventDefault();
if (this.show.classList.contains('isDisabled')) {
this.showErrorDiv("Please select a new date range before submitting.");
} else {
let that = this;
let estimatedTotalTime = ( (this.countUsers*this.customerMachines)/777 ) * 0.098;
let estimatedTime = estimatedTotalTime/99;
let estimatedTimeMs = estimatedTime*1000;
let timer = setInterval( function(){that.incrementProgress(timer);}, estimatedTime);
console.log('Total Time: ' + estimatedTotalTime + 's');
console.log('Estimated Time for 1%: ' + estimatedTime + 's');
$.ajax({
type: 'POST',
url: "/manageit/ajax.php?module=qw_module",
dataType: 'json',
data: {
start_ts: that.start_date,
stop_ts: that.end_date,
submitted: true,
company_name: "<?php echo $_SESSION['company_name']; ?>"
},
beforeSend: function() {
// Show Chart Loading
that.qwChart.showLoading({
color: '#00b0f0',
// text: that.qwChartProgress
text: ''
});
// If data div isn't displayed
if (!that.dataDisplayed) {
// Show divs loading
that.showMainDiv();
} else {
that.$qwTbody.slideUp('fast');
that.$qwTbody.html('');
}
},
complete: function(){},
success: function(result){
// Reset show btn
that.resetShowButton();
// Clear timer
clearInterval(timer);
// Set progressbar to 100%
that.setProgressBarTo100();
// Show Download Button
that.downloadBtn.style.display = 'inline-block';
// Insert Chart Data
that.insertChartData(result);
// Insert Table Data
that.insertTableData(result);
}
});
that.dataDisplayed = true;
}
},
showErrorDiv: function(errorTxt){
if (!this.errors){
this.errors = document.createElement("div");
this.errors.className = "qw_errors_div";
this.errors.textContent = errorTxt;
this.$alistToolbar.append(this.errors);
}
},
// Insert Data to Table
insertTableData: function(json){
let str = '';
let isOdd = ' rowspan="2" ';
for ( let i=1; i<9; i++ ) {
str += '<tr>';
for (let j = 0; j < 8; j++) {
if ((i%2 === 0) && (j==0)){
// do nada
} else {
str += '<td ';
str += ((i % 2 !== 0)&&(j==0)) ? isOdd : '';
str += '> ';
str += json[i][j];
str += '</td>';
}
}
str += '</tr>';
}
this.$qwTbody.html(str);
this.$qwTbody.slideDown('fast', function(){
if ($(this).is(':visible'))
$(this).css('display','table-row-group');
});
// Apply colors on table.
this.tableHover();
},
tableHover: function(){
this.$qwTd = this.$qwTbody.find('td');
var that = this;
this.$qwTd.eq(0).hover( function(){
that.$qwTd.eq(0).css('background-color', '#f5f5f5');
that.$qwTd.eq(0).parent().css('background-color', '#f5f5f5');
that.$qwTd.eq(0).parent().next().css('background-color', '#f5f5f5');
}, function(){
that.$qwTd.eq(0).css('background-color', '');
that.$qwTd.eq(0).parent().css('background-color', '');
that.$qwTd.eq(0).parent().next().css('background-color', '');
});
this.$qwTd.eq(15).hover( function(){
that.$qwTd.eq(15).css('background-color', '#f5f5f5');
that.$qwTd.eq(15).parent().css('background-color', '#f5f5f5');
that.$qwTd.eq(15).parent().next().css('background-color', '#f5f5f5');
}, function(){
that.$qwTd.eq(15).css('background-color', '');
that.$qwTd.eq(15).parent().css('background-color', '');
that.$qwTd.eq(15).parent().next().css('background-color', '');
});
this.$qwTd.eq(30).hover( function(){
that.$qwTd.eq(30).css('background-color', '#f5f5f5');
that.$qwTd.eq(30).parent().css('background-color', '#f5f5f5');
that.$qwTd.eq(30).parent().next().css('background-color', '#f5f5f5');
}, function(){
that.$qwTd.eq(30).css('background-color', '');
that.$qwTd.eq(30).parent().css('background-color', '');
that.$qwTd.eq(30).parent().next().css('background-color', '');
});
this.$qwTd.eq(45).hover( function(){
that.$qwTd.eq(45).css('background-color', '#f5f5f5');
that.$qwTd.eq(45).parent().css('background-color', '#f5f5f5');
that.$qwTd.eq(45).parent().next().css('background-color', '#f5f5f5');
}, function(){
that.$qwTd.eq(45).css('background-color', '');
that.$qwTd.eq(45).parent().css('background-color', '');
that.$qwTd.eq(45).parent().next().css('background-color', '');
});
},
incrementProgress: function(timer){
if (this.progress == 99)
clearInterval(timer);
else
this.progress++;
},
// Insert Data to Chart
insertChartData: function(json){
var posList = [
'left', 'right', 'top', 'bottom',
'inside',
'insideTop', 'insideLeft', 'insideRight', 'insideBottom',
'insideTopLeft', 'insideTopRight', 'insideBottomLeft', 'insideBottomRight'
];
this.qwChart.configParameters = {
rotate: {
min: -90,
max: 90
},
align: {
options: {
left: 'left',
center: 'center',
right: 'right'
}
},
verticalAlign: {
options: {
top: 'top',
middle: 'middle',
bottom: 'bottom'
}
},
position: {
options: echarts.util.reduce(posList, function (map, pos) {
map[pos] = pos;
return map;
}, {})
},
distance: {
min: 0,
max: 100
}
};
this.qwChart.config = {
rotate: 90,
align: 'left',
verticalAlign: 'middle',
position: 'insideBottom',
distance: 15,
onChange: function () {
var labelOption = {
normal: {
rotate: this.qwChart.config.rotate,
align: this.qwChart.config.align,
verticalAlign: this.qwChart.config.verticalAlign,
position: this.qwChart.config.position,
distance: this.qwChart.config.distance
}
};
this.qwChart.setOption({
series: [{
label: labelOption
}, {
label: labelOption
}, {
label: labelOption
}]
});
}
};
var labelOption = {
normal: {
show: true,
position: this.qwChart.config.position,
distance: this.qwChart.config.distance,
align: this.qwChart.config.align,
verticalAlign: this.qwChart.config.verticalAlign,
rotate: this.qwChart.config.rotate,
// formatter: '{c} {name|{a}}',
formatter: '{name|{a}}',
fontSize: 16,
rich: {
name: {
// textBorderColor: '#fff',
// color: '#333',
// color: '#717171',
color: '#525252',
shadowColor: 'transparent',
shadowBlur: 0,
textBorderColor: 'transparent',
textBorderWidth: 0,
textShadowColor: 'transparent',
textShadowBlur: 0
}
}
}
};
option = {
color: ['#007bff', '#00b0f0', 'red', '#e5323e'],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['Inactives / Viewers', 'Inactives / Viewers / Less than 1min per day', 'Light no Macro']
},
toolbox: {
show: true,
orient: 'vertical',
left: 'right',
top: 'center',
feature: {
mark: {show: true},
// dataView: {show: true, readOnly: false},
// magicType: {show: true, type: ['line', 'bar', 'stack', 'tiled']},
restore: {show: true},
saveAsImage: {show: true}
}
},
calculable: true,
xAxis: [
{
type: 'category',
axisTick: {show: false},
data: ['Excel', 'Word', 'PowerPoint', 'All 3 Apps']
}
],
yAxis: [
{
type: 'value',
name: 'Score'
}
],
series: [
{
name: 'Light no Macro',
type: 'bar',
label: labelOption,
color: 'red',
data: [ [3, json[7][7]] ]
},
{
name: 'Inactives / Viewers',
type: 'bar',
barGap: 0,
label: labelOption,
data: [ json[1][7], json[3][7], json[5][7], json[8][7] ]
},
{
name: 'Inactives / Viewers / Less than 1min per day',
type: 'bar',
label: labelOption,
data: [ json[2][7], json[4][7], json[6][7] ]
}
]
};
// Set charts options
this.qwChart.setOption(option);
// Hide Loading
this.qwChart.hideLoading();
},
// Show Main div on submition (only)
showMainDiv: function(){
// Show all contatner div
this.$dataContainer.slideDown('slow');
},
// Sets a new value for the progress bar
setProgressBarValue: function(value){
this.progressBar.style.width = this.returnNumWithPrecent(value);
},
returnNumWithPrecent: function(num){
return num.toString() + '%';
},
setProgressBarTo100: function(){
var that = this;
// Show Download Button
this.progress = 100;
setTimeout(function () {
// Show Download Button
that.progress = 0;
}, 1000);
}
}
// run object
qwData.init();
})();
но я вижу другие примеры, которые пишут функциональность под функцией, а не объектом:
webApp = function (){ ... };
как пример:
var Background = (function() {
'use strict';
// placeholder for cached DOM elements
var DOM = {};
/* =================== private methods ================= */
// cache DOM elements
function cacheDom() {
DOM.$background = $('#background');
}
// coordinate async assembly of image element and rendering
function loadImage() {
var baseUrl = 'https://source.unsplash.com/category',
cat = 'nature',
size = '1920x1080';
buildElement(`${baseUrl}/${cat}/${size}`)
.then(render);
}
// assemble the image element
function buildElement(source) {
var deferred = $.Deferred(function (task) {
var image = new Image();
image.onload = function () {
task.resolve(image);
};
image.onerror = function () {
task.reject();
};
image.src = source;
});
return deferred.promise();
}
// render DOM
function render(image) {
DOM.$background
.append(image)
.css('opacity', 1);
}
/* =================== public methods ================== */
// main init method
function init() {
cacheDom();
loadImage();
}
/* =============== export public methods =============== */
return {
init: init
};
}());
У меня есть 2 вопроса по этому поводу:
В чем разница между использованием объекта и внутри него установки функций, vars, ect ':
var webApp = {...};
и переменная функции с одинаковыми характеристиками (только с синтаксисом, написанным по-разному). как пример из ссылки, которую я вставил.
var webApp = function (){ ... };
- Правильно ли писать код, подобный тому, где все (вроде отдельные элементы, такие как график, таблица, индикатор выполнения) в одном объекте / функции? Должно ли это быть лучше разделено на разные объекты? Если есть более новый и лучший способ написания такого кода, пожалуйста, укажите, что я должен исследовать.
3 ответа
Одна из проблем с интернет-уроками заключается в том, что они не достигли актуальности, и лишь немногие авторы поддерживают их актуальность. На землях JS дела идут очень быстро, и отраслевой стандарт 5 лет назад (например, jQuery) теперь кажется странным, когда вы все еще сталкиваетесь с ним.
Итак, чтобы воплотить в жизнь полезную привычку, которую я приставляю к другим за то, что опускаю:
Состояние модулей JavaScript, середина 2018 года, быстро меняется, # устарело
Это беспорядок.
Сначала у вас есть модули ES 6. ES 6 был переименован в ES 2015, а часть модулей была выделена и превращена в отдельную спецификацию, что означает, что браузер может быть совместим с ES 2015 и при этом не иметь собственных модулей. Однако спустя 3 года каждый браузер с соответствующей долей на мировом рынке (Chrome, Android Chrome, Firefox, iOS Safari) реализует по крайней мере базовую версию родной модульной системы (как, например, Edge, Opera и т. Д.). Мне неясно, так как я считаю, что спецификация позволяет путям быть более щадящими (мы вернемся к этому через минуту), но вот синтаксис, относительный или абсолютный путь к файлу с необходимым расширением:
import Foo from './foo.js'; // Import default export from foo.js, alias to 'Foo'
import { Foo } from './foo.js'; // Import named export 'Foo' from foo.js
export default function () {}; // exports a function as default, importer aliases
const Foo = class Foo {};
export Foo; // exports named class Foo
Они имеют много преимуществ перед всем остальным (прежде всего вам не нужны специальные инструменты или процессы сборки), но, поскольку они появились совсем недавно, они пока не получили широкого распространения в экосистеме JavaScript. Поскольку они долго ожидали, и у людей была работа, которую нужно было выполнить, они реализовали различные другие шаблоны модулей / инструменты / системы. Одним из самых ранних является вопрос в вашем вопросе, но эта модель, хотя и лучше, чем ничего, имеет достаточно проблем, чтобы люди начали осматриваться.
Модули AMD
Еще одно раннее предложение было require.js
определение асинхронного модуля. В то время как у этого были некоторые неотразимые технические преимущества, он фактически мертв.
Модули Common.js
node.js взорвался на сцене со своей собственной системой модулей, основанной на модулях common.js (которые в основном стали разновидностью defacto для common.js). Люди начали говорить "эй, было бы здорово сделать это и в браузере", и, таким образом, browserify. Browserify был инструментом, который просматривал ваш график зависимостей и конвертировал require
выражения во что-то, что браузер может обработать (в основном, делая требуют функцию). Модули Node в некотором роде не подходят для браузера, но конвергенция по одному стандарту была лучше, чем восемьдесят миллионов конкурирующих adhoc-реализаций. Люди посмотрели на эти три конкурирующих модели / системы модулей (один в вашем вопросе, AMD, common.js) и сказали: "Эй, мы можем объединить их". таким образом
Определение универсального модуля
Если вы видели дикий код, который выглядит следующим образом:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
Тогда вы видели UMD. Обратите внимание, как он проверяет, настроена ли среда, в которой он находится, для AMD или common.js. Трансформаторы были написаны для преобразования обоих стилей в это для унаследованного кода и разборчивости (это довольно много шаблонного).
Но люди хотели большего: они хотели иметь возможность выражать все зависимости своих веб-приложений (включая CSS и изображения) в коде с помощью инструмента для его фрагментирования и выборочной загрузки. Плюс к этому времени спецификация нативного модуля уже была в проекте, и люди хотели использовать этот синтаксис. И поэтому
Модули Webpack
Webpack в настоящее время является системой defacto, используемой сегодня (хотя многие люди все еще используют browserify). Синтаксис модуля Webpack выглядит примерно так:
import Foo from 'foo'; // imports default export from foo.js somewhere on PATH
Это выглядит знакомо? Очень похоже на (но тонко отличается от родных модулей). Webpack также может делать следующее:
import 'Something.css'; // commonly seen in react apps
import 'logo.svg'; // ditto
Это удобно, так как люди переходят к компонентным системам, приятно иметь возможность выражать все зависимости компонентов в файле точек входа для этого компонента. К сожалению, импорт HTML, который позволил бы вам сделать это изначально без шага сборки, умер страшной ужасной смертью.
Несовместимость с родной системой модулей, subtle (пути и расширения файлов) и gross (импорт ресурсов, отличных от js), вызывает сожаление, это означает, что один или другой придется изменить, потому что я пытался написать собственный модуль- основанные на приложениях в последнее время, и очень трудно использовать библиотеки (почти ни одна из которых не предлагает родной вариант модуля).
Что использовать - это своего рода самоуверенный вопрос, но если вы используете платформу, используйте то, что является общим для других пользователей этой платформы. Common.js и webpack достаточно распространены, так что существует множество инструментов для их потребления, и, вероятно, они являются лучшим выбором на данный момент. Другая вещь, на которую нужно обратить внимание, - это динамический импорт, уже загруженный в несколько браузеров.
Извините, это все так запутанно, вы просто вводите JavaScript в очень переходный период.
Если вы знакомы с объектно-ориентированным программированием, это можно понять по аналогии с частными и открытыми свойствами и функциями класса. Другой подход: модульная структура, см. Узловые модули. Речь идет об упаковке.
Посмотрите на этот код:
var obj = (function() {
let a = function() { console.log('function a'); };
let b = function() { a(); console.log('function b'); };
return { b: b };
})();
obj.b();
// function a
// function b
obj.a();
// TypeError: obj.a is not a function
Или, может быть, более знакомым таким образом:
function MyStuff() {
let a = function() { console.log('function a'); };
let b = function() { a(); console.log('function b'); };
return { b: b };
}
var obj = MyStuff();
obj.b();
// function a
// function b
obj.a();
// TypeError: obj.a is not a function
Внутри функции у вас есть локальная область, недоступная извне. в этой локальной области вы создаете переменные, функции и т. д.
В операторе return вы возвращаете объект, экспортирующий функции и переменные во внешнюю область видимости. Это все равно что решить, какие функции / реквизиты должны быть общедоступными, а какие - частными.
Когда вы создаете объект в var webApp = {...}
Кстати, вы непосредственно создаете объект, и все его свойства и функции являются общедоступными для области, в которой вы определяете переменную "webApp".
Отвечая на ваш второй вопрос, да, вы должны разделить вещи на отдельные объекты по интересам. Смотрите твердые принципы.:)
Шаблон IIFE учитывает частные переменные - DOM
, cacheDom
, loadImage
- которые не доступны извне IIFE. В шаблоне объекта все доступно публично как свойства объекта. Иногда вы вообще не можете создать нужный объект, не объявив сначала некоторые переменные, поэтому полезно хранить их изолированными внутри IIFE.