Maison > Article > interface Web > Compétences module_javascript de graine de conception de framework JavaScript
Le module seed est également appelé module core, qui est la première partie du framework à être exécutée. Même une bibliothèque de fonctions à fichier unique comme jQuery est divisée en plusieurs modules internes. Certains modules doivent être exécutés immédiatement au début lorsqu'ils sont exécutés, et certains modules ne sont exécutés que lorsqu'ils sont utilisés. Certains modules sont inutiles, ont un faible sens de l'existence et ne peuvent fonctionner que sous des navigateurs spécifiques.
Le module seed est l'un des pionniers. Les méthodes qu'il contient ne doivent pas nécessairement être entièrement fonctionnelles et bien conçues, mais elles doivent être extrêmement évolutives, couramment utilisées et stables.
L'extensibilité signifie que d'autres modules peuvent être inclus via eux ; l'utilisation courante signifie que la plupart des modules peuvent les utiliser pour éviter la duplication du travail. La stabilité signifie qu'elle n'est pas facilement remplacée par de nouvelles méthodes lors des itérations de version.
En référence à l'implémentation de nombreux frameworks et bibliothèques, nous pensons que le module seed comprend les fonctions suivantes : expansion d'objets, mise en tableau, détermination de type, liaison et déchargement simples, gestion sans conflit, chargement de module et domReady Le contenu de. ce chapitre est basé sur Le module de départ Mass Framework est un modèle.
https://github.com/RubyLouvre/mass-Framework
1. Espace de noms
En tant que partie initiale d'un cadre, le module de démarrage est chargé d'aider à la construction de l'infrastructure mondiale. jQuery a un bon départ, en utilisant IIFE (expression de fonction immédiatement invoquée).
LIFE est l'infrastructure la plus importante des frameworks JavaScript modernes. Elle s'enveloppe comme une cellule pour éviter toute pollution variable. Tout comme un point d'ancrage, il s'agit d'espaces de noms, tels que prototype.js et mootools. Ils vous empêchent de ressentir l'existence du framework. Son importance atteint tous les coins de l'ensemble de l'environnement d'exécution, tels que javascript, DOM, BOM, etc. , et a un impact profond sur le prototype d'objet natif peut être étendu. En raison de la forte opposition de Douglas (l'auteur de JSON), de nouveaux frameworks sont construits sur des espaces de noms.
Voyons comment simuler des espaces de noms en JavaScript. JavaScript est entièrement basé sur des objets, mais seuls les objets qui répondent au type peuvent répondre aux exigences, telles que la fonction, RegExp et l'objet, mais les plus couramment utilisés sont l'objet et la fonction. Nous ajoutons un attribut à un objet, et cet attribut est un objet. Nous pouvons ajouter un objet à cet objet, de cette façon, nous pouvons construire notre framework de manière ordonnée. Si l'utilisateur souhaite appeler une méthode, il peut l'appeler sous la forme xxx.yyy.zzz().
if( typeof(Ten) === "undefined" ){ Ten = {}; Ten.Function = { /*略*/ } Ten.Array = { /*略*/ } Ten.Class = { /*略*/ } Ten.JSONP = new Ten.Class(/*略*/ ) Ten.XHR = new Ten.Class(/*略*/ ) }
En regardant l'implémentation des principales bibliothèques, elles définissent essentiellement une variable globale comme un espace de noms au début, puis l'étendent, comme Base2 de Base, Ext's Ext, jQuery de jQuery, YUI de YUI et dojo de dojo, mochKit pour MochiKit . Du point de vue du degré de pollution des variables globales, elles se répartissent en deux catégories :
prototype.js et mootools sont classés dans la même catégorie que Base2. La philosophie de Prototype est d'étendre les objets natifs de javascript. Dans les premières années, le prototype était presque considéré comme la norme de facto. Le problème de la coexistence avec d’autres bibliothèques n’est donc pas pris en compte. Basic Prototype a également développé d'excellentes bibliothèques de classes telles que script.aculo.us, rico, Plotr, protoChart, Script2 et une large catégorie de plug-ins payants. De plus, presque tous les plug-ins ayant certaines origines sont liés à Prototype, comme lightBox. mootools est une version améliorée de prototype.js, qui est plus OO et réplique entièrement son API. Base2 veut corriger les bugs d'IE et laisser IE disposer d'API de navigateur standard, ce qui pollue également tous les objets natifs.
La deuxième catégorie est celle des frameworks jQuery, YUI et EXT. YUI et Ext sont construits en imbriquant des objets dans des objets. jQuery adopte une approche différente. Il est orienté sélecteur, son espace de noms est donc une fonction qui permet aux utilisateurs de transmettre la chaîne de l'expresseur CSS. Il recherche ensuite dans le sélecteur et renvoie enfin une instance d'objet jQuery.
jQuery utilisait initialement $ comme espace de noms comme Prototype. Par conséquent, il a implémenté le mécanisme de coexistence de nombreuses bibliothèques. Vous pouvez basculer entre $ et jQuery à volonté. Le principe de la coexistence multi-bibliothèques de jQuery est très simple, il est donc devenu plus tard. le mécanisme de coexistence de nombreuses bibliothèques Configuration standard pour Xiaoku. Tout d'abord, enregistrez l'espace de noms dans une variable temporaire (notez que cet objet ne fait pas partie de votre propre framework pour le moment, il peut s'agir de prototype.js ou autres), puis utilisez noConflict pour le remettre.
//jQuery1.2 var _jQuery = window.jQury , _$ = window.$; //把可能存在同名变量先保存起来 jQury.extend({ noConflict : function(deep) { window.$ = _$; //这时再放回去 if (deep) //补充 if ( deep && window.jQuery === jQuery ) window.jQury = _jQuery; return jQury; } })
Référence : http://zhidao.baidu.com/question/1239712776390687219.html
Mais noConflict de jQuery n'est utile que pour les frameworks de bibliothèques de classes à fichier unique et ne peut pas être copié comme Ext. Par conséquent, après avoir renommé l'espace de noms, définissez Ext sur null, puis introduisez-le dans un nouveau fichier javascript via un chargement dynamique. Le fichier sera appelé avec Ext, ce qui provoquera une erreur.
2. Extension d'objet
Nous avons besoin d'un mécanisme pour ajouter de nouvelles fonctionnalités à notre espace de noms. Cette méthode est généralement appelée extend ou mixin en JavaScript. Avant la naissance du descripteur de propriété (Property Descriptor), un objet JavaScript pouvait ajouter, modifier et supprimer ses membres à volonté. Par conséquent, étendre un objet est très pratique. Une implémentation simple d’une méthode d’extension ressemble à ceci.
function extend (destination,source){ for (var property in source) destination[property] = source[property]; return destination; }
不过,旧版本IE在这里有个问题,它认为像Object的原型方法就是不应该被遍历出来,因此for in循环是无法遍历valueOf、toString的属性名。这导致,模拟Object.keys方法是现实时也遇到了这个问题。
Object.keys = Object.keys || function(obj){ var a = []; for(a[a.length] in obj); return a; }
在不同的框架,这个方法还有不同的实现,如Ext分别为apply与applyIf两个方法,前者会覆盖目标对象的同名属性,而后者不会。dojo允许多个对象合并在一起。jQuery还支持深拷贝。下面是mass Farmework的mix方法。支持多对象合并与选择是否覆写。
function mix(target,source){ //如果最后参数是布尔,判定是否覆盖同名属性 var args = [].slice.call(arguments), i = 1, key, ride = typeof args[args.length - 1] == "boolean" ? args.pop() : true; if (args.length === 1){ //处理$.mix(hash)的情形 target = !this.window ? this : {}; i = 0; } while ((source = args[i++])) { for (key in source){ //允许对象糅杂,用户保证都是对象 if (ride || !(key in target)) { target[key] = source[key]; } } } return target; }
3.数组化
浏览器下存在很多类数组对象,如function内的arguments,通过document.forms、form.elements,document.links、select.options、document.getElementsByName,document.getElementsByTagName、childNodes、children等方式获取的节点的结合(HTMLCollection 、NodeList)或按照某些特殊的写法自定义对象。
类数组对象是一个很好的存储结构。不过功能太弱了,为了能使用纯数组的那些便捷的方法,我们会在处理它们前都会做一下转换。
通常来说,使用[].slice.call就能转换了 ,不过功能不够用,但在旧版本的HTMLCollection、NodeList不是Object的子类,采用如上的方法会导致IE执行异常。我们看一下
jQuery:makeArray var makeArray = function(array) { var ret = [] ; if(array != null){ var i = array.length; if(i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval) ret[0] = array; else while (i) ret(--i) = array[i]; } return ret; }
mass的实现,一开始就进行区分,直接[].slice.call,IE的话自己动手实现一个slice方法
$.slice = window.dispatchEvent ? function(nodes,start,end){ return [].slice.call(nodes,start,end); } : function (nodes,start,end){ var ret = [], n = nodes.length; if (end === void 0 || typeof end === "number" && isFinite(end)){ start = parseInt (start,0) || 0; end = end == void 0 ? n:parseInt (end,10); if(start < 0){ start += n; } if (end > n) { end =n }; if (end < 0) { end += n }; for (var i = start; i < end; ++i){ ret[i-start] = nodes[i]; } } return ret; }
4.类型的判定
javascript存在两套类型系统,一套是基本的数据类型,另一套是对象类型系统。基本数据类型包括6中 。分别是undefined、string、null、boolean、function、object。基本数据类型是通过typeof来检测的。对象类型系统是以基础类型系统为基础的,通过instanceof来检测的。然而,javascript自带的这两套识别机制非常不靠谱,于是就催生了isXXX系列。就拿typeof来说,它只能粗略识别出string、number、boolearn、function、undefined、object这6种数据类型,无法识别null,RegExpArgument等细分的对象类型。
typeof null // => "object" typeof document.childNodes //=> safari: "function" typeof document.creatElement('embed') //=> ff3-10 "function" typeof document.creatElement('object') //=> ff3-10 "function" typeof document.creatElement('object') //=> ff3-10 "function" typeof /\d/i //在实现了ecma262v4的浏览器返回"function" typeof window.alert //ie678 "object" var iframe = document.creatElement("iframe") document.body.appendChild(iframe) xArray = window.frames[window.frames.length - 1].Array; var arr = new xArray(1,2,3) //=>[1,2,3] arr instanceof Array ;// false isNaN("aaa") //=> true
另外,以前人们总是以document.all来判断是否为ie,这其实是很危险的,因为,用document.all来取得页面中所有的元素是不错的注意,这个方法FF,chrome打算使用很久了,不过人们都这样判断,就是在chrome下有这样的闹剧。
typeof document.all //undefined document.all //HTMLAllCollection [728] (728为元素总数)
在判定undefined、null、string、number、boolean、function这六个还算简单,前面两个可以分别与void(0)、null比较,后面4个的typeof也可以满足90%的情形。这样说是因为string、number、boolean可以包装成伪对象。
typeof new Boolean(1); //=>"object" typeof new Number(1); //=>"object" typeof new String("aa"); //=> "object"
这些还不是最难的,难点在于RegExp与Array.判定RegExp的情况很少。Array则不一样。有关isArray的实现不下二十种。都是因为鸭式辨型被打破了。直到prototype.js把Object.prototype.toString发掘出来。此方法是直接输出内部的[[Class]],绝对精准。有了它,95%的陷阱被跳过了。
function isArray(arr){ return arr instanceof Array; } function isArray(arr){ return !!arr && arr.constructor === Array; } function isArray(arr) { //prototype.js 1.6 return arr != null && typeof arr === "object" && 'splice' in arr && 'join' in arr; } function isArray(arr){// Douglas Crockford(JSON作者,反对原型污染) return typeof arr.sort == "function" } function isArray(array){ //kriszyp var result = false; try{ new array.constructor (Math.pow(2,32)) } catch (e){ result = /Array/.test(e.message) } return result; }; function isArray(o){//kangax try{ Array.prototype.toString.call(o); return true; } catch (e) { } return false; } function isArray(o){ //kangax if(o && typeof o == 'object' && typeof o.length == 'number' && isFinite(o.length)) { var _origLength = o.length; o[o.length] = '__test__'; var _newLength = o.length; o.length = _origLength; return _newLength == _origLength + 1; } return false }
至于null 、 undefined 、NaN直接这样写
function isNaN(obj) { return obj !== obj } function isNull(obj) { return obj === null; } function isUndefined(obj){ return obj === void 0; }
最后要判定的是对象是window,由于ECMA是不规范的Host对象,window对象属于host.所以也就没有被约定。就算Object.prototype.toString也对它没办法
[object Object] IE6 [object Object] IE7 [object Object] IE8 [object Window] IE9 [object Window] ff3.6 [object Window] opera10 [object DOMWindow] safari 4.04 [object global] chrome5.0
不过根据window.window和window.setInterval去判定更加不靠谱,用一个技巧就可以完美识别ie6 ie7 ie8的window对象,其它还用toString,这个神奇的hack就是,window与document互相比较,如果顺序不一样,结果也是不一样的!
剩下的就是一些经典方法:
在prototype.js中,拥有isElement,isArray,isHash,isFunctoion,isString,isNumber,isDate,isUndefined。
mootools有一个typeOf判定基本类型,instanceOf判定自定义“类”
RightJS 有isFunction , isHash , isString , isNumber , isArray ,isElement, isNode.
Ext有比较全面的判断,isEmpty,isArray,isDate,isObject,isSimpleObject,isPrimitive,isFunction,isNumber,isMumeric,isString,isBoolean,isElement,isTextNode,isDefined,isIterable,应有尽有。最后,还有typeOf判断基本类型。
Underscore.js有isElement,isEmpty,isArray,isArgument,isObject,isFunction,isString,isNumber,isFinite,isNaN,isBoolean,isDate,isRegExp,isNull,isUndefined.
jQuery就不与其它框架一样了,在jQuery1.4中只有isFunction,isArray,isPlainObject,isEmptyObject。isFunction,isArray用户肯定用的较多,isPlainObject是用来判断是否是纯净的js对象。既不是DOM,BOM对象,也不是自定义的“类”的实例对象,制造它的目的最初是为了深拷贝,避开像window那样自己引用自己的对象。isEmptyObject是用于数据缓存的系统,当此对象为空时,就可以删除它。
//jQuery2.0纯净数组的判定思路 jQuery.isPlainObject = function(obj){ //首先排除基础类型不为Object的类型,然后是DOM节点与window对象 if(jQuery.type(obj) !== "object" || object.nodeType || jQuery.isWindow(obj)){ return false; } //然后回溯它的最近的原型对象是否有isPrototypeOf. //旧版本的IE一些原生的对象没有暴露constructor、prototype。因此在这里过滤掉 try{ if (obj.constructor && !hasOwn.call(obj.constructor.prototype,"isPrototypeOf")){ return false; } } case(e) { return false; } return true; }
avalon.mobile中有一个更精简的版本,只支持最新的浏览器,可以大胆的使用ecma262v5新API
avalon.isPlainObject = function(obj){ return obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype }
isArrayLike也是一个常用的方法,但是判定一个类数组太难了,唯一的识别方法就是判断有一个大于0或等于0的length属性,此外,还有一些共识,如window与函数和元素节点,如(form元素),不算类数组,虽然它们都满足前面的条件。因此,至今jQuery都没有把它暴露出来。
//jQuery2.0 function isArrayLike(obj){ var length = obj.length , type = jQuery.type(obj); if (jQuery.isWindow(obj)){ return false; } if (obj.nodeType === 1 && length){ return true } return type === "array" || type !== "function" && (length === 0 || typeof length === "number" && length > 0 && (length -1) in obj); } // avalonjs function isArrayLike (obj) { if (obj && typeof obj === "object"){ var n = obj.length if (+n === n && !( n % 1) && n >= 0){//检测length是否为非负整数 try{ if ({}.prototypeIsEnumerable.call(obj,'length') === false){ return Array.isArray(obj) || /^\s?function/.test(obj.item || obj.callee) } return true; } catch (e) { //IE的NodeList直接报错 return true; } } } return false }
在Prototype.js1.3版本中的研究成果(Object.prototype.toString.call)就应用于jQuery,在jQuery1.2中,判断一个变量是否为函数非常复杂。
jQuery1.43中引入isWindow来处理makeArray中对window的判定,引入isNaN用于确保样式赋值的安全。同时引入type代替typeof关键字,用于获取基本数据的基本类型。
class2type = {}; jQuery.each("Boolean Number String Function Array Date RegExpObject".split(" "),function( i , name ){ class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); jQuery.type = function(obj){ return obj == null ? String(obj) : class2type[toString.call(obj)] || "object"; }
jQuery1.7中添加isNumeric代替isNaN。这是个不同于其它框架的isNumber,它可以是个字符串,只要外观上像数字就可以了。但jQuery1.7还做了一个违背之前提到稳定性的事情。冒然去掉jQuery.isNaN,因此,基于旧版jQuery的有大批插件失效。
//jQuery.1.43-1.64 jQuery.isNaN = function ( obj ) { return obj == null || !rdigit.test( obj ) || isNaN( obj ); } //jQuery1.7就是isNaN的去反版 jQuery.isNumeric = function ( obj ) { return obj != null && rdigit.test( obj ) && !isNaN( obj ); } //jQuery.1.71 - 1.72 jQuery.isNumeric = function ( obj ) { return !isNaN( parseFloat(obj) ) && isFinite( obj ); } //jQuery2.1 jQuery.isMumeric = function( obj ) { return obj - parseFloat(obj) >= 0; }
massFarmeWork的思路与jQuery一致,尽量减少isXXX系列的代码,把is Window,isNaN,nodeName等方法做了整合。代码较长,既可以获取类型,也可以传入第二参数进行类型比较。
var class2type = { "[objectHTMLDocument]" : "Document", "[objectHTMLCollection]" : "NodeList", "[objectStaticNodeList]" : "NodeList", "[objectIXMLDOMNodeList]" : "NodeList", "[objectDOMWindow]" : "window", "[object global]" : "window", "Null" : "Null", "undefined" : "undefined" }, toString = class2type.toString; "Boolean,Number,String,Function,Array,Date,RegExp,Window,Document,Arguments,NodeList".replace($.rword,function( name ) { class2type[ "[object" + name + "]" ] = name; }); //class2type这个映射几乎把常用的判定对象一网打尽 mass.type = function( obj , str ){ var result = class2type[ (obj == null || obj !== obj) ? obj : toString.call(obj) ] || obj.nodeName || "#"; if(result.charAt(0) === "#") { //兼容旧版浏览器的个别情况,如window.opera //利用IE678 window === document为true,document === window为false if( obj == obj.document && obj.document != obj ) { result = "window"; //返回构造器名称 } else if ( obj.nodeType === 9 ) { result = "Document"; } else if ( obj.callee) { result = "Arguments"; } else if ( isFinite(obj.length) && obj.item ) { result = "NodeList" //处理节点集合 } else { result = toString.call(obj).slice(8,-1); } } if(str){ result str === result; } return result; }
然后type方法就十分轻松了,用toSring.call(obj)得出值的左键,直接从映射中取得。IE678,我们才费一些周折处理window,document,argument,nodeList等对象。
百度的七巧板基于实用主义,判定也十分严谨。与EXT一样,能想到的写上,并且判定十分严谨。
目前版本2.0.2.5 http://tangram.baidu.com/api#baidu.type()
baidu.isDate = function( unknow ) { return baidu.type(unknow) == "date" && unknow.toString() != 'Invalid Date' && !isNaN(unknow); }; baidu.isNumber = function( unknow ) { return baidu.type(unknow) == "number" && isFinite( unknow ); };
5.主流框架的引入机制-domReady
domReady其实是一种名为"DOMContentLoaded"事件的别称,不过由于框架的需要,它与真正的DOMContentLoaded有一点区别,在很多新手和旧的书中,很多人将其写在window.onload回调中,防止dom树还没有建完就开始对节点操作。而对于框架来说,越早越介入dom就越好,如要进行特征侦测之类的。domready还可以满足用户提前绑定事件需求,因为有时页面图片过多等,window.onload事件迟迟不能触发,这时用户操作都没有效果,因此,主流的框架都引入了domReady机制,并且费了很大周折才兼容所有浏览器。具体的策略如下:
对于支持DOMContentLoaded事件使用DOMcontentLoaded事件
旧版本IE使用Diego perini发现的著名Hack
//by Diego Perini 2007.10.5 function IEContentLoaded (w, fn) { var d = w.document, done = false, // 只执行一次用户的回调函数init() init = function () { if (!done) { done = true; fn(); } }; (function () { try { // DOM树未创建完之前调用doScroll会抛出错误 d.documentElement.doScroll('left'); } catch (e) { //延迟再试一次~ setTimeout(arguments.callee, 50); return; } // 没有错误就表示DOM树创建完毕,然后立马执行用户回调 init(); })(); //监听document的加载状态 d.onreadystatechange = function() { // 如果用户是在domReady之后绑定的函数,就立马执行 if (d.readyState == 'complete') { d.onreadystatechange = null; init(); } }; }
此外,IE还可以通过script defer hack进行判定
document.write("<script id=__ie_onload defer src=//0 mce_src=http://0></scr"+"ipt>"); script = document.getElementById("__ie_onload"); script.onreadystatechange = function() { //IE即使是死链也能触发事件 if (this.readyState == "complete") init(); // 指定了defer的script在dom树建完才触发 };
不过还有个问题,如果我们的种子模块是动态加载的,在它插入dom树时,DOM树是否已经建完呢?这该怎么触发ready回调?jQuery的方案是,连onload也监听了 ,但如果连onload也没赶上,就判定document.readyState等于complete。(完美)可惜ff3.6之前没有这属性,看mass的方案
var readyList = []; mess.ready = function( fn ) { if ( readyList ) { fn.push( fn ); } else { fn(); } } var readyFn ,ready = W3C ? "DOMContentLoaded" : "readyStatechange"; function fireReady() { for (var i = 0 , fn; fn = readyList[i++]) { fn(); } readyList = null; fireReady = $.noop; //惰性函数,防止IE9调用_checkDeps } function doScrollCheck() { try { //IE下通过doScrollCheck检测DOM树是否建设完 html.doScroll("left"); fireReady(); } catch (e){ setTimeout(doScrollCheck); } } //FF3.6前,没有readyState属性 if (!document.readyState){ var readyState = document.readyState = document.body ? "complete" : "loading"; if (document.readyState === "complete") { fireReady(); //如果在domReay之外加载 } else { $.bind(document,ready,readyFn = function(){ if(W3C || document.readyState === "complete") { fireReady(); if(readyState){ //IE下不能该项document.readyState document.readyState = "complete"; } } }); if (html.doScroll){ try {//如果跨域就报错,证明有两个窗口 if (self.eval === parent.eval) { doScrollCheck(); } } catch (e) { doScrollCheck(); } } } }
6.无冲突处理
无冲突处理也叫多库共存,$是这个重要的函数名,以至于大家都爱拿它来做自己的命名空间,当jQuery开始发展时,Prototype是主流,jQuery发明了noConflict函数
var window = this, undefined, _jQuery = window.jQuery, _$ = window.$, //将window存入闭包中的同名变量,方便内部函数调用windows不用太麻烦查找它。 //_jQuery与_$用于以后重写 jQuery = window.jQuery = window.$ = function(selector, context){ //用于返回一个jQuery对象 return new jQuery.fn.init(selector,context); } jQuery.extend({ noConflict : function(deep) { //引入jQuery类库后,闭包外边的window.$与window.jQuery都储存着一个函数 //它是用来生成jQuery对象或在domReady后执行里面的函数 //回顾最上面的代码,在还没有把function赋值给它们时,_jQuery和_$已经被赋值了,因此,它们两的值必然是undefined //因此,这种放弃控制权的技术很简单,就是用undefined把window.$里边jQuery函数清除掉。 //这时,prototype或mootools的$就被拿走了 window.$ = _$; //相当于window.$ = undefined,如果你有一个叫jQuery的库,也能大方的过渡出去。 //这时,需要给noConflict添加一个布尔值,true if (deep) //但我们必须使用一个东西接纳jQuery与jQuey的入口函数 //闭包里边的东西除非被window等宿主引用,否则是不可见的 //因此,我们把闭包里的jQuery return出去,外面用一个变量接纳就可以 window.jQuery = _jQuery; //相当window.jQuery = undefined return jQuery; } })
使用时,先引入别人的库,然后引入jQuery,使用调用$.noConflict()进行改名,这样就不影响别人的$运行了。
mass的操作方式是在script标签上定义一个nick属性,那么释放出来的命名空间就是你的那个属性值。里面实现了类似jQuery的机制。
<script nike="aaa" src="mass.js"></script> <script> aaa.log("xxxxxx") </script>
以上所述就是本文的全部内容了,希望大家能够喜欢。