【译】CSS media queries在JavaScript中的应用(二)

原文链接:http://www.nczonline.net/blog/2012/01/19/css-media-queries-in-javascript-part-2/

在我的前一篇文章中,我介绍了CSS media query在JavaScript中的应用,包括一个自制的实现函数和CSSOM Views里的matchMedia()方法。在CSS和JavaScript中,Media query都是那么实用,以至于我继续对其进行了一些研究,探索更好的利用方式。最后我发现,matchMedia()方法还有一些我在写第一篇文章时没有意识到的有趣特性。

matchMedia()和它的有趣特性

再次调用matchMedia()会返回一个MediaQueryList对象,这个对象可以让你确定给出的media类型是否和浏览器当前状态匹配。MediaQuery对象的matches属性会使用一个布尔型数据来表明匹配的结果。很明显,在每一次调用marches属性的时候,它都会获取一次浏览器的状态:

var mql = window.matchMedia("screen and (max-width:600px)");
console.log(mql.matches);
//resize the browser
console.log(mql.matches); //requeries

这显然非常实用,因为这样你就可以保持对MediaQueryList对象的引用,从而可以重复检查query与页面之间的状态。

Chrome和safari还有一个怪异的行为。即使matches属性的初始值是正确的,默认情况下还是不会更新matches的值,除非页面含有一个对应相同的query和至少一个规则定义的media块。比如说,为了让一个表现形式为”“screen and (max-width:600px)““的MediaQueryList对象正确显示出来(包括能正确触发事件),你必须在你的CSS中包含一些内容:

@media screen and (max-width:600px) {
 .foo { }
}

media块中必须含有至少一个CSS规则,但是该规则是可以为空的。只要这个规则存在于页面上,那么MediaQueryList对象就可以正确更新,并且所有通过addListener()绑定的事件都可以正确触发。

你可以使用JavaScript来修复这个问题:

var style = document.createElement("style");
style.appendChild(document.createTextNode("@media screen and (max-width:600px) { .foo {} }"));
document.head.appendChild(style); //WebKit支持document.head

当然你或许会需要为每一个使用matchMedia()访问的media query应用这个修复,这可能会显得有些繁杂。

Firefox中的实现也有一些怪异的特性。理论上,你可以注册一个处理器监听query状态的改变,这样你就不需要一直保持对MediaQUeryList对象的引用了,如下:

//在Firefox中无效
window.matchMedia("screen and (max-width:600px)").addListener(function(mql) {
 console.log("Changed!");
});

在Firefox中,即使media query是有效的,监听器或许仍然不会被调用。在我的测试中,它可能会被触发0到3次,然后再也不会被触发。Firefox团队已经了解了这个bug,并且应该很快会修复这个问题。所以,在使用这个方案的同时,你还是需要继续引用MediaQueryList对象来确保监听器被触发了:

//fix for Firefox
var mql = window.matchMedia("screen and (max-width:600px)");
mql.addListener(function(mql) {
 console.log("Changed!");
});

因为mql的存在,监听器就一直可以被触发。

监听器相关的更多内

在我的上一篇文章中,由于对一部分内容的误解,我对media query的描述有一些错误。监听器会在两个情景下被触发:

  1. media query一开始是有效的。就像前一个例子中,屏幕宽度变成600px或者更低。
  2. media query一开始是无效的。例如屏幕宽度大于600px。

这就是为什么需要将MediaQueryList对象传入到监听器中,这样你可以通过检查matches属性来确定media query是否有效。例如:

mql.addListener(function(mql) {
 if (mql.matches) {
 console.log("Matches now!");
 } else {
 console.log("Doesn't match now!");
 }
});

通过这样的代码,你就可以监控一个web应用的状态,从而可以做一些适当的调整。

是否需要对matchMedia()进行模拟(不支持的浏览器中)

在我在了解matchMedia()的时候,我试图创建一个函数模拟matchMedia()(原文称为polyfill,作为一种不支持matchMedia()方法情况下的替代措施)。Paul Irish使用类似我最近一篇文章里描述的技术实现了一个polyfill。Paul Hayes之后开设了一个分支来创建polyfill,使用了一个基于简单CSS transition的监听器来检测改变。然而,由于它依赖于CSS transitions,监听器的兼容性受到CSS transition兼容性的限制。加上调用matches不能再次获取浏览器状态与Firefox和webkit中的bug,让我坚信创建一个polyfill并不是一个正确的方向。毕竟,当真实环境中存在那么多明显的bug时,polyfill又怎么能正确执行。

我选择的方式是创建一个容器来包裹API的行为,从容器处消除这些问题。当然,我选择将这个API作为YUI Gallery的一个模块实现,叫做gallery-media。这个API很简单,并由两个方法组成。第一个是Y.Media.matches(),调用media query字符串并返回匹配结果。不需要记录任何对象,只需要直接这样获取:

var matches = Y.Media.matches("screen and (max-width:600px)");

第二个方法是Y.Media.on(),它允许你来指定一个media query和一个监听器,当media query生效或者失效时执行。监听器的参数是一个含有matches和media属性的对象,提供media query的信息。例如:

var handle = Y.Media.on("screen and (max-width:600px)", function(mq) {
 console.log(mq.media + ":" + mq.matches);
});
//detach later
handle.detach();

取代使用CSS transitions来监控改变,我使用了一个简单的onresize事件处理器。在桌面系统中,浏览器的尺寸是最有可能被改变的(在移动设备中,方向也有可能被该改变),那么我为旧的浏览器做了这么一个假设。API使用了原生的matchMedia()函数,在WebKit和Chrome中都有效并进行了一些修复,从而可以得到稳定的行为。

总结

JavaScript中的CSS media query比我想象的复杂一些,但是非常实用。我不觉得通过polyfill的方式修复marchMedia()是一个好主意,它让你不能在多个浏览器中使用相同的方式编码。好处是,它将你从bug和改变中隔离出来,从而看起来会更加先进一些。现在让我们使用JavaScript和CSS media query创造更多的可能。

参考

  1. CSS media queries in JavaScript, Part 1
  2. Rob Flaherty’s tweet
  3. matchMedia() MediaQueryList not updating
  4. matchMedia() listeners lost
  5. matchMedia polyfill
  6. matchMedia polyfill
  7. YUI 3 Gallery Media module

前一篇:【译】CSS media queries在JavaScript中的应用(一)

【译】CSS media queries在JavaScript中的应用(一)

原文地址:http://www.nczonline.net/blog/2012/01/03/css-media-queries-in-javascript-part-1/

在2011年初,我参与了一个关于JavaScript特性检测的项目。一些问题的出现让我觉得使用CSS media query或许会更好,所以我花了一些时间编写一个使用JavaScript操作CSS media queries的函数。我的思路很简单:如果我只需要基于media query就可以控制特定的CSS,那么,我也可以基于media query执行特定的JavaScript。

var isMedia = (function(){
 var div;
 return function(query){
 //如果<div>不存在, 则创建一个并让其隐藏
 if (!div){
 div = document.createElement("div");
 div.id = "ncz1";
 div.style.cssText = "position:absolute;top:-1000px";
 document.body.insertBefore(div, document.body.firstChild);
 }
 div.innerHTML = "_<style media=\"" + query + "\"> #ncz1 { width: 1px; }</style>";
 div.removeChild(div.firstChild);
 return div.offsetWidth == 1;
 };
})();

这个函数的思路很简单。我创建了一个<style>节点,在里面设置了media属性来对应我正在测试的样式。这个样式会应用在<div>标签上,我需要做的就是检查这个样式是否已经应用到对应的节点。我觉得应该避免一些浏览器探测,所以我没有使用currentStyle或者getComputedStyle(),而是选择改变一个元素的宽度,然后用offsetWidth来检查这个改变。

很快,我们就创建了一个函数,并且它可以在几乎所有的浏览器中运行。和你猜的一样,在IE6和IE7上会有一些问题。在这两个个浏览器上,<style>元素是一个NoScope元素(比如文本节点,注释节点或者script)。NoScope意味着当页面受到innerHTML或者其他类似的操作注入时,会发生一些可怕的事情。如果NoScope元素是作为HTML字符串的最前面部分被添加的,那么所有这些元素都会被丢弃。为了能正常使用NoScope元素,你必须确定这个元素不是HTML字符串的第一部分。因此,我在<style>元素签名增加了一条下划线,然后再将其删除,这样就能保证在IE6和IE7下保持正常。其他浏览地没有这个NoScope问题,但是使用这个技术并不是一个完美的方案(我之前说过我尽量要避免浏览器探测)。

最后,你可以这样使用这个函数:

if (isMedia("screen and (max-width:800px)"){
 //在screen模式下这样使用
}
if (isMedia("all and (orientation:portrait)")){
 //portrait模式下
}

isMedia()函数在所有浏览器里都工作地很好,它可以很好地检测media query在浏览器中的有效性。也就是说传入一个无效的media query会返回一个false。比如在IE6下,传入”screen”会返回true,但是更复杂的参数传入的话会返回false。这个结果是可以接收的,因为其他没有匹配到的media query中的任意CSS样式都不应该在对应的浏览器中应用。

CSSOM View

CSS Object Model(CSSOM)Views规范增加了对JavaScript操作CSS media query的原生支持,它在window对象下增加了matchMedia()方法。你可以传入一个CSS media query然后返回一个MediaQueryList对象。这个对象包括两个属性:matches,布尔值数据,表示CSS media query是否与当前的显示状态匹配;media对应传入的参数字符串。例如:

var match = window.matchMedia("screen and (max-width:800px)");
console.log(match.media); //"screen and (max-width:800px)"
console.log(match.matches); //true or false

到这里,API没有提供的支持和我之前的想法差不多。你或许会考虑为什么matchMedia()会返回一个对象?毕竟,如果没有匹配成功,返回的对象也毫无用处。答案就在addListener()和removeListener()上。

这两个方法允许你基于CSS media query与视图状态进行交互。例如,当模式转换到“portrait”模式时,或许你会想要有一个提示。你可以这么做:

var match = window.matchMedia("(orientation:portrait)");
match.addListener(function(match){
 if (match.media == "(orientation:portrait)") {
 //do something
 }
});

这段代码对media query进行了监听。当query值和当前的视图状态对应时,监听器对应的函数就会执行,而对应的MediaQueryList对象也会传入。用这个方式吗,你可以让你的JavaScript可以很快地响应布局变化,并且不需要用轮询的方式。那么,这里就和我的想法不太一样了,这个API允许你监听视图状态的改变,并响应调整界面的行为。

matchMeida()方法在Chrome,Safari5.1+,FireFox 9+和IOS 5+上的Safari上都已生效。这些是我了解并已核实的浏览器。IE和Opera依然没有在最近的几个版本中实现这个方法。

注意:WebKit上的实现有一个BUG,当MediaQueryList对象创建后,matches不会更新,query监听器也不会触发。希望这个问题能早点修复。

总结

CSS media query带来了一个在CSS和JavaScript中都适用的特性检测语法。我期望media query可以在未来成为JavaScript代码中的重要部分,在界面变化发生时,开发者能检测得到。实在没有理由说web应用的行为不应该响应布局的变化,而CSS media让我们变得更加强大。

参考

A function for detecting if the browser is in a given media mode
MSDN: innerHTML Property
CSS Object Model View
matchMedia() MediaQueryList is not updating

解码jQuery系列4 – 函数作用域、jQuery连缀模式和jQuery.fn


石川著, 李松峰编译

“OOP与jQuery”是“解码jQuery”系列中的一个子系列,主要讨论 jQuery 的内部构成及相关的OOP(Object Oriented Programming,面向对象编程)概念。

在这篇文章中,我们会讨论函数作用域、jQuery连缀模式和jQuery.fn。

1.函数作用域

我们都看到了,jQuery对象被包装在了很多层函数中。为什么我们用函数来定义作用域呢?因为JavaScript不能用花括号定义作用域。JavaScript变量只有全局作用域和函数作用域。

举例来说吧,为了把下面这个jQuery变量变成私有变量,必须把它包装在一个函数中:

function a() {
  var jQuery = "hellow world";
}
console.log(jQuery); // undefined

JavaScript函数具有词法作用域。什么意思呢?就是说,函数的作用域是在定义的时候创建的,而不是在执行的时候创建的。

var jQuery = 'hi there';
var f = function() {
  console.log(jQuery); // "undefined"
  var jQuery = "hello world";
};
f();
// undefined

所以,如果运行上面的代码,返回的值会是`undefined`,而不是`’hi there’`。 就是说,在同一个变量作用域或者同一个函数内,只要有使用var声明变量jQuery的语句,就可以在函数中的任何位置访问它,包括在var语句之前。所以上面的等同于:

var jQuery = 'hi there';
var f = function() {
  var jQuery; // same as -> var jQuery = undefined;
  console.log(jQuery); // "undefined"
  jQuery = "hello world";
};
f();
// undefined

就是说,函数f有自己的作用域,它没有读var jQuery = ‘hi there’。但在函数f内部,确实定义了jQuery变量,只不过调用console.log()时,jQuery还没有定义。

2.jQuery连缀模式

看看init方法,不知道你是否注意到了,这个方法返回this。

var jQuery = function( selector, context ) {
  // jQuery对象实际上就是一个“增强版的”init构造函数
  return new jQuery.fn.init( selector, context, rootjQuery );
}
jQuery.fn = jQuery.prototype = {
  init: function( selector, context, rootjQuery ) {
    //...
    return this;
    //...
  }
}

在jQuery中,可以在一个方法调用后面再连缀另一个方法调用。之所以能够如此,是因为每个方法都像这里一样返回this对象。

还是不清楚?我们来解释一下。首先,来看看对象中的this关键字有什么用。

var city = {
  name: 'beijing',
  getName: function() {
    return this.name;
  }
}
console.log(city.getName());

以上代码输出的城市名是beijing,说明this引用的是city对象。

再进一步,如果我们返回的是整个对象呢?

var num = {
  value: 1,
  minus: function (n) {
   this.value -= n;
   return this;
  },
  plus: function (n) {
    this.value += n;
    return this;
  },
  getVal: function () {
    console.log(this.value);
  }
};

num.minus(2).plus(5).getVal();

这样,就可以把方法调用连缀起来了。

3.jQuery.fn

在core.js中,我们可以看到这行代码:

jQuery.fn = jQuery.prototype = {
  //...
}

在我们开发jQuery插件时,经常要用到$.fn,也就是jQuery.fn。现在知道了吧,jQuery插件其实就是添加到jQuery.prototype的方法。正因为如此,插件里也不要忘了返回this噢。

(function( $ ){
  $.fn.fontcolor = function() {
    return this.each(function() {
      $(this).css("color", "orange");
    });
  };
})( jQuery );

系列其他文章

解码jQuery系列3 – 原型


石川著, 李松峰编译

“OOP与jQuery”是“解码jQuery”系列中的一个子系列,主要讨论 jQuery 的内部构成及相关的OOP(Object Oriented Programming,面向对象编程)概念。

第一篇文章以 jQuery 代码为例解释了变量(数据)和函数的概念。第二篇文章通过 jQuery 介绍了对象。

在这篇文章中,我们来谈一谈原型(prototype)。因为 JavaScript 是基于原型的语言,所以原型是这门语言中的一个非常重要的概念。那基于类的语言呢?这篇文章会不会比较它和基于原型的语言?不会,至少现在不会。我觉得要理解原型,不一定非要对类和原型进行比较。假如你想教人说日语,那不一定要让他先学会希腊语。当然,这个人掌握了日语之后,对他学希腊语肯定是有帮助的 :) 下面就来看一看什么是原型,以及jQuery怎么使用原型。

1.每个函数都有一个原型,原型是对象

可以在 JavaScript 控制台中测试 core.js 文件:

var jQuery = function( selector, context ) {
    //...
}
console.log(typeof jQuery.prototype);

// 返回对象

2.给原型添加方法和属性

给函数添加方法和属性的一种常见方式是像下面这样:

jQuery.prototype.constructor = jQuery;
jQuery.prototype.init = function( selector, context, rootjQuery ) {
    //...
}

jQuery的原型(大约在core.js的第78行)就保存在jQuery函数的prototype属性中,提取出来的话,就像下面这样(第78行代码中实际上还有一个 jQuery.fn,它只不过是 jQuery.prototype 的别名而已;为了简单起见,就把它先忽略吧):

jQuery.prototype = {
    constructor: jQuery,
    init: function( selector, context, rootjQuery ) {
      //...
    }
}

对以上jQuery代码而言,它其实是用一个新对象完全重写和替换了最初的原型。那么,逐个给原型添加属性与完全重写原型有什么区别吗?没有什么太大区别,这就像是一个意思的两种不同表达方式,比如:

我可以说:我有一只绿色的猫,一只蓝色的猫和一只粉红色的猫。
也可以说:我有三只猫,分别是绿色的、蓝色的和粉红色的。

实际上还有第三种为函数添加属性和方法的方式:

function jQuery() {
    this.constructor= jQuery;
    this.init= function( selector, context, rootjQuery ) {
      //...
    }
}

3.使用原型的方法和属性

要使用方法和属性,必须创建一个新对象。以下就是在jQuery中创建新对象的方式,大约是在第6行:

new jQuery.fn.init( selector, context, rootjQuery );

另外,大约在第303行,还有如下代码:

jQuery.fn.init.prototype = jQuery.fn;

哎呀,jQuery函数、对象、jQuery.fn、原型,还有init之间是什么关系呢?

下一篇……
下一篇,我们将继续探索jQuery核心,介绍jQuery.fn、原型和init方法之间的关系。

var jQuery = function( selector, context ) {
    // jQuery对象实际上就是一个“增强版的”init构造函数
    return new jQuery.fn.init( selector, context, rootjQuery );
}
jQuery.fn = jQuery.prototype = {
    init: function( selector, context, rootjQuery ) {
      //...
    }
}
// 为 init 函数赋予 jQuery 原型,以方便后面实例化
jQuery.fn.init.prototype = jQuery.fn;

推荐阅读
Stoyan Stefanov 编著的 Object-Oriented JavaScript 是一本最容易看懂的 OOP JavaScript 图书。

系列其他文章

JavaScript闭包详解【2】

之前在交流会上,和杜欢讨论过闭包的一些小东西,这里的文章主要还是从闭包产生的结果切人,讲述闭包的一些妙用,不探讨闭包产生的原因

函数参数的嵌入

函数参数的嵌入技术,原文是”Partially Applying Functions”,是一种在函数执行前,对其增加参数的方法。事实上,这个方法让你调用了一个新的函数。

注:assert是qunit单元测试所用的方法,第一个参数若为true则输出第二个参数,通常true表示执行结果为预期的值,下面都用到了assert对结果进行校验

String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = ("John, Resig, Boston").csv();
assert( results[1] == "Resig", "字母切分成功" );

partial方法就是能够实现partially apply的函数,我们可以看到虽然csv在调用的时候没有带任何参数,但是还是按照/,\s*/进行split了。

prototype框架里有一个curry函数,和partial比较类似。

Function.prototype.curry = function() {
  var fn = this, args = Array.prototype.slice.call(arguments);
  return function() {
    return fn.apply(this, args.concat(
      Array.prototype.slice.call(arguments)));
 };
};

通过闭包的使用,我们将this变量和args变量保存下来,并可以让返回的函数能够访问到,在返回的函数中,我们将本身的参数和保存下的args参数进行合并,也就做到了嵌入参数的功能。

curry函数看起来很妙,但是我们还可以做的更好。如果我们需要将一些参数保留给可能传入的函数,那么就需要做一些更多的处理。

Function.prototype.partial = function(){
 var fn = this, args = Array.prototype.slice.call(arguments);
 return function(){
  var arg = 0;
  for ( var i = 0; i < args.length && arg < arguments.length; i++ )
   if ( args[i] === undefined )
    args[i] = arguments[arg++];
  return fn.apply(this, args);
 };
};

我们通过将预设参数中的undefine值替换为传入的函数,从而实现了一个更方便的参数嵌入函数。看几个实用的例子。

//delay函数的实现
var delay = setTimeout.partial(undefined, 10);
delay(function(){
 assert( true, "函数延迟调用成功" );
});
//事件绑定的实现
var bindClick = document.body.addEventListener
 .partial("click", undefined, false);
bindClick(function(){
 assert( true, "当前函数绑定了click事件" );
});

这个技术的主要目的函数缩小代码的复杂度,让API的使用更加方便清晰。

对闭包的产生和原理感兴趣的同学,可以看下http://www.otakustay.com/closure-ppt/

本文翻译自《Secrets_of_the_JavaScript_Ninja》

解码jQuery系列2 – 对象


石川著, 李松峰编译

“OOP与jQuery”是“解码jQuery”系列中的一个子系列,主要讨论 jQuery 的内部构成及相关的OOP(Object Oriented Programming,面向对象编程)概念。

上一篇中,我们以 jQuery 代码为例解释了变量(数据)和函数的概念。本篇将通过 jQuery 来介绍对象。

1.对象就是对象,跟人和椅子一样
既然我们讨论的是面向对象编程,那对象是什么呢?对象就是对象,比如苍井空老师就是一个对象,桌椅或狗也是对象。所谓面向对象,不过就是用编程语言来表示对象而已。

如上所述,那么 shichuan 就是一个对象。因此在 OOP 中,可以用一个对象来表示他:

var shichuan = {};

每个对象,都可以有属性和方法(行为)。比如,shichuan 的头发是黑色的,这就是他的属性。因为天生就是黑色的,他不用时不时地把头发染黑,所以它不是方法(行为)。我们可以把这个属性添加到 shichuan 对象中:

var shichuan = {
    hair: "black"
};

好了,假设 shichuan 有一个特长是骑独角兽,那么骑独角兽(riding unicorn)就是他的一个方法(行为)。用 OOP 来表示,就是这样:

var shichuan = {
    hair: "black",
    ridingUnicorn: function() {
        // 我怎么骑独角兽
    }
};

总结一下对象的概念:

包含对象的变量名叫 shichuan
对象的内容被包含在 { 和 } 中
对象的元素(属性和方法)用逗号来分隔
键/值对用冒号分隔,比如 key : value(或者上面例子中的 hair : “black”)
方法实际上也是函数,比如 ridingUnicorn 就是对象 shichuan 的一个方法,行为(函数)

2.jQuery 中的对象
那么,jQuery 中是怎么使用对象的呢?还记得我们在第一篇文章里谈到的 jQuery 函数的局部副本吧。下面我们就看看 jQuery 函数的内部(大约在第4行),实际上这个函数里只有一行代码,其注释说:jQuery 对象实际上就是一个“增强版的”init构造函数。那这个增强的 init 是什么样呢?

var jQuery = function( selector, context ) {
    // jQuery对象实际上就是一个“增强版的”init构造函数
    return new jQuery.fn.init( selector, context, rootjQuery );
}

如果你在代码里搜索“jQuery.fn”,在大约第76行可以找到它,大致是这样的:

jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    init: function( selector, context, rootjQuery ) {
    ...
    },
    ...
}

jQuery的原型(下一篇文章将介绍原型)——jQuery.prototype,是一个对象,一个大对象。这个对象有很多很多属性和方法。比如,它的属性有 constructor、selector、jquery、length,等等;它的方法有 init、size、toArray、get、pushStack,等等。

3.函数是数据,也是对象
在第一篇文章中,我们说过函数就是数据,通过以下两种方式定义jQuery函数,结果是相同的:

// 局部 jQuery
function jQuery( selector, context ){
    //...
}
// 局部 jQuery
var jQuery = function( selector, context ){
    //...
}

实际上,还有第三种方法:

// 局部 jQuery

var jQuery = new Function('selector', 'context', '//...');

在OOP中,使用 new 加构造函数(这里的 Function)是创建新对象的典型方式。虽然这种方法也能创建函数,但通常并不是最好的方式,因为 JavaScript 会使用 eval 对传入的源代码进行求值。

下一篇……
下一篇,我们将继续探索jQuery核心,介绍原型。

推荐阅读
Stoyan Stefanov 编著的 Object-Oriented JavaScript 是一本最容易看懂的 OOP JavaScript 图书。

系列其他文章

解码jQuery系列1 – 变量和函数


石川著, 李松峰编译

光开放源代码是不够的。为了让开源进一步开放,在“解码jQuery”系列中,我们会剖析jQuery的每一个方法,领略jQuery框架之美,同时向这个框架背后的天才们致敬。

“OOP与jQuery”是“解码jQuery”系列中的一个子系列,主要讨论jQuery的内部构成及相关的OOP(Object Oriented Programming,面向对象编程)概念。

学习OOP概念的最佳方式,就是剖析一个真实的框架。学习某个框架的最佳方式,就是领会其中的OOP概念。

jQuery在整合压缩前是分为很多不同文件的。它的源代码可以在github的这页看到。 jQuery core (在core.js里)是jQuery的核心。面向对象有5个重要的概念:变量(数据),函数,对象,原型和继承。这些是JS语言面向对象的基础,也是jQuery的基础。

jQuery核心(core.js)是jQuery的“大脑”,其中涉及五个重要概念:变量(数据)、函数、对象、原型和继承。这五个方面是jQuery核心的五个“脑叶”,又是探求OOP概念的五个“意识”。

整个jQuery库都浓缩在一个jQuery变量中。实际上,core.js中有两个jQuery变量(var jQuery),一个是全局的,另一个是局部的。今天我们就通过jQuery来了解JavaScript中的变量和函数这两个概念。下面就是刚提到的两个jQuery变量:

// 全局 global jQuery
var jQuery = (function() {
  // 局部 local jQuery
  var jQuery = function( selector, context ) {
    // ...
  }
})();

1. 变量保存数据,函数也是数据
我们先来看一看局部的jQuery:

// 局部 local jQuery
var jQuery = function( selector, context ) {
  //...
}

JavaScript中的函数实际上是数据。也就是说,通过以下两种方式定义jQuery函数,结果是相同的:

// 局部 local jQuery
function jQuery( selector, context ){
  //...
}
// 局部 local jQuery
var jQuery = function( selector, context ){
  //...
}

2. 函数可以匿名
再来看一看全局的jQuery:

// 全局 global jQuery
var jQuery = (function() {
  //...
})();

从中把函数部分提取出来,就会发现它是匿名的:

function() {
  //...
}

在JavaScript中,可以不把数据赋值给变量,而数据照样可以存在。比如,下面这个字符串可以存在于JavaScript代码中,而且不会返回任何错误:

"我是数据,我不会导致错误。"

前面第1点提到了“函数是数据”,因此函数也可以独立存在,而不需要被赋值给某个变量。假如你运行上面的匿名函数,它100%可以运行,而且不会导致任何错误。那匿名函数有什么用呢?下一点就来回答这个问题。

3. 匿名函数可以自调用
匿名函数的一个优点是可以作为自调用的函数来使用。举个例子,下面的函数在页面加载时会自动执行,你可以在控制台看到输出:

var jQuery = (function() {
  console.log("我是自调用。");
})();

那为什么要像这样来使用匿名函数呢?因为这样可以在不创建全局变量的情况下执行一些内部操作。jQuery使用匿名自调用函数来完成它的一次性初始化。

下一期…
下一篇,我们将继续探索jQuery核心,介绍原型和对象的概念。

系列其他文章

使用Coffeescript编写jQuery插件

如果你知道jQuery和Coffeescript,要编写一个jQuery插件是很容易的。

我们将通过写一个jQuery的插件,它会允许我们为表中的行添加交替颜色交替。

这里是整个插件:

$ = jQuery
$.fn.zebraTable = (options) ->
    defaults =
        evenColor: '#ccc'
        oddColor : '#eee'

    options = $.extend(defaults, options)
    @each ->
        $("tr:even", this).css('background-color', options.evenColor)
        $("tr:odd" , this).css('background-color', options.oddColor)

让我们看看它是怎么执行的:

  1. 我们把为$绑为jQuery对象。
  2. 我们创建了一个匿名函数并添加到jQuery$,分配给fn.zebraTable。
  3. 在此之后,我们将能够做$(“选择”)。zebraTable()或`$(选择)。zerbraTable(optionsDict)
  4. 此功能将设置交替行的背景颜色。
(function() {
  var $;

  $ = jQuery;

  $.fn.zebraTable = function(options) {
    var defaults;
    defaults = {
      evenColor: '#ccc',
      oddColor: '#eee'
    };
    options = $.extend(defaults, options);
    return this.each(function() {
      $("tr:even", this).css('background-color', options.evenColor);
      return $("tr:odd", this).css('background-color', options.oddColor);
    });
  };

}).call(this);

你可以这样调用它。

$(function() {
  $("table").zebraTable(  {
      evenColor: 'red',
      oddColor: 'yellow'
    });
});

显着特点:

  1. 由于Coffeescript将代码括在函数里, $= jQuery不覆盖一切,没有必要附上自己的封闭的插件。
  2. 插件里面的this是指选定的jQuery对象,所以也没有必要用$(this)。

参考
翻译自 http://agiliq.com/blog/2012/01/writing-jquery-plugins-using-coffeescript/

OYE – AMD模块化开发思想的实现原理及应用

为了推行模块话的开发思想,将刚推出不久的AMD模块化加载管理器 - OYE,拿出来跟大家分享一下,希望能够多尽一份力!

AMDCommonJS规范的一个分支,它更适合在浏览器端进行实现。

OYE是对AMD规范的一个实现!

OYE概述

  • ✔ 目标单纯,只做模块化,超轻量
  • ✔ 不依赖Script onload事件,更稳定可靠
  • ✔ 完美支持具名、匿名模块
  • ✔ 支持模块提取完整依赖列表
  • ✔ 支持模块循环依赖检测
  • ✔ 支持require与define在同一个文档

OYE实现原理

辅助项

[[当前正在处理的模块名称]], [[请求队列]]

define

define函数执行时,如果是具名模块,则直接将模块定义体与模块名进行注册;如果是匿名模块,则取[[当前正在处理的模块名称]]为模块名称进行模块注册;最后将[[当前正在处理的模块名称]]置空;

require

require函数执行时,如果[[当前正在处理的模块名称]]为空,则发起文件请求,并将本次请求的模块名称置为[[当前正在处理的模块名称]];否则,将本次请求丢到[[请求队列]]

依赖关系

采用触发器方式,一个模块准备好后,触发所有依赖它的模块进行检查,看它们是否也准备好

应用指引

开发阶段

确认当前项目准备采用模块化的方式进行开发;
每个模块定义为匿名模块,并存储为一个独立的JS文件;
每个页面最好只引入一个模块,如:首页,可以有一个app/home这样的模块,在这个模块中再去调用其他模块,如:

<你的站点路径或CDN路径>resource/js/app/home.js
define(['common/head','common/foot','app/home/content'],
function(head,foot,content){
    return {
        init:function(){
        head.init();
        foot.init();
        content.init();
    }
};
});

然后,在home页面,我们只要这样去调用即可:

<script src="<你的站点路径或CDN路径>resource/js/oye/oye.min.js"></script>
<script>
require('app/home', function(home){
    home.init();
});
</script>

请注意:

模块文件home.js与oye.js的目录关系,在oye里,模块名对应模块的路径!
如果模块名是以http或https等协议开头的,则视为绝对路径,oye会直接去请求这个URL;
否则,都被视为相对路径,相对于oye.js所在的路径而言,而不是相对于当前页面的路径,这个很重要!

为什么设计成这样?

因为我们需要覆盖线上线下两种应用场景:
线下,即开发阶段,我们的资源文件与项目内容基本上是作为一个项目的整体来开发的;
线上,资源文件和项目内容多数情况下会分开部署到不同的域;

更多具体用法,请参见我们给出的 OYE DEMO演示 !

项目上线(可选)
如果公司的站点线上与开发阶段完全不一致,则需要考虑以下步骤。

在BUILD工具中添加提取JS模块依赖列表、合并压缩的功能;实现方式大致如下:
通过对模块依赖列表的提取,将各依赖模块的模块名自动添加到提取出来的内容中,即为原来的匿名模块加上模块名称,如:
(举例)我们通过对app/home提取依赖列表,可以得到:

app/home: http://你的站点路径/resource/js/app/home.js
common/head: http://你的站点路径/resource/js/common/head.js
common/foot: http://你的站点路径/resource/js/common/foot.js
app/home/content: http://你的站点路径/resource/js/app/home/content.js

现在,我们的BUILD工具就可以遍历读取这些文件,并将读取的内容做如下替换:
将 define( 替换成 define(“app/home” ,即将模块名填充进去。
将已知的具名模块配置中,添加一条:define.amd.namedModules['模块名'] = true;
将提取并修改后的所有内容保存到一个JS文件,如:app/home.min.js
在页面上,引入oye.min.js文件的后面,添加一个对该文件的引入:<script src=”<你的站点路径或CDN路径>resource/js/app/home.min.js”></script>

现在就去体验使用OYE作为模块化开发的便利吧!