YUI 读码日记之 YAHOO.lang.is*March 4, 2008

在 YUI 框架的 %BULID%/yahoo/yahoo.js 中,包含了一系列的变量类型检测方法,它们被分装成 YAHOO.lang.is* 。其中,这些函数的大部分封装都是 typeof 操作符的封装,我个人比较敢兴趣的事 isArray 与 isValue 的两个函数。

YAHOO.lang = YAHOO.lang || {
    isArray: function(o) { 
        if (o) {
           var l = YAHOO.lang;
           // 如果该对象有 length 这个属性,同时支持 splice 方法,
           // 那么就认为它为数组。
           return l.isNumber(o.length) && l.isFunction(o.splice);
        }
        return false;
    },

    isBoolean: function(o) {
        return typeof o === 'boolean';
    },

    isFunction: function(o) {
        return typeof o === 'function';
    },
        
    isNull: function(o) {
        return o === null;
    },
        
    isNumber: function(o) {
        return typeof o === 'number' && isFinite(o);
    },
      
    isObject: function(o) {
        return (o && (typeof o === 'object' || 
                            YAHOO.lang.isFunction(o))) || false;
    },
        
    isString: function(o) {
        return typeof o === 'string';
    },
        
    isUndefined: function(o) {
        return typeof o === 'undefined';
    },
    
    //...

    isValue: function(o) {
        // Infinity fails
        // return (o || o === false || o === 0 || o === '');
        var l = YAHOO.lang;
        return (l.isObject(o) || l.isString(o) || 
                           l.isNumber(o) || l.isBoolean(o));
    }
};

…… 复制粘贴分割线 ……

据悉 ,在 YUI 2.2.0 版本以前,YAHOO.lang.isArray 是这样写的。

isArray: function(obj) { 
    // safari 有 bug,只好处理字符串
    if (obj && obj.constructor && 
                 obj.constructor.toString().indexOf('Array') > -1) {  
        return true;  
    } else {
        return YAHOO.lang.isObject(obj) && obj.constructor == Array;  
    }  
},

而这样的判断数组类型是有缺陷的,比如下面的代码

function myArray() {
   this.name = 'name';  
}  
var o2 = new myArray();  
alert(YAHOO.util.isArray(o2));  // 弹出true  
// 因为 obj.constructor.toString() 中包含 myArray 字样,所以返回true  
  
function Obj() {  
    this.name = 'name';  
}  
var o = new Obj();  
o.constructor = Array;
alert(YAHOO.util.isArray(o));  // 弹出 true  
// 因为在 JavaScript 里,constructor 也是属性
// 可以动态指定,所以返回 true

因此,在 YUI 的后续版本,YAHOO.lang.isArray 被修改成了目前的这个样子

isArray: function(o) { 
    if (o) {
       var l = YAHOO.lang;
       // 如果该对象有 length 这个属性,同时支持 splice 方法,
       // 那么就认为它为数组。
       return l.isNumber(o.length) && l.isFunction(o.splice);
    }
    return false;
},

新的实现用了另外的思路:如果该对象有 length 这个属性,同时支持 splice 方法,那么就认为它为数组。当然,它依然有漏洞,我们仍然可以创建一个对象,使其拥有 length 属性和 splice 方法。但我觉得现在的实现更为合理,因为一来可能性不大,二来避免了诡异的浏览器的 BUG 。

再看 YUI 2.3.0 后引入的 YAHOO.lang.isValue,其实就是判断参数是否是一个有意义的值,只要参数不是 null/undefined/NaN,那么都返回 true。(注意这和一般的判断真假的不同就是, 0/false/''(空字符串) 这些都算是有效的值),所以 YAHOO.lang.isValue 非常适合用来判断表单域的值是否为有效值。

YUI 读码日记之 YAHOO.namespaceMarch 1, 2008

YUI 提供了类似于 Java 的 命名空间 机制。%BULID%/yahoo/yahoo.js 文件中,YAHOO 基类定义好了以后,第一个定义的方法就是 namespace(在 YUI 2.5 中,第 75 行)。下面是 YUI 手册上的介绍

namespace

static Object namespace ( arguments )
Returns the namespace specified and creates it 
if it doesn't exist

YAHOO.namespace("property.package");
YAHOO.namespace("YAHOO.property.package");

Either of the above would create YAHOO.property, then 
YAHOO.property.package Be careful when naming packages. 
Reserved words may work in some browsers and not others. 
For instance, the following will fail in Safari:

YAHOO.namespace("really.long.nested.namespace");

This fails because "long" is a future reserved word in 
ECMAScript

Parameters:
    arguments <String*> 1-n namespaces to create 

Returns: Object
    A reference to the last namespace object created

下面看下相应的代码。

YAHOO.namespace = function() {
    var a=arguments, o=null, i, j, d;

    // 根据参数的长度循环获取属性或方法名   
    for (i=0; i<a.length; i=i+1) {
        d=a[i].split(".");
        o=YAHOO;

        // 避免定义 YAHOO.YAHOO
        for (j=(d[0] == "YAHOO") ? 1 : 0; j<d.length; j=j+1) {
            // 加入相应的方法或属性
            o[d[j]]=o[d[j]] || {};
            o=o[d[j]];
        }
    }
    
    // 返回 YAHOO 本身
    return o;
};

根据文档,YUI 默认会添加 "util"、"widget"、"example" 三个命名空间,相应的代码如下。

(function() {
    // 添加默认的命名空间
    YAHOO.namespace("util", "widget", "example");

    if ("undefined" !== typeof YAHOO_config) {
        var l=YAHOO_config.listener,ls=YAHOO.env.listeners,unique=true,i;
        if (l) {
            // if YAHOO is loaded multiple times we need to check to see if
            // this is a new config object.  If it is, add the new component
            // load listener to the stack
            for (i=0;i<ls.length;i=i+1) {
                if (ls[i]==l) {
                    unique=false;
                    break;
                }
            }
            if (unique) {
                ls.push(l);
            }
        }
    }
})();

从 namespace 的函数写法可以得知,在 定义 Javascript 类的方法或者属性 也可以使用类似于数组的声明方式。比如

var o = {};
o.method = function () {
    alert("hello, world!");
}
o.method();

就可以写成

var o = {};
o["method"] = function () {
    alert("hello, world!");
}
o.method();

但请小心使用此种特性,毕竟数组操作与类的声明显性的区分,有利于以后的代码维护。

更新,感谢 小马 的指证:

「但请小心使用此种特性,毕竟数组操作与类的声明显性的区分」

    * {} 不是类,是对象
    * Javascript 里的对象就是关联数组
    * 什么时候使用 o.method() ,什么时候使用 o['method']() ,依据实际应用环境

建议结合 《JavaScript 权威指南》读 YUI,会让你读出更深刻的知识。

YUI 读码日记之 YAHOO.env.uaFebruary 29, 2008

正如 YAHOO.env.ua 字面所指(ua 就是 user-agent 的意思),这篇文章主要分析 YUI 如何检测浏览器的类型与版本。有关 YAHOO.en.ua 的文档 在这里说明

目前为止,检测浏览器与版本主要有两大主流的做法。其一,就是分析浏览器提供的 user-agent 字符串;其二,就是根据其功能判别。YUI 使用第一种判断方式,也就是分析 user-agent 字符串。

下面是 YUI 相应的代码实现,经过本人精简以后并加了点注释,这段程序在 %BUILD%/yahoo/yahoo.js 中。

YAHOO.env.ua = function() {
    var o={ie:0, opera:0, gecko:0, webkit:0, mobile: null};
    var ua=navigator.userAgent, m;

    // 是否为基于 KHTML 引擎的浏览器,比如 Konqueror 。
    if ((/KHTML/).test(ua)) {
        o.webkit=1;
    }

    // 检测 Apple 系列的浏览器,包括移动版本
    m=ua.match(/AppleWebKit\/([^\s]*)/);
    if (m&&m[1]) {
        o.webkit=parseFloat(m[1]);

        // YUI 是我见过的唯一一个检测移动浏览器的框架 :^)
        if (/ Mobile\//.test(ua)) {
            o.mobile = "Apple"; // iPhone or iPod Touch
        } else {
            m=ua.match(/NokiaN[^\/]*/);
            if (m) {
                o.mobile = m[0]; // Nokia N-series, ex: NokiaN95
            }
        }
    }

    if (!o.webkit) { // 检测其他浏览器
        // 检测 Opera
        m=ua.match(/Opera[\s\/]([^\s]*)/);
        if (m&&m[1]) {
            o.opera=parseFloat(m[1]);
            // 是否为 Opera 移动版本
            m=ua.match(/Opera Mini[^;]*/);
            if (m) {
                o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316
            }
        } else {
            // 检测 IE 和 Mozilla
            m=ua.match(/MSIE\s([^;]*)/);
            if (m&&m[1]) {
                o.ie=parseFloat(m[1]);
            } else {
                m=ua.match(/Gecko\/([^\s]*)/);
                if (m) {
                    o.gecko=1;
                    m=ua.match(/rv:([^\s\)]*)/);
                    if (m&&m[1]) {
                        o.gecko=parseFloat(m[1]);
                    }
                }
            }
        }
    }
    
    return o;
}();

根据代码的流程,我们可以很容易理解 YUI 判断浏览器的逻辑。而我在之前也分析过 jQuery 在这方面是如何做的 ,可以对比参照下。

从代码量上考虑,jQuery 的代码是非常的精简的,而 YUI 「看起来」更像是我们平常一般的写法:保守、但是有效。而我个人更倾向于 jQuery 的写法,当然 YUI 这这样的判断流程自然也是有它的道理的。

请允许我多事一下,下面是本人根据 jQuery 的代码「重写」 YUI 的 YAHOO.env.ua ,让大家见笑了。

YAHOO.env.ua = function() {
    var ua = navigator.userAgent.toLowerCase();
    var version = (ua.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1];
    return {
        webkit: /webkit/.test(ua) ? version : 0,
        opera: /opera/.test(ua) ? version : 0,
        ie: /msie/.test(ua) && !/opera/.test(ua) ? version : 0,
        gecko: /mozilla/.test(ua)&&!/(compatible|webkit)/.test(ua) ? version : 0,
        mobile: / mobile\//.test(ua) || ua.match(/nokian[^\/]*/) || 
                                       ua.match(/opera mini[^;]*/) ? version : 0
    };
}();
Yahoo 统计