
Research
/Security News
Miasma Mini Shai-Hulud Hits ImmobiliareLabs npm Packages
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.
简单, 是一种美、一种哲学、一种信仰. sojs 提供了最佳的 javascript 编程方式, 让代码更加简单的编写、阅读和维护.
sojs 代码示例:
//创建cookie类
sojs.define({
name: 'cookie', //类名
namespace: 'sojs.utility', //命名空间
prefix: 'prefix-', //属性
getCookie:function(key){...} //函数
});
//使用cookie类
var cookie = sojs.using('sojs.utility.cookie');
var id = cookie.getCookie('id');
sojs 是 编程框架 而非 类库 , 适用于所有的js项目, 可以和各种规范如ADM,CDM等一起使用. sojs中的so即Simple Object Oriented, 简单面向对象. 与传统的面向对象理论略有不同, 我们认为: 对象是组织代码的最小单位.
sojs 核心理念:
万物皆对象 对象皆JSON
sojs 主要功能:
使用JSON结构描述类 使用命名空间组织类 兼容全版本node环境 兼容全部浏览器环境
#传统的JS编程方式
首先, 让我们了解为何传统js编程方式会导致代码的可读性下降. 因为js的灵活性, 在开发中经常会出现孤零的变量和函数, 比如:
var a = function(){return b};
var b = 'hello';
var c = a();
function d(){
//...
}
所以我们常常见到这样组织代码的:
var property1 = 'a';
function method1(){...};
var property2 = 'b';
function method2(){...};
var property3 = '';
if(property1==='a'){
property3 = 'c';
var method3 = method1(property3);
}
再加上匿名函数和闭包的乱入:
//匿名函数和闭包举例
var property4 = 'd';
var method4 = method1(function(){
function(){
return property4;
}
})
即使是有名的js项目或者大神们编写的代码已经尽量清晰的去组织变量和代码, 当代码量过多时也必然会导致可读性下降. 本质原因是传统的js开发 将变量作为了组织代码的最小单位 .
sojs的思想是 将对象作为组织代码的最小单位 . 实际上, 已经有很多的开发者意识到了这一问题.比如在最新版本的jQuery中源码, 已经通过使用JSON对象提高了代码可读性:
jQuery.extend({
// Unique for each copy of jQuery on the page
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
// Assume jQuery is ready without the ready module
isReady: true,
error: function( msg ) {
throw new Error( msg );
},
...
noop: function() {}
});
由此可见, 使用JSON字面量创建对象, 是最自然的面向对象编程方式.
基于此, sojs诞生了.
名词解释-全限定性名: 命名空间+类名.比如类C, 所在的命名空间是A.B, 则C的全限定性名是A.B.C
下面是一个简单的node程序示例.
//node环境下引用sojs
require('node-sojs');
//定义cookie类
sojs.define({
name: 'cookie',
namespace: 'sojs.utility',
getCookie:function(key){...},
setCookie:function(key, value, option){...}
});
//使用cookie类, 因为cookie是静态类所以不需要实例化
var cookie = sojs.using('sojs.utility.cookie');
//使用cookie类的getCookie函数
var id = cookie.getCookie('id');
在sojs中, 使用JSON结构声明一个类. 通过sojs.define完成类的定义. 通过sojs.using获取到类的引用.
下面详细的讲解使用sojs的步骤.
sojs支持浏览器和node环境. 使用sojs的第一步就是引入sojs.
使用npm或者cnpm安装最新版本的sojs. 在项目根目录运行npm命令:
npm install sojs
在程序的入口处引用:
require('sojs');
整个程序进程只需要引用一次.
在浏览器环境下, 需要手动下载sojs文件. 项目地址:
https://github.com/zhangziqiu/sojs
sojs项目的bin目录下面, 有两个js文件:
将sojs.js下载到项目中后, 直接引用即可:
<script type="text/javascript" src="./bin/sojs.js"></script>
在 [项目根目录]/src/utility 目录下创建template.js文件, 内容如下:
//template类提供一个render方法用于模板和数据的拼接
sojs.define({
name: 'template', // node环境可忽略, name取值为文件名
namespace: 'utility', // node环境可忽略, namespace取值为相对于src的文件夹路径
render: function (source, data) {
var regexp = /{(.*?)}/g;
return source.replace(regexp, function (match, subMatch, index, s) {
return data[subMatch] || "";
});
}
});
上面就是一个完整类的代码. 在sojs中, js类文件都是以sojs.define开始, 里面包括一个JSON格式的对象. 开发者可以自由的添加属性和方法, 但是要注意 此JSON对象的以下属性是sojs框架使用的:
现在我们已经有了一个template类. 下面介绍如何在不同的环境下使用template类.
通常程序都有一个main函数作为程序入口, 在sojs中稍有不同, 借助sojs的依赖管理和静态构造函数, 我们可以构造一个入口类main.js:
sojs.define({
name: 'main',
deps: { template: 'utility.template' },
$main: function(){
var result = this.template.render('My name is {name}', {name:'ZZQ'});
console.log(result);
}
});
main.js可以放置在任意目录,而且也没有命名空间. main的静态构造函数$main作为程序的入口, 在静态构造函数中通过"this.template"引用到template类.
在node环境中运行main.js, 需要在main.js顶部添加引用sojs库的代码:
//引用sojs库
require('sojs');
//后面就是main类的完整代码.
sojs.define({
name: 'main',
deps: { template: 'utility.template' },
$main: function(){
var result = this.template.render('My name is {name}', {name:'ZZQ'});
console.log(result);
}
});
运行:
node main.js
即可看到输出结果:
My name is ZZQ
在node环境下, 当加载 utility.template 类时, 默认会从如下路径加载类文件:
[node运行目录]/src/utility/template.js
即将 [node运行根目录]/src 目录作为代码存放的根目录, 每一个命名空间就是一个文件夹.
假设项目目录就是网站的根目录, 并且网站名称是 localhost. 在根目录下创建main.html, 编写如下代码:
<!DOCTYPE html>
<html>
<body>
<!-- 引入sojs, 假设将sojs.js下载到了src目录中 -->
<script type="text/javascript" src="./src/sojs.js"></script>
<!-- 设置类文件根目录 -->
<script>
sojs.setPath('http://localhost/src/');
</script>
<script>
//下面是main.js的内容, 可以将main直接写在页面里
sojs.define({
name: 'main',
deps: { template: 'utility.template' },
$main: function(){
var result = this.template.render('My name is {name}', {name:'ZZQ'});
console.log(result);
}
});
</script>
</body>
</html>
打开 http://localhost/main.html页面, 即可在console控制台中看到:
My name is ZZQ
在浏览器中使用时, 需要设置类文件的根目录.sojs框架将从指定的根目录, 使用异步的方式加载类. 比如上面的例子加载 utility.template 类的地址是:
http://localhost/src/utility/template.js
如果main有多个依赖类, 会同时并行异步加载, 并且在所有的类都加载完毕后在运行$main静态构造函数.
通过上面实例的详细讲解, 已经可以使用sojs编写简单可维护的js代码了. 下面的篇幅会介绍一些sojs细节和高级用法.
使用sojs的项目代码是用类组织的.在入门示例中, template.js 加载时, 默认使用如下路径:
node: [node运行的目录]/src/utility/template.js
浏览器: [页面当前域名]/src/utility/template.js
即无论是node还是浏览器, 在项目的根目录里创建src文件夹, src内根据命名空间来组织文件夹.
sojs提供了setPath函数, 可以为特定的类或者命名空间指定自己特殊的加载路径.
//设置全局根目录
sojs.setPath('./src2/');
sojs.setPath('http://localhost/asset/js/');
//为 A 和 A.B 命名空间设置特殊的加载路径
sojs.setPath({
'A':'./src1/A/',
'A.B':'./src2/A/B/'
});
sojs的路径查找使用的是树状结构. 以"A.B.C"这个类的查找规则为例:
通过上面的查找行为可以看出, 优先使用更加精确的类的加载路径, 否则使用父路径.
bin目录中, 用于存放编译后的文件. bin目录下有以下两个js文件:
在bin的根目录下, 放置的是代码压缩后的js文件. 通常在项目中引用的就是代码压缩后的js文件. 同时为了应对一些比如调试等场景, bin目录下还包括如下文件夹:
上面每一个文件夹中, 也都包含 sojs.core.js和sojs.js两个文件(gzip目录下是.gz后缀), 可以根据需要使用.
src目录存放sojs的源码文件. sojs框架自身也是按照sojs的编程方式实现的, 即sojs是自解析的.
bin和src两个目录是最主要的文件夹. 除了这两个文件夹, sojs的项目根目录还包括如下文件:
.gitignore: git配置文件, 里面写明了git不需要管理的文件. 比如 node_modules 文件夹.
README.md: 说明文档. 即您正在阅读的这个页面的内容.
make.js: 编译文件. 使用 node make.js 命令, 可以从src目录生成bin目录下面的所有文件.
package.json: 包描述文件.
假设我们声明了一个类:
sojs.define({
name:'myClass',
word: 'Hello World',
say: function(){
alert(this.word);
}
})
myClss类有一个say函数, 输出myClass类的word属性. say函数中通过this引用myClass类自身. 这在通常情况下都是正确的.
但是在事件中, 比如常见的按钮单击事件, 或者一个定时器函数, this指针并不是总指向myClass自身的:
window.word = 'I am window';
var myClass = sojs.using('myClass');
setTimeout(myClass.say, 1000);
上面的代码会输出"I am window"而不是myClass类中的"Hello World". 因为在setTimeout中的this指向了window对象而不是myClass.
sojs提供了proxy函数用于解决this指针问题. 默认情况下为了使用方便, 会为function的原型添加proxy函数. 如果不希望对原型造成污染也可以通过配置取消此功能.
proxy函数用来修改事件的this指针. 比如上面的代码可以这样修改:
var myClass = sojs.using('myClass');
setTimeout(myClass.say.proxy(myClass), 1000);
使用了proxy之后, 可以正常的输出"Hello World".
proxy函数的第一个参数表示this指针需要指向的对象.
proxy函数还可以 修改事件处理函数的签名 , 下面举一个复杂的例子.
在nodejs中, 系统提供了socket对象, 用于网络编程.
var net = require('net');
var client = net.connect({port: 8124},
function() { //'connect' listener
console.log('client connected');
client.write('world!\r\n');
});
调用 net.connect函数时, 需要传递一个回调函数, 并且回调函数是无参数的. 通常, 使用上面的例子, 我们传递了一个匿名的回调函数, 并且在这个回调函数中使用 client变量, 此时会生成一个闭包, 以便在回调函数执行时, 可以正确访问到client变量.
使用proxy函数, 可以用一种 显式闭包 的方式, 将client作为参数传递, 让其看起来是通过参数传递的而不是使用闭包:
var net = require('net');
var client = net.connect({port: 8124},
function(mySocket) { //'connect' listener
console.log('client connected');
mySocket.write('world!\r\n');
}.proxy(this, client));
注意, 这里通过proxy除了传递this对象外, 还传递了client变量. connect原本的回调函数是没有签名的, 但是你会发现在回调函数执行时, mySocket可以被正常访问. 此时我们将原本无参数的事件处理函数, 变成了一个有参数的事件处理函数.
proxy函数看似神奇,其实内部还是使用闭包实现. 所以我在这里称其为 显式闭包 . 使用显示闭包极大的增加了代码的可读性和可维护性. 可以说显示闭包让邪恶的闭包从良了.
另外, 显示闭包还可以解决循环中使用闭包的常见错误, 看下面的例子: (参见:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures)
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus 事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。
该问题的原因在于赋给 onfocus 是闭包(showHelp)中的匿名函数而不是闭包对象;在闭包(showHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus 的回调被执行时,循环早已经完成,且此时 item 变量(由所有三个闭包所共享)已经指向了 helpText 列表中的最后一项。
使用proxy函数就不会出现上面的问题:
document.getElementById(item.id).onfocus = function(ev, item) {
showHelp(item.help);
}.proxy(this, item);
特别要注意, proxy函数只能在原有的事件处理函数后面新增参数. onfocus事件原本是包括一个事件对象参数的. 即上面的ev. 所以需要将item作为第二个函数参数使用.
相对于传统的node变成, 下面来看看使用sojs实现的完整的socket服务器的例子:
require('node-sojs');
sojs.define({
name: 'socketServer',
/**
* 静态构造函数
*/
$socketServer: function () {
var net = require('net');
//启动服务
this.server = net.createServer();
this.server.on('connection', this.onConnection.proxy(this));
this.server.on('error', this.onError.proxy(this));
this.server.listen(8088, function () {
base.log.info('server bound');
});
},
/**
* 服务器连接事件处理函数.
* @param {Object} socket 本次连接的socket对象
*/
onConnection: function (socket) {
socket.on('data', this.onData.proxy(this, socket));
socket.on('end', this.onEnd.proxy(this, socket));
socket.on('error', this.onError.proxy(this, socket));
},
/**
* socket接收数据事件处理函数.
* @param {Object} data 本次介绍到的数据对象buffer
* @param {Object} socket 本次连接的socket对象
*/
onData: function (data, socket) {
//do something...
},
/**
* socket 关闭事件处理函数.
* @param {Object} socket 本次连接的socket对象
*/
onEnd: function (socket) {
//do something...
},
/**
* socket 异常事件处理函数.
* @param {Object} err 异常对象
* @param {Object} socket 本次连接的socket对象
*/
onError: function (err, socket) {
//do something...
}
});
#sojs的原型继承和快速克隆 sojs中使用特有的快速克隆方法实现高效的对象创建. 主要用在内部的sojs.create函数中, 此函数用于创建一个类实例.
假设a是classA的一个实例. 此时的原型链情况如下:
a.contructor.prototype->classA
当访问a的一个属性时, 有以下几种情况:
为了解决此问题, sojs在创建classA的实例时, 会遍历classA的属性, 如果发现属性的类型是引用类型, 则对其进行快速克隆:
/**
* 快速克隆方法
* @public
* @method fastClone
* @param {Object} source 带克隆的对象. 使用此方法克隆出来的对象, 如果source对象被修改, 则所有克隆对象也会被修改
* @return {Object} 克隆出来的对象.
*/
fastClone: function (source) {
var temp = function () {};
temp.prototype = source;
var result = new temp();
}
传统的克隆对象是十分消耗性能的, sojs的最初也是用了传统的克隆方法. 最后改进成使用快速克隆方法.
假设这个属性为A, 此时相当于将属性A作为类, 创建了一个属性A的实例, 即关系是:
a.A.constructor.prototype -> classA.A
此时, 如果A的所有属性都不是引用类型, 则可以解决上面的赋值问题. 但是如果属性A本身, 又含有引用类型, 则同样会出现赋值是修改原形的问题. 假设: A.B为object 则通过 a.A.B 获取到的对象与 classA.A.B 获取到的对象是同一个对象. 对于a.A.B的修改同样会影响到classA.A.B 通过递归的快速克隆可以解决此问题, 但是因为性能开销太大, 所以sojs最后不支持多层的对象嵌套.
实际上, 我们可以通过编程方式来解决这个问题.
sojs.define({
name: 'classA'
A: null
classA:function(){
this.A = { B:1 }
}
});
所以一定要注意, 如果类的属性是对象, 并且是实例属性(运行时会被修改),则必须在动态构造函数中创建.
另改一个问题就是a对象的遍历. 同样因为使用了原型继承, 不能够通过hasOwnProperty来判断一个属性是否是实例a的. 可以通过遍历classA来解决:
for(var key in a){
if(key && typeof a[key] !== 'undefined' && classA.hasOwnProperty(key)){
//do something...
}
}
#事件编程 js中常常使用事件和异步, 在浏览器端的Ajax是异步, 在nodejs中更是到处都是异步事件.
在异步事件的编程中, 常常会遇到多层嵌套的情况. 比如:
var render = function (template, data, l10n) {
//do something...
};
$.get("template", function (template) {
// something
$.get("data", function (data) {
// something
$.get("l10n", function (l10n) {
// something
render(template, data, l10n);
});
});
});
在异步的世界里, 需要在回调函数中获取调用结果, 然后再进行接下来的处理流程, 所以导致了回调函数的多层嵌套, 并且只能串行处理.
sojs提供了sojs.event, 来解决此问题. 比如要实现上面的功能, 可以进行如下改造:
var render = function (template, data, l10n) {
//do something...
};
var ev = sojs.create(sojs.event);
ev.bind('l10n', function(data){
ev.emit('l10n', data);
});
ev.bind('data', function(data){
ev.emit('data', data);
});
ev.bind('template', function(data){
ev.emit('template', data);
});
//并行执行template, data和l10n事件, 都执行完毕后会触发group中的回调函数
ev.group('myGroup', ['template','data','l10n'], function(data){
render(data.template, data.data, data.l10n);
});
sojs.event的group可以将事件打包成一组. 在group的回调函数中, 会传递一个参数data, 这是一个object对象, 其中key为group中绑定的每一个事件名, value为事件的返回值. 所以可以通过data[事件名]获取到某一个事件的返回值.
sojs.event中的group还可以动态添加新的事件. 比如:
ev.group('myGroup', ['template','data','l10n'], function(data){
render(data.template, data.data, data.l10n);
});
ev.group('myGroup', ['another'], function(data){
anotherData = data.another;
});
注意上面的代码, 虽然为myGroup又添加了一个another事件. 但是此时mygroup绑定了两个事件处理函数, 这两个函数都会在所有事件完成时执行, 但是不一定哪个在前. 所以sojs.event还提供了afterGroup事件, 此事件会在所有group绑定的callback执行完毕后再执行:
ev.group('myGroup', ['template','data','l10n']);
ev.group('myGroup', ['another']);
ev.afterGroup('myGroup', function(data){
render(data.template, data.data, data.l10n, data.another);
});
sojs.event使用oo的思想实现. node中本身自带EventEmmiter也实现了部分功能.
使用event事件编程, 可以解决回调函数嵌套的问题. 但是是否有更好的解决办法呢? 答案是使用Promise.
通过举例, 来快速的了解什么是Promise.
传统的回调函数方式编程, 在这个例子中, step1,step2,step3 三个函数都是异步顺序执行的:
var step1,step2,step3 = function(data, callback){
var result = data + 1;
callback(result);
}
// step1开始执行
var data1 = 1;
step1(data1, function(data2){
// step2开始执行
step2(data2, function(data3){
// step3开始执行
step3(data3, function(data4){
// 输出最终执行结果
console.log(data4);
})
})
})
使用event事件编程, 绑定了一个事件序列:
var ev = sojs.create(sojs.event);
ev.bind('step1', function(data1){
// step1开始执行
var data2 = data1 + 1;
ev.emit('step2', data2);
});
ev.bind('step2', function(data2){
// step2开始执行
var data3 = data2 + 1;
ev.emit('step3', data3);
});
ev.bind('step3', function(data3){
// step3开始执行
var data4 = data3 + 1;
// 输出最终执行结果
console.log(data4);
});
// 从step1开始
ev.emit('step1', 1);
使用Promise编程, 使用then函数将事件串联起来:
// 创建了一个完成态的promise对象,下一个then会立刻执行,并且传递参数 1.
var promise = sojs.promise.resolve(1);
promise.then(function(data1){
// step1开始执行
var data2 = data1 + 1;
return data2;
}).then(function(data2){
// step2开始执行
var data3 = data2 + 1;
return data3;
}).then(function(data3){
// step3开始执行
var data4 = data3 + 1;
// 输出最终执行结果
console.log(data4);
})
从上面三个实例可见, event和promise都可以解决回调函数嵌套的问题.
不同的是, event需要创建三个事件, 以step1事件为例,需要在step1的事件处理函数中,显示的调用step2.即事件序列的执行迅 速分散在了每一个事件处理函数中.
而使用promise时, 是由一个promise对象通过调用then函数, 将step1-3串联起来, 整个事件的执行顺序集中在了一起管理.
有关promise的深入学习,推荐以下学习资料: 《JavaScript Promise迷你书(中文版)》 http://liubin.org/promises-book/#ch2-promise-all
oo不仅仅是一种编程方法, 而是组织代码的最小单位.
看几个使用AMD规范的例子就会明白, AMD中最后一个参数factory虽然美其名曰构造函数, 但是在这个函数中, 你可以做任何事情:创建局部function, function中再嵌套function, 使用闭包, 处理一些业务逻辑. 最后的结果是这个factory不易阅读和维护.
究其原因, js编程很容易陷入面向过程编程的方式中. 而AMD等规范只注重"模块"的开发, 却忽视了一个模块内部的代码如何组织和管理.
js中让代码不易管理的几个杀手包括: 闭包, 零散的函数对象, 异步机制(node中尤其重要).
sojs使用oo的思想, 减少闭包的使用, 让每一个函数对象都挂靠在类对象上, 减少孤零的函数对象的存在. 再配合sojs.event的事件机制, 解决异步编程中的事件嵌套噩梦.
可以说sojs为js的大规模开发提供了有效地基础保障.
sojs还在发展中, 我们尽量不在核心的sojs.js中加入过多的功能, 保持核心精简. 同时通过sojs团队成员的努力, 让sojs适用于更多的场景.
欢迎有志之士加入到sojs的开发中来!
FAQs
Simple Object-Oriented JavaScript
The npm package sojs receives a total of 24 weekly downloads. As such, sojs popularity was classified as not popular.
We found that sojs demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.

Security News
/Research
Mini Shai-Hulud expands into the Go ecosystem after hitting LeoPlatform npm packages and targeting GitHub Actions workflows.