在class出现之前,我们实现一个类通常通过定义1个function A,并在A.prototype上定义方法和属性,例如:
function Browser (soft) {
///<summary>软件api</summary>
///<field name="parent" type="ZXYSoft">soft根</field>
///<field name="Size" type="Sizes">各种窗体大小值</field>
///<field name="Location" type="Locations">各种窗体坐标值</field>
this.parent = soft;
this._ver = undefined;
this.Size = new Sizes();
this.Location = new Locations();
};
Browser.prototype._webkitVer = function () {
/// <summary>webkit底层版本号(1000表示旧版本,1001表示qt-webkit)</summary>
/// <returns type="Number" />
if (this._ver == null) {
if (this.parent.isApp) {
var ver = this.parent.APP.ver;
this._ver = typeof ver === "number" ? ver : 1000;
} else {
this._ver = 1000;
}
}
return this._ver;
};
Browser.prototype.appname = function () {
///<summary>应用名(exe名)</summary>
///<returns type="String" />
if (this.parent.isApp) {
var appname = this.parent.APP.appname;
return typeof appname === "string" ? appname : "";
}
return "";
};在2014年的时候,公司接入了第一个Hybrid App-新浪页游助手(webkit[vc]+html+js),前端基础库由我负责实现,当时的基础库的编写大量应用了prototype,我按功能模块将他们封装在不同使用闭包中,下面为截取的一部分源码:
//窗体设置相关
Browser.prototype.SetWindowPos = function (hWndlnsertAfter, x, y, cx, cy, ulflag) {
///<param type="Number" name="hWndlnsertAfter">在z序中的位于被置位的窗口前的窗口句柄。 参看枚举》Enum.SetWindowPosOrder</param>
///<param type="Number" name="x">以客户坐标指定窗口新位置的左边界</param>
///<param type="Number" name="y">以客户坐标指定窗口新位置的顶边界</param>
///<param type="Number" name="cx">以像素指定窗口的新的宽度</param>
///<param type="Number" name="cy">以像素指定窗口的新的高度</param>
///<param type="Number" name="ulflag">窗口尺寸和定位的标志。 参看枚举》Enum.SetWindowPosUlflag</param>
///<returns type="Boolean">此处无返回值</returns>
if (this.parent.isApp/* && typeof this.parent.APP.setWindowPos === "function"*/) {
return this.parent.APP.setWindowPos(hWndlnsertAfter, x, y, cx, cy, ulflag) === 1;
}
return false;
};
Browser.prototype.setTopMost = function (isTopMost) {
/// <summary>设置窗体置顶</summary>
/// <param name="isTopMost" type="Boolean">是否置顶</param>
/// <returns type="Boolean">是否设置成功</returns>
var type = isTopMost ? Enum.SetWindowPosOrder.HWND_TOPMOST : Enum.SetWindowPosOrder.HWND_NOTOPMOST;
return this.SetWindowPos(type, 0, 0, 0, 0, Enum.SetWindowPosUlflag.SWP_NOMOVE | Enum.SetWindowPosUlflag.SWP_NOSIZE | Enum.SetWindowPosUlflag.SWP_NOACTIVATE);
};
Browser.prototype.setWindowSize = function (x, y, width, height) {
///<summary>设置窗体位置和大小</summary>
///<param type="Number" name="x">以客户坐标指定窗口新位置的左边界。</param>
///<param type="Number" name="y">以客户坐标指定窗口新位置的顶边界。</param>
///<param type="Number" name="width">以像素指定窗口的新的宽度。</param>
///<param type="Number" name="height">以像素指定窗口的新的高度。</param>
///<returns type="Boolean">是否设置成功</returns>
if (this.parent.isApp/* && typeof this.parent.APP.setWindowSize === "function"*/) {
return this.parent.APP.setWindowSize(x, y, width, height) === 1;
}
return false;
};//坐标相关
Browser.prototype.getScreenCenterLocation = function (width, height) {
///<summary>根据指定的矩形窗口大小返回居中于屏幕(包含任务栏)的坐标</summary>
///<returns type="Object" />
var s = this.Size.screen;
var x = parseInt((s.width() - width) / 2);
var y = parseInt((s.height() - height) / 2);
return { x: x, y: y }
};
Browser.prototype.getDesktopCenterLocation = function (width, height) {
///<summary>根据指定的矩形窗口大小返回居中于桌面(不含任务栏)的坐标</summary>
///<returns type="Object" />
var s = this.Size.desktop;
var x = parseInt((s.width() - width) / 2);
var y = parseInt((s.height() - height) / 2);
return { x: x, y: y }
};
Browser.prototype.getAppCenterLocation = function (width, height) {
///<summary>根据指定的矩形窗口大小返回居中于当前窗口的坐标(若窗体居中位置在屏幕外会自动被修正到屏幕边缘)</summary>
///<returns type="Object" />
var s = this.Size.window;
var p = this.Location.window;
var x_max = this.Size.desktop.width() - width;
var y_max = this.Size.desktop.height() - height;
var x = p.x() + parseInt((s.width() - width) / 2);
var y = p.y() + parseInt((s.height() - height) / 2);
if (x < 0) {
x = 0;
} else if (x > x_max) {
x = x_max;
}
if (y < 0) {
y = 0;
} else if (y > y_max) {
y = y_max;
}
return { x: x, y: y }
};
前端模块化兴起,webpack大行其道。2019年5月,终于到了需要在代码层重构该库的时候,如何将这超过3400行的代码重构成符合ES6规范的并且能拆解成若干模块的文件成为了第一道拦路虎。
我们知道Class可以通过extends关键字继承来自其他类的所有属性和方法,参看:http://es6.ruanyifeng.com/#docs/class-extends ,但这也产生了根本问题,我可能按功能模块拆分出了N个js文件(N>2),如何将他们合并到1个Class供外部统一使用?最直观的方法可能如下
//child2.js
class Child2 {
constructor(){}
}
export {Child2};//child1.js
import { Child2 } from "./child2.js";
class Child1 extends Child2 {
constructor(){}
}
export {Child1};//browser.js
import { Child1 } from "./child1.js";
class Browser extends Child1{
constructor(){}
}上述代码虽然按模块拆分了功能,但是各子模块间存在extends,没有真正的分离,被我抛弃了,我在阮一峰大拿关于Class的继承章节的结尾找到了关于Mixin 模式的实现,做了一些细微的修改:
//utils.js
/***
* mixins class
* @param {...Class} mixins
*/
function mixinsClass(...mixins) {
class MixinClass {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin(this)); // 拷贝实例属性 同时执行内部初始化
}
}
};
let proto = MixinClass.prototype;
for (let mixin of mixins) {
copyProperties(MixinClass, mixin); // 拷贝静态属性
copyProperties(proto, mixin.prototype); // 拷贝原型属性
}
return MixinClass;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if (key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
export {
mixinsClass,
}和阮一峰提供的范例版本相比,唯一差别在
copyProperties(this, new mixin(this)); // 我将this指针传入到各组成成员的constructor中,这样方便子模块对新对象做一些数据绑定。

以下为一些代码选段:
//soft.js
import { mixinsClass } from "./utils.js";
import { Enum } from "./Enum.js";
//mixins-import
import { DataFormat } from "./soft-mixins/DataFormat.js";
import { Version } from "./soft-mixins/Version.js";
import { Debug } from "./soft-mixins/Debug.js";
import { Method } from "./soft-mixins/Method.js";
import { IframeListener } from "./soft-mixins/IframeListener.js";
import { BossKey } from "./soft-mixins/BossKey.js";
import { JSONP } from "./soft-mixins/JSONP.js";
import { GetIframeData } from "./soft-mixins/GetIframeData.js";
import { Ajax } from "./soft-mixins/Ajax.js";
import { PostMessage } from "./soft-mixins/PostMessage.js";
/**
* 软件框架级接口层
* @augments Method
* @augments DataFormat
* @augments Version
* @augments Debug
* @augments IframeListener
* @augments BossKey
* @augments JSONP
* @augments GetIframeData
* @augments Ajax
* @augments PostMessage
*
*/
class Soft extends mixinsClass(
Method,
DataFormat,
Version,
Debug,
IframeListener,
BossKey,
JSONP,
GetIframeData,
Ajax,
PostMessage
) {
/**
* 当前页面是否运行在软件中
*/
isApp = false;
/**
* 软件接口根
* @type {Object}
*/
APP = null;
/**
* 枚举、常量值
*/
Enum = Enum;
/**
* url模块
*/
Url = new Url();
/**
* 软件层交互
*
* @type {Browser}
*/
get Browser(){
if(this._Browser==null){
this._Browser=new Browser(this);
}
return this._Browser;
}
}
export {
Soft,
Enum,
}//Version.js
//flash检测版本比对模块
class Version {
/**
* 获取Flash版本号 不存在返回undefined
* @returns {String|Undefined}
*/
getFlashVersion () {
let result;
for (let i =0,length=navigator.plugins.length;i<length;i++) {
let tmp = navigator.plugins[i];
if (tmp && typeof tmp.name === "string" && tmp.name.toLowerCase() === "shockwave flash") {
let filename = tmp.filename;
if (typeof filename === "string") {
if (/^NPSWF\d+(_\d+)+.dll$/gi.test(filename)) {
filename = filename.replace(/^NPSWF\d+((?:(?:_|\.|-)\d+)+).dll$/gi, "$1").replace(/_/gi, ".");
return filename.substr(1);
} else {
let description = tmp.description;
if (/^Shockwave Flash \d+(\.\d+)+ /gi.test(description)) {//可能有多版本 不能直接return
result = description.replace(/^Shockwave Flash (\d+(?:\.\d+)+) .*$/gi, "$1");
}
}
}
};
}
return result;
};
/**
* 比较versionA和versionB的版本,如果A的版本高于或等于B版本,返回true
* @param {String} versionA 版本versionA eg:1.1 1.1.1 1.1.1.1
* @param {String} versionB 版本versionB eg:1.1 1.1.1 1.1.1.1
* @returns {Boolean}
*/
compareVersion (versionA, versionB) {
if (typeof versionA !== "string") return false;
if (typeof versionB !== "string") return true;
let verReg = /^\d{1,7}(\.\d{1,7})*$/i;
if (!verReg.test(versionA)) return false;
if (!verReg.test(versionB)) return true;
versionA = versionA.split(".");
versionB = versionB.split(".");
let lengthA = versionA.length;
let lengthB = versionB.length;
if (lengthA > lengthB) {
for (let i = lengthB; i < lengthA; i++) {
versionB.push("0");
}
lengthB = lengthA;
} else if (lengthA < lengthB) {
for (let i = lengthA; i < lengthB; i++) {
versionA.push("0");
}
lengthA = lengthB;
}
for (let i = 0; i < lengthA; i++) {
let tmpA = parseInt(versionA[i]);
let tmpB = parseInt(versionB[i]);
if (tmpA > tmpB) {
return true;
} else if (tmpA < tmpB) {
return false;
}
}
return true;
};
}
export {Version};//method.js
/**
* 提供给软件的交互api
*/
let openapi = {};
//软件交互相关方法
class Method{
/**
*
* @param {*} extendCls 最终继承类
*/
constructor(extendCls){
/**
* 注册到window提供给软件端调用
*/
window.invokeMethod = function (modulename, methodname, parm, parmIsJSON) {
return this.data2String(this.invokeWebMethod(modulename, methodname, parm, parmIsJSON));
}.bind(extendCls);
}
/**
* 获取指定的已注册的接口
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @returns {Function|Undefined}
*/
getWebMethod(modulename, methodname){
if (typeof modulename !== "string" || modulename.length<1 || typeof methodname !== "string"||methodname.length<1) return;
let fun = openapi[modulename];
if (typeof fun != "object") return;
fun = fun[methodname];
if (typeof fun !== "function") return;
return fun;
}
//web提供给软件端的接口
/**
* 插入1个可供软件端调用(invokeMethod)的api
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {Function} method 函数
*/
addWebMethod (modulename, methodname, method) {
if (typeof modulename !== "string" || typeof methodname !== "string" || typeof method !== "function") return false;
if (!openapi[modulename]) openapi[modulename] = {};
delete openapi[modulename][methodname];
if (modulename !== "ServerData" && modulename !== "ServerJSONP" && modulename !== "DEBUG") {
let name = "WM_" + modulename + "_" + methodname;
delete this[name];
this[name] = method;
name = undefined;
}
openapi[modulename][methodname] = method;
modulename = methodname = method = null;
return true;
};
/**
* 移除1个可供软件端调用(invokeMethod)的api
* @param {String} modulename 模块名
* @param {String} methodname 方法名
*/
removeWebMethod (modulename, methodname) {
if (typeof modulename !== "string" || typeof methodname !== "string") return false;
if (!openapi[modulename]) return false;
if (openapi[modulename][methodname] === undefined) return false;
if (!delete openapi[modulename][methodname]) {
openapi[modulename][methodname] = null;
}
if (modulename !== "ServerData" && modulename !== "ServerJSONP" && modulename !== "DEBUG") {
let name = "WM_" + modulename + "_" + methodname;
delete this[name];
name = undefined;
}
return true;
}
/**
* 引发addWebMethod的方法
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {*} parm 传递的参数信息
* @param {Boolean} [parmIsJSON] 指示parm是否就是字符串,无需转义(当遇到base64图片流的时候请使用该项),默认为true,将尝试将JSON.parse(parm)
*/
invokeWebMethod (modulename, methodname, parm, parmIsJSON) {
if (typeof modulename !== "string" || typeof methodname !== "string") return undefined;
let fun = openapi[modulename];
if (!fun) {
return undefined;
}
fun = fun[methodname];
if (typeof fun !== "function") {
return undefined;
}
(parmIsJSON === undefined || parmIsJSON === true) && (parm = this.string2Data(parm));
return fun.call(this, parm);
};
//软件端提供给web的接口
/** 调用软件端方法 (异步,无返回值)
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {Object} parm 传递的参数信息
*/
invokeSoftMethodAsync(modulename, methodname, parm) {
};
/**
* 调用软件端方法
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {Object} parm 传递的参数信息
* @param {Boolean} [resultIsJSON] 指示是否尝试转义软件返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true
*/
invokeSoftMethod (modulename, methodname, parm, resultIsJSON) {
};
/**
* 跨窗体调用其他窗体前端注册的方法(addWebMethod) (异步,无返回值)
* @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {*} parm 传递的参数信息
* @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true
*/
crossWindowInvokeWebMethodAsync = function (winType, modulename, methodname, parm, parmIsJSON) {
};
/**
* 跨窗体调用其他窗体前端注册的方法(addWebMethod)
* @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {*} parm 传递的参数信息
* @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true
* @param {Boolean} [resultIsJSON] 指示是否尝试转义返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true
*/
crossWindowInvokeWebMethod (winType, modulename, methodname, parm, parmIsJSON, resultIsJSON) {
};
/**
* 跨窗体调用其他窗体的指定Iframe下前端注册的方法(addWebMethod) (异步,无返回值)
* @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN
* @param {String} frameId iframe的id (请注意,不支持孙级iframe,仅支持子级)
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {*} parm 传递的参数信息
* @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true
*/
crossWindowInvokeWebMethod2Async(winType, frameId, modulename, methodname, parm, parmIsJSON) {
};
/**
* 跨窗体调用其他窗体的指定Iframe下前端注册的方法(addWebMethod)
* @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN
* @param {String} frameId iframe的id (请注意,不支持孙级iframe,仅支持子级)
* @param {String} modulename 模块名
* @param {String} methodname 方法名
* @param {*} parm 传递的参数信息
* @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true
* @param {Boolean} [resultIsJSON] 指示是否尝试转义返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true
*/
crossWindowInvokeWebMethod2(winType, frameId, modulename, methodname, parm, parmIsJSON, resultIsJSON) {
};
}
export {Method};不幸的是,JSDoc 似乎目前仅支持标记1个 @augments(@extends) ,智能提示方面可能需要编写.d.ts

