無標題文檔

全局空间的「污染」哲学

八卦文章,所以标题党下,见笑。期前编写 JavaScript 框架的规则, 玉伯在他的 Blog 已有提到 。而今天看见 Ajaxian 上 收集的目前主流 JavaScript 框架占用的 全局空间变量数目 ,我也忍不住八卦下。

大家或许都知道,使用全局空间要格外的谨慎, 如果滥用全局空间会造成诸多问题 。随着 Javascript 的框架越来越多,不同的设计哲学相互碰撞,形成很有趣的局面:

https://friable.rocks/_/2009_11_05/557076d94e40.jpg

其实,从上面几个图就能看出 Javascript 框架能分成几个流派,就我使用过的几个典型的框架,谈下我的看法:

YUI 看起来像是少林寺,代码调理清晰谨慎也非常容易阅读和理解,但略显庞大和臃肿。其使用全局空间的方式,也 犹如少林寺规一样 ,不敢「越雷池」半步,如数绑定到了 YAHOO 上。

jQuery 犹如丐帮,使用面广、上手容易,但要学得「降龙十八掌」和「打狗棒」并非易事。对于全局空间的态度也虽谨慎(全部绑定到 jQuery),但大家似乎更习惯使用「$」,而忘记还有个 jQuery 变量。

Mootools/Prototype 很逍遥派,代码优雅高效,但真正理解的掌握需要有高深的内功。全局空间上使用也近乎随意,甚至对浏览器内置的功能也有不同程度的扩展。Mootools 的作者也 讨论过相关的设计哲学 ,在这里就不复述。

另外还有个很有趣的现象,就是似乎大家都很喜欢使用美元符「$」变量。但这意味着如果载入不同的框架,就会造成命名空间的相互污染( 详细 )。

避免全局空间的污染,在一定程度上能避免以后自找麻烦,也更有利于代码的模块化。撇开上述框架的八卦,在编写实际代码时更应该注意全局空间的污染问题。

使用 toString 进行类型检测

toString 方法 返回对象的描述信息,很多时候我们都忘记了还有这属性的存在。 在查看 jQuery 源码时 ,发现其 isArray 竟然令人惊艳的使用 toString 进行类型判断:

597 toString = Object.prototype.toString;
    ...
616 isArray: function(obj) {
617    return toString.call(obj) === "[object Array]";
618 }

Javascript 这种弱类型的脚本语言进行类型检测,是件很头痛的事情。 巧合的是 Perfection Kills 近期也发布了篇同内容的文章 ,并得出相似的结论。

Perfection Kills 还阐述了使用 instanceof 和 typeof 需要格外注意的问题,以及其他如何使用「正统保险」的方法实现 isArray 函数(另,针对 YUI 的 isArray 写法,可以 参考这里 )。

回到上述 jQuery 代码, Mozilla 针对 toString 的描述 道破其中玄机:

If this method is not overridden in a custom object, 
toString returns [object type], where *type* is the object type. 

那么,可以同时 toString 使用针对统一的返回格式进行类型检测。由于 call 的首个参数必须是 Object 类型以及

alert(typeof null);

返回的是 Object ( 标准如此规定 让我很困惑),那么索性针对 undefined 以及 null 独立的判断。根据上述观点实现简单的检测代码如下

var chkType = function (obj) {
    if (obj === null) {
        return 'Null';
    }

    if (obj === undefined) {
        return 'Undefined';
    }

    return Object.prototype.toString.call(obj).match(/s(.+)]$/)[1];
};

最后, 简单得写了个 DEMO

由 IE8 User-Agent 更新想到的

IE 开发团队更改了 IE8 的 User-agent ,更改的部分信息如下:

IE8 on Windows Vista (Compatibility View)
    Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0) 

IE8 on Windows Vista
    Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)

除了 IE8 因 Compatibility View 功能 造成单浏览器「原生」多个 User-agent 外,还有个情况也变得非常的有趣,就是几乎每个浏览器都将自己渲染引擎的标识加入到了 User-agent 中(Gecko、 WebkitTrident) )。

User-agent 信息常被用作检测浏览器类型和版本的最佳途径( YUIjQuery ),而上述的改动是否意味着以后类似的检测脚本会变得更加的复杂?回答这个问题之前,让我们看下 Mootools 如何检测浏览器信息

var Browser = {
    Engine: {name: 'unknown', version: 0},
    Features: {
        xpath: !!(document.evaluate),     // 是否支持 XPath
          air: !!(window.runtime),        // 是否支持 Air 扩展
        query: !!(document.querySelector) // 是否支持 CSS 选择器
    },
    Engines: {
        // 判断 Opera
        presto: function() {
            return (!window.opera) ? 
                false : ((arguments.callee.caller) ? 
                    960 : ((document.getElementsByClassName) ? 950 : 925));
        },
        // 判断 IE,根据 ActiveX 和 特有的 XMLHttpRequest 对象
        trident: function() {
            return (!window.ActiveXObject) ? 
                           false : ((window.XMLHttpRequest) ? 5 : 4);
        },
        // Webkit 核心的浏览器,如 Safari 和 Chrome
        webkit: function() {
            return (navigator.taintEnabled) ? 
                false : ((Browser.Features.xpath) ? 
                    ((Browser.Features.query) ? 525 : 420) : 419);
        },
        // Mozilla Gecko 核心浏览器,如 Firefox
        gecko: function() {
            return (document.getBoxObjectFor == undefined) ?
                false : ((document.getElementsByClassName) ? 19 : 18);
        }
    }
};

Browser.detect = function() {
    for (var engine in this.Engines){
        var version = this.Engines[engine]();
        // 如果具有特定的浏览器对象
        if (version){
            this.Engine = {name: engine, version: version};
            this.Engine[engine] = this.Engine[engine + version] = true;
            break;
        }
    }
    return {name: engine, version: version};
};

Browser.detect();

上述代码让人感到耳目一新,它是根据浏览器功能而非 User-agent 判断浏览器类型。仔细考虑一下, User-agent 信息可以被伪造 ,同时浏览器厂商日后也会更改 User-agent 信息,所以此种情况下根据功能判断浏览器类型会可靠得多。

延伸下此策略,比如我们会编写这样的代码:

if (ie) {
  // ie only
} else {
  // other browsers
}

这样因浏览器差异而编写的「硬代码」,往往会造成维护两套实际相同功能的代码,并造成逻辑上的混乱。何不先抛开浏览器兼容的问题,然后再判断相应的对象是否被浏览器支持。

OK,有关编程思想的问题就不继续了…

-- Split --

PS,目前判断是否是 IE8 可这样编写(来自 舜子 ):

var isIE8 = !!window.XDomainRequest;

奇异的 arguments(翻译)

function format(string) {
  var args = arguments;
  var pattern = new RegExp("%([1-" + arguments.length + "])", "g");
  return String(string).replace(pattern, function(match, index) {
    return args[index];
  });
};

这个函数实现了模板替换,你可以在要动态替换的地方使用 %1 到 %9 标记,然后其余的参数就会依次替换这些地方。例如:

format("And the %1 want to know whose %2 you %3", "papers", "shirt", "wear");

上面的脚本就会返回

"And the papers want to know whose shirt you wear" 。

在这里需要注意的是,即便在 format 函数定义中,我们仅定义了个名为 string 的参数。而 Javascript 不管函数自身定义的参数数量,它都允许我们向一个函数传递任意数量的参数,并将这些参数值保存到被调用函数的 arguments 对象中。

转换成实际数组

虽然 arguments 对象并不是真正意义上的 Javascript 数组,但是我们可以使用数组的 slice 方法将其转换成数组,类似下面的代码

var args = Array.prototype.slice.call(arguments);

这样,数组变量 args 包含了所有 arguments 对象包含的值。

创建预置参数的函数

使用 arguments 对象能够简短我们编写的 Javascript 代码量。下面有个名为 makeFunc 的函数,它根据你提供的函数名称以及其他任意数目的参数,然后返回个匿名函数。此匿名函数被调用时,合并的原先被调用的参数,并交给指定的函数运行然后返回其返回值。

function makeFunc() {
  var args = Array.prototype.slice.call(arguments);
  var func = args.shift();
  return function() {
    return func.apply(null, args.concat(Array.prototype.slice.call(arguments)));
  };
}

makeFunc 的第一个参数指定需要调用的函数名称(是的,在这个简单的例子中没有错误检查),获取以后从 args 中删除。makeFunc 返回一个匿名函数,它使用函数对象的(Function Object)apply 方法调用指定的函数。

apply 方法的第一个参数指定了作用域,基本上的作用域是被调用的函数。不过这样在这个例子中看起来会有点复杂,所以我们将其设定成 null ;其第二个参数是个数组,它指定了其调用函数的参数。makeFunc 转换其自身的 arguments 并连接匿名函数的 arguments,然后传递到被调用的函数。

有种情况就是总是要有个输出的模板是相同的,为了节省每次是使用上面提到的 format 函数并指定重复的参数,我们可以使用 makeFunc 这个工具。它将返回一个匿名函数,并自动生成已经指定模板后的内容:

var majorTom = makeFunc(format, "This is Major Tom to ground control. I'm %1.");

你可以像这样重复指定 majorTom 函数:

majorTom("stepping through the door");
majorTom("floating in a most peculiar way");

那么当每次调用 majorTom 函数时,它都会使用第一个指定的参数填写已经指定的模板。例如上述的代码返回:

"This is Major Tom to ground control. I'm stepping through the door."
"This is Major Tom to ground control. I'm floating in a most peculiar way."

自引用的函数

您可能会认为这很酷,先别急着高兴,后面还有个更大的惊喜。它(arguments)还有个其他非常有用的属性:callee 。arguments.callee 包含了当前调用函数的被引用对象。那么我们如何使用这玩意做些的事情?arguments.callee 是个非常有用的调用自身的匿名函数。

下面有个名为 repeat 的函数,它的参数需要个函数引用和两个数字。第一个数字表示运行的次数,而第二个函数定义运行的间隔时间(毫秒为单位)。下面是相关的代码:

function repeat(fn, times, delay) {
  return function() {
    if(times-- > 0) {
      fn.apply(null, arguments);
      var args = Array.prototype.slice.call(arguments);
      var self = arguments.callee;
      setTimeout(function(){self.apply(null,args)}, delay);
    }
  };
}

repeat 函数使用 arguments.callee 获得当前引用,保存到 self 变量后,返回个匿名函数重新运行原本被调用的函数。最后使用 setTimeout 以及配合个匿名函数实现延迟执行。

作为个简单的说明,比如会在通常的脚本中,编写下面的提供个字符串并弹出个警告框的简单函数:

function comms(s) {
  alert(s);
}

好了,后来我改变了我的想法。我想编写个「特殊版本」的函数,它会重复三次运行每次间隔两秒。那么使用我的 repeat 函数,就可以像这样做到:

var somethingWrong = repeat(comms, 3, 2000);
somethingWrong("Can you hear me, major tom?");

结果就犹如预期的那样,弹出了三次警告框每次延时两秒。

最后,arguments 即便不会经常被用到,甚至显得有些诡异,但是它上述的那些惊艳的功能(不仅仅是这些!)值得你去了解它。

使用 CSS 异步跨域获取数据

这是个非常「猥琐」的异步获取数据的方法,原理是通过动态载入一段 CSS,然后解析其中的字段提取数据( DEMO )。

先来说说它的优点。首先是跨域,数据获取的实质其实就是载入一段 CSS 。其次避免了类似 JSONP 的跨站脚本注入攻击。

然后考虑下可能会出现的问题。首先是通过 rawurlencode 编码 过的数据容量会大很多,而且不可读。

其次,如果使用不当可能会影响页面渲染,看作者的 Javascript 代码中加入了段

e.setAttribute("media", "print, csshttprequest");

很有意思。还有就是使用

@import url(about:chr:data);

真的让人感觉太太「猥琐」了。

原官方有个 [Python 编写的 Encoder 实现](http://nbio.googlecode.com/svn/csshttprequest/trunk/python/nbio/csshttprequest/

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHubTwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 要知道作为码农取名是件很难的事情,所以不想在取名这事情上太费心思。

作为八零后,自认为还仅存点点可能不怎么被理解的幽默感,以及对平淡生活的追求和向往。 为了避免不必要的麻烦,声明本站所输出的内容以及观点仅代表个人,不代表自己所服务公司或组织的任何立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

分类

搜索

文章