namespace.js Javascript的命名空间库


Posted in Javascript onOctober 11, 2011

github:https://github.com/hirokidaichi/namespace-js
定义Namespace对象:
var Namespace
现在来具体看一下Namespace对象的定义,它是一个NamespaceDefinition对象。该对象是一个函数对象(NamespaceDefinition对象的构造函数,如果不给参数的话就默认生成一个main的命名空间),还有三个属性,Object,Definition,Proc。其值依次为NamespaceObjectFactory,NamespaceDefinition,createProcedure函数对象类。

196 var createNamespace = function(fqn){ 
197 return new NamespaceDefinition( 
198 NamespaceObjectFactory.create(fqn || 'main') 
199 ); 
200 }; 
201 merge(createNamespace, { 
202 'Object' : NamespaceObjectFactory, 
203 Definition: NamespaceDefinition, 
204 Proc : createProcedure 
205 });

NamespaceObjectFactory:根据fqn生成NamespaceObject对象。
NamespaceObjectFactory对象只有一个create方法,参数就是命名空间的名字(Fully Qualified Name)。该方法有一个闭包环境,该环境里有一个cache变量用于缓存所有生成的NamespaceObject对象。
一个NamespaceObject对象包含有三个属性,stash(记录当前namespace),fqn(namespace名字),proc(createProcedure对象)。方法包括:enqueue,call,valueof,merge,getStash,getExport
74 var NamespaceObject = function _Private_Class_Of_NamespaceObject(fqn){ 
75 merge(this, { 
76 stash: { CURRENT_NAMESPACE : fqn }, 
77 fqn : fqn, 
78 proc : createProcedure() 
79 }); 
80 }; 
81 merge(NamespaceObject.prototype, { 
82 enqueue: function(context) { 
83 this.proc.next(context); 
84 }, 
85 call: function(state,callback) { 
86 this.proc.call(state, callback); 
87 }, 
88 valueOf: function() { 
89 return "#NamespaceObject<" + this.fqn + ">"; 
90 }, 
91 merge: function(obj) { 
92 merge(this.stash,obj); 
93 return this; 
94 }, 
95 getStash: function() { 
96 return this.stash; 
97 }, 
98 getExport: function(importName) { 
99 if (importName === '*') return this.stash; 
100 
101 var importNames = importName.split(/,/), 
102 retStash = {}; 
103 for(var i = 0,l=importNames.length;i<l;i++){ 
104 retStash[ importNames[i] ] = this.stash[ importNames[i] ]; 
105 } 
106 return retStash; 
107 } 
108 }); 
109 var NamespaceObjectFactory = (function() { 
110 var cache = {}; 
111 return { 
112 create :function(fqn){ 
113 _assertValidFQN(fqn); 
114 return (cache[fqn] || (cache[fqn] = new NamespaceObject(fqn))); 
115 } 
116 }; 
117 })();

NamespaceDefinition:
该对象包括5个属性,namespaceObject,requires,useList,stash,defineCallback。并提供了相关的方法:use,_mergeStashWithNS,loadImport,define,getStash,valueOf,apply
该对象是namespace库的核心,它提供了所有需要的方法。
初始化的时候,在NamespaceObject的proc的steps队列里追加一个函数对象,该函数将调用apply方法。
119 var NamespaceDefinition = function _Private_Class_Of_NamespaceDefinition(nsObj) { 
120 merge(this, { 
121 namespaceObject: nsObj, 
122 requires : [], 
123 useList : [], 
124 stash : {}, 
125 defineCallback : undefined 
126 }); 
127 var _self = this; 
128 nsObj.enqueue(function($c){ _self.apply($c); }); 
129 }; 
130 merge(NamespaceDefinition.prototype, { 
131 use: function(syntax){ 
132 this.useList.push(syntax); 
133 var splitted = syntax.split(/\s+/); 
134 var fqn = splitted[0]; 
135 var importName = splitted[1]; 
136 _assertValidFQN(fqn); 
137 this.requires.push(function($c){ 
138 var context = this; 
139 var require = NamespaceObjectFactory.create(fqn); 
140 require.call(this,function(state){ 
141 context.loadImport(require,importName); 
142 $c(); 
143 }); 
144 }); 
145 return this; 
146 }, 
147 _mergeStashWithNS: function(nsObj){ 
148 var nsList = nsObj.fqn.split(/\./); 
149 var current = this.getStash(); 
150 
151 for(var i = 0,l=nsList.length;i<l-1;i++){ 
152 if( !current[nsList[i]] ) current[nsList[i]] = {}; 
153 current = current[nsList[i]]; 
154 } 
155 
156 var lastLeaf = nsList[nsList.length-1]; 
157 current[lastLeaf] = merge(current[lastLeaf] || {}, nsObj.getStash()); 
158 }, 
159 loadImport: function(nsObj,importName){ 
160 if( importName ){ 
161 merge( this.stash, nsObj.getExport(importName) ); 
162 }else{ 
163 this._mergeStashWithNS( nsObj ); 
164 } 
165 }, 
166 define: function(callback){ 
167 var nsDef = this, nsObj = this.namespaceObject; 
168 this.defineCallback = function($c) { 
169 var ns = { 
170 provide : function(obj){ 
171 nsObj.merge(obj); 
172 $c(); 
173 } 
174 }; 
175 merge(ns, nsDef.getStash()); 
176 merge(ns, nsObj.getStash()); 
177 callback(ns); 
178 }; 
179 }, 
180 getStash: function(){ 
181 return this.stash; 
182 }, 
183 valueOf: function(){ 
184 return "#NamespaceDefinition<"+this.namespaceObject+"> uses :" + this.useList.join(','); 
185 }, 
186 apply: function(callback){ 
187 var nsDef = this; 
188 createProcedure(nsDef.requires) 
189 .next(nsDef.defineCallback) 
190 .call(nsDef,function(){ 
191 callback( nsDef.getStash() ); 
192 }); 
193 } 
194 });

createProcedure:该对象是一个函数对象,返回Procedure对象的next方法的结果。
Procedure:
Procedure对象有三个属性:state,steps,_status(默认为init)。提供一下几种方法:next,isRunnig,enqueue,dequeue,call,_invoke。
1 var Namespace = (function(){ 
2 /* utility */ 
3 var merge = function(target, source){ // 把source的所有自身属性copy到target中去,并返回target对象 4 for(var p in source) 
5 if(source.hasOwnProperty( p )) target[p] = source[p]; 
6 return target; 
7 }; 
8 var _assertValidFQN = function(fqn){ // 验证namespace名字的有效性,必须是小写英数字,下划线和点 
9 if(!(/^[a-z0-9_.]+/).test(fqn)) throw('invalid namespace'); 
10 }; 
11 
12 var Procedure = function _Private_Class_Of_Proc(){ 
13 merge(this, { 
14 state : {}, // 状态 
15 steps : [], // 存储状态的数组 
16 _status: 'init' 
17 }); 
18 }; 
19 merge(Procedure.prototype, { 
20 next: function(state){ // 如果state有值,存入到steps队列队尾,然后返回this 
21 if(state) this.enqueue(state); 
22 return this; 
23 }, 
24 isRunning: function(){ // 判定是否为running状态 
25 return (this._status === 'running'); 
26 }, 
27 enqueue: function(state){ // 这里其实是用数组steps来模拟队列,enqueue就是从队尾入队列 
28 this.steps.push(state); 
29 }, 
30 dequeue: function(){ // dequeue就是从对头出队列 
31 return this.steps.shift(); 
32 }, 
33 call: function(initialState,callback){ // 
34 if( this.isRunning() ) throw("do not run twice"); 
35 
36 this.state = initialState || {}; // 保存当前state(NamespaceDefinition对象) 
37 this.enqueue(function($c){ // 函数入队列steps 
38 $c(); // 执行传进来的函数 
39 if(callback)callback(this); // 如果存在回调函数的,执行回调函数 
40 }); 
41 this._status = 'running'; // 设置状态为running 
42 this._invoke(); // 调用_invoke 
43 }, 
44 _invoke: function(){ 
45 var _self = this; 
46 var step = _self.dequeue(); // 对象出队列(FIFO) 
47 if( !step ){ 
48 _self._status = 'finished'; // 如果队列为空,则设置状态为finished 
49 return; 
50 } // step是数组的情况下不走这条路径 
51 if( step.call ) { // 如果该对象是函数对象的时候,则执行该call方法,并指定内部this为_self.state,回调函数里面继续调用_invoke 
52 return step.call( _self.state,function _cont(state){ 
53 if( state ) _self.state = state; 
54 _self._invoke(); 
55 }); 
56 } 
57 var finishedProcess = 0; 
58 if( step.length === 0 ) _self._invoke(); // 如果该数组长度为0,则调用_invoke 
59 for(var i =0,l=step.length;i<l;i++){ // 针对数组里的所有对象(函数对象)调用各自的call方法,并指定各自函数的this为_self.state对象(nsDef对象) 
60 step[i].call(_self.state,function _joinWait(){ 
61 finishedProcess++; 
62 if( finishedProcess == l ){ 
63 _self._invoke(); 
64 } 
65 }); 
66 } 
67 } 
68 }); 
69 
70 var createProcedure = function(state) { 
71 return new Procedure().next(state); 
72 }; 
73 
74 var NamespaceObject = function _Private_Class_Of_NamespaceObject(fqn){ 
75 merge(this, { 
76 stash: { CURRENT_NAMESPACE : fqn }, 
77 fqn : fqn, 
78 proc : createProcedure() 
79 }); 
80 }; 
81 merge(NamespaceObject.prototype, { 
82 enqueue: function(context) { 
83 this.proc.next(context); 
84 }, 
85 call: function(state,callback) { 
86 this.proc.call(state, callback); 
87 }, 
88 valueOf: function() { 
89 return "#NamespaceObject<" + this.fqn + ">"; 
90 }, 
91 merge: function(obj) { 
92 merge(this.stash,obj); 
93 return this; 
94 }, 
95 getStash: function() { 
96 return this.stash; 
97 }, 
98 getExport: function(importName) { 
99 if (importName === '*') return this.stash; 
100 
101 var importNames = importName.split(/,/), 
102 retStash = {}; 
103 for(var i = 0,l=importNames.length;i<l;i++){ 
104 retStash[ importNames[i] ] = this.stash[ importNames[i] ]; 
105 } 
106 return retStash; 
107 } 
108 }); 
109 var NamespaceObjectFactory = (function() { 
110 var cache = {}; 
111 return { 
112 create :function(fqn){ 
113 _assertValidFQN(fqn); 
114 return (cache[fqn] || (cache[fqn] = new NamespaceObject(fqn))); 
115 } 
116 }; 
117 })(); 
118 
119 var NamespaceDefinition = function _Private_Class_Of_NamespaceDefinition(nsObj) { 
120 merge(this, { 
121 namespaceObject: nsObj, 
122 requires : [], 
123 useList : [], 
124 stash : {}, 
125 defineCallback : undefined 
126 }); 
127 var _self = this; 
128 nsObj.enqueue(function($c){ _self.apply($c); }); 
129 }; 
130 merge(NamespaceDefinition.prototype, { 
131 use: function(syntax){ // 使用namespace 
132 this.useList.push(syntax); // 该namespace字符串存入数组useList 
133 var splitted = syntax.split(/\s+/); // namespace和它的对象使用空格分开 
134 var fqn = splitted[0]; // 取得namespace 
135 var importName = splitted[1]; // 取得namespace中的对象 
136 _assertValidFQN(fqn); 
137 this.requires.push(function($c){ // 放一个函数到requires数组中 
138 var context = this; 
139 var require = NamespaceObjectFactory.create(fqn); // 获取指定的NamespaceObject对象,之前生成过得对象可以直接从缓存中获取 
140 require.call(this,function(state){ // 调用NamespaceObject对象的call方法 
141 context.loadImport(require,importName); 
142 $c(); 
143 }); 
144 }); 
145 return this; 
146 }, 
147 _mergeStashWithNS: function(nsObj){ 
148 var nsList = nsObj.fqn.split(/\./); 
149 var current = this.getStash(); 
150 
151 for(var i = 0,l=nsList.length;i<l-1;i++){ 
152 if( !current[nsList[i]] ) current[nsList[i]] = {}; 
153 current = current[nsList[i]]; 
154 } 
155 
156 var lastLeaf = nsList[nsList.length-1]; 
157 current[lastLeaf] = merge(current[lastLeaf] || {}, nsObj.getStash()); 
158 }, 
159 loadImport: function(nsObj,importName){ 
160 if( importName ){ 
161 merge( this.stash, nsObj.getExport(importName) ); 
162 }else{ 
163 this._mergeStashWithNS( nsObj ); 
164 } 
165 }, 
166 define: function(callback){ 
167 var nsDef = this, nsObj = this.namespaceObject; 
168 this.defineCallback = function($c) { // 给defineCallback赋值,同时定义一下该回调函数的上下文,nsDef和nsObj两个对象。 
169 var ns = { 
170 provide : function(obj){ 
171 nsObj.merge(obj); 
172 $c(); 
173 } 
174 }; 
175 merge(ns, nsDef.getStash()); 
176 merge(ns, nsObj.getStash()); 
177 callback(ns); 
178 }; 
179 }, 
180 getStash: function(){ 
181 return this.stash; 
182 }, 
183 valueOf: function(){ 
184 return "#NamespaceDefinition<"+this.namespaceObject+"> uses :" + this.useList.join(','); 
185 }, 
186 apply: function(callback){ 
187 var nsDef = this; 
188 createProcedure(nsDef.requires) 
189 .next(nsDef.defineCallback) 
190 .call(nsDef,function(){ 
191 callback( nsDef.getStash() ); 
192 }); 
193 } 
194 }); 
195 
196 var createNamespace = function(fqn){ 
197 return new NamespaceDefinition( 
198 NamespaceObjectFactory.create(fqn || 'main') 
199 ); 
200 }; 
201 merge(createNamespace, { 
202 'Object' : NamespaceObjectFactory, 
203 Definition: NamespaceDefinition, 
204 Proc : createProcedure 
205 }); 
206 return createNamespace; 
207 })();

追加定义Namespace支持的方法:
Namespace.use
Namespace.fromInternal
Namespace.GET
Namespace.fromExternal
1 Namespace.use = function(useSyntax){ return Namespace().use(useSyntax); } 
2 Namespace.fromInternal = Namespace.GET = (function(){ 
3 var get = (function(){ 
4 var createRequester = function() { 
5 var xhr; 
6 try { xhr = new XMLHttpRequest() } catch(e) { 
7 try { xhr = new ActiveXObject("Msxml2.XMLHTTP.6.0") } catch(e) { 
8 try { xhr = new ActiveXObject("Msxml2.XMLHTTP.3.0") } catch(e) { 
9 try { xhr = new ActiveXObject("Msxml2.XMLHTTP") } catch(e) { 
10 try { xhr = new ActiveXObject("Microsoft.XMLHTTP") } catch(e) { 
11 throw new Error( "This browser does not support XMLHttpRequest." ) 
12 } 
13 } 
14 } 
15 } 
16 } 
17 return xhr; 
18 }; 
19 var isSuccessStatus = function(status) { 
20 return (status >= 200 && status < 300) || 
21 status == 304 || 
22 status == 1223 || 
23 (!status && (location.protocol == "file:" || location.protocol == "chrome:") ); 
24 }; 
25 
26 return function(url,callback){ 
27 var xhr = createRequester(); 
28 xhr.open('GET',url,true); 
29 xhr.onreadystatechange = function(){ 
30 if(xhr.readyState === 4){ 
31 if( isSuccessStatus( xhr.status || 0 )){ 
32 callback(true,xhr.responseText); 
33 }else{ 
34 callback(false); 
35 } 
36 } 
37 }; 
38 xhr.send('') 
39 }; 
40 })(); 
41 
42 return function(url,isManualProvide){ 
43 return function(ns){ 
44 get(url,function(isSuccess,responseText){ 
45 if( isSuccess ){ 
46 if( isManualProvide ) 
47 return eval(responseText); 
48 else 
49 return ns.provide( eval( responseText ) ); 
50 }else{ 
51 var pub = {}; 
52 pub[url] = 'loading error'; 
53 ns.provide(pub); 
54 } 
55 }); 
56 }; 
57 }; 
58 })(); 
59 
60 Namespace.fromExternal = (function(){ 
61 var callbacks = {}; 
62 var createScriptElement = function(url,callback){ 
63 var scriptElement = document.createElement('script'); 
64 
65 scriptElement.loaded = false; 
66 
67 scriptElement.onload = function(){ 
68 this.loaded = true; 
69 callback(); 
70 }; 
71 scriptElement.onreadystatechange = function(){ 
72 if( !/^(loaded|complete)$/.test( this.readyState )) return; 
73 if( this.loaded ) return; 
74 scriptElement.loaded = true; 
75 callback(); 
76 }; 
77 scriptElement.src = url; 
78 document.body.appendChild( scriptElement ); 
79 return scriptElement.src; 
80 }; 
81 var domSrc = function(url){ 
82 return function(ns){ 
83 var src = createScriptElement(url,function(){ 
84 var name = ns.CURRENT_NAMESPACE; 
85 var cb = callbacks[name]; 
86 delete callbacks[name]; 
87 cb( ns ); 
88 }); 
89 } 
90 }; 
91 domSrc.registerCallback = function(namespace,callback) { 
92 callbacks[namespace] = callback; 
93 }; 
94 return domSrc; 
95 })(); 
96 
97 try{ module.exports = Namespace; }catch(e){}

具体看一个例子:
1 Namespace('logtest') 
2 .define(function (ns) { 
3 console.log(2); 
4 ns.provide({ 
5 log : function () { console.log(3); } 
6 }); 
7 }); 
8 
9 console.log(4); 
10 
11 Namespace 
12 .use('logtest') 
13 .apply( function (ns) { 
14 console.log(5); 
15 ns.logtest.log(); 
16 });

1:Namespace('logtest') => new NamespaceDefinition(NamespaceObjectFactory.create('logtest'))
即生成一个NamespaceDefinition对象,该对象是由NamespaceObject对象来初始化的,该对象同时还有三个属性,Object,Definition,Proc。其值依次为 NamespaceObjectFactory,NamespaceDefinition,createProcedure函数对象类。Namespace('logtest') 返回的结果就是生成的NamespaceDefinition对象,然后调用其define方法,初始化defineCallback。此时仅仅是定义,不做具体的动作。

166 define: function(callback){ 
167 var nsDef = this, nsObj = this.namespaceObject; 
168 this.defineCallback = function($c) { // 给defineCallback赋值,同时定义一下该回调函数的上下文,nsDef和nsObj两个对象。 
169 var ns = { 
170 provide : function(obj){ 
171 nsObj.merge(obj); 
172 $c(); 
173 } 
174 }; 
175 merge(ns, nsDef.getStash()); 
176 merge(ns, nsObj.getStash()); 
177 callback(ns); 
178 }; 
179 },

2:使用之前定义的namespace里面的对象,Namespace.use() => Namespace().use() => Namespace('main').use()。调用use的时候初始化requires数组。
131 use: function(syntax){ // 使用namespace 
132 this.useList.push(syntax); // 该namespace字符串存入数组useList 
133 var splitted = syntax.split(/\s+/); // namespace和它的对象使用空格分开 
134 var fqn = splitted[0]; // 取得namespace 
135 var importName = splitted[1]; // 取得namespace中的对象 
136 _assertValidFQN(fqn); 
137 this.requires.push(function($c){ // 放一个函数到requires数组中 
138 var context = this; 
139 var require = NamespaceObjectFactory.create(fqn); // 获取指定的NamespaceObject对象,之前生成过得对象可以直接从缓存中获取 
140 require.call(this,function(state){ // 调用NamespaceObject对象的call方法 
141 context.loadImport(require,importName); 
142 $c(); 
143 }); 
144 }); 
145 return this; 
146 },

3:调用main的apply方法。看一下具体的apply方法
186 apply: function(callback){ 
187 var nsDef = this; 
188 createProcedure(nsDef.requires) 
189 .next(nsDef.defineCallback) 
190 .call(nsDef,function(){ 
191 callback( nsDef.getStash() ); 
192 }); 
193 }

取出requires数组里面的对象生成Proc对象,并把该requires数组里的对象入队列steps中,还有nsDef.defineCallback也入队列(此例中为undefined),调用Proc的call方法。第一个参数是nsDef,第二个参数是回调函数。
具体使用方法:
在定义Namespace的时候,所有的定义放在define里面,并且以匿名函数的形式定义。function (ns) {

// 具体的实现

// 定义对外公开的对象,外部在use该nsName之后,就可以通过ns.nsName.key()来调用它

ns.provide({ key : value});}
使用namespace.js的好处,所有的定义都是在需要的时候才开始执行的,也就是在具体使用的时候才开始解析定义。

ps:具体内部调用关系还是没有弄明白,今后有时间再整理,这篇太乱了。

Javascript 相关文章推荐
javascript 数组排序函数
Aug 20 Javascript
关于Aptana Studio生成自动备份文件的解决办法
Dec 23 Javascript
用JQuery调用Session的实现代码
Oct 29 Javascript
载入jQuery库的最佳方法详细说明及实现代码
Dec 28 Javascript
jquery实现简单的轮换出现效果实例
Jul 23 Javascript
鼠标点击input,显示瞬间的边框颜色,对之修改与隐藏实例
Dec 26 Javascript
详解Angular2表单-模板驱动的表单(Template-Driven Forms)
Aug 04 Javascript
JS实现十字坐标跟随鼠标效果
Dec 25 Javascript
layer设置maxWidth及maxHeight解决方案
Jul 26 Javascript
vue element upload实现图片本地预览
Aug 20 Javascript
javascript实现简单打字游戏
Oct 29 Javascript
微信小程序封装多张图片上传api代码实例
Dec 30 Javascript
基于jquery的当鼠标滚轮到最底端继续加载新数据思路分享(多用于微博、空间、论坛 )
Oct 10 #Javascript
jquery.tmpl JQuery模板插件
Oct 10 #Javascript
brook javascript框架介绍
Oct 10 #Javascript
jQuery 一个图片切换的插件
Oct 09 #Javascript
跨域请求之jQuery的ajax jsonp的使用解惑
Oct 09 #Javascript
多浏览器兼容性比较好的复制到剪贴板的js代码
Oct 09 #Javascript
jquery利用event.which方法获取键盘输入值的代码
Oct 09 #Javascript
You might like
PHP实现多条件查询实例代码
2010/07/17 PHP
解析PHP工厂模式的好处
2013/06/18 PHP
基于PHP magic_quotes_gpc的使用方法详解
2013/06/24 PHP
thinkPHP5框架路由常用知识点汇总
2019/09/15 PHP
PHP+MySQL实现在线测试答题实例
2020/01/02 PHP
原生js和jQuery随意改变div属性style的名称和值
2014/10/22 Javascript
JQuery设置时间段下拉选择实例
2014/12/30 Javascript
NodeJS学习笔记之Connect中间件模块(二)
2015/01/27 NodeJs
JavaScript使用addEventListener添加事件监听用法实例
2015/06/01 Javascript
jquery关于事件冒泡和事件委托的技巧及阻止与允许事件冒泡的三种实现方法
2015/11/27 Javascript
javascript日期比较方法实例分析
2016/06/17 Javascript
JavaScript prototype属性详解
2016/10/25 Javascript
关于jQuery EasyUI 中刷新Tab选项卡后一个页面变形的解决方法
2017/03/02 Javascript
Vue-cli创建项目从单页面到多页面的方法
2017/09/20 Javascript
微信小程序点击item使之滚动到屏幕中间位置
2020/03/25 Javascript
利用PHP实现递归删除链表元素的方法示例
2020/10/23 Javascript
React服务端渲染原理解析与实践
2021/03/04 Javascript
[47:43]完美世界DOTA2联赛PWL S3 Magama vs GXR 第二场 12.19
2020/12/24 DOTA
Django实现快速分页的方法实例
2017/10/22 Python
python+opencv实现动态物体追踪
2018/01/09 Python
pandas实现to_sql将DataFrame保存到数据库中
2019/07/03 Python
python字符串分割及字符串的一些常规方法
2019/07/24 Python
Python 面向对象部分知识点小结
2020/03/09 Python
Python 实现自动登录+点击+滑动验证功能
2020/06/10 Python
python能做哪些生活有趣的事情
2020/09/09 Python
python用分数表示矩阵的方法实例
2021/01/11 Python
html5 canvas简单封装一个echarts实现不了的饼图
2018/06/12 HTML / CSS
h5网页水印SDK的实现代码示例
2019/02/19 HTML / CSS
Hotels.com英国:全球领先的酒店住宿提供商
2019/01/24 全球购物
高中学生干部学习的自我评价
2014/02/21 职场文书
红色故事演讲稿
2014/05/22 职场文书
完美的中文自荐信
2014/05/24 职场文书
2014坚持党风廉政建设思想汇报
2014/09/18 职场文书
商务邀请函
2015/01/30 职场文书
科技馆观后感
2015/06/08 职场文书
TS 类型收窄教程示例详解
2022/09/23 Javascript