CoffeeScript入门实践

嗨,大家好。

在这一期的节目中,我们将介绍下CoffeeScript, 那么什么是CoffeeScript呢?CoffeeScript是一门小语言,也是一个编译器。用CoffeeScript编写的代码,可以编译成JavaScript代码。

CoffeeScript简化了JavaScript的编写,同时运用了JavaScript最佳实践模式。

本期节目将分为3个部分

  1. 1. 第一部分中我们讲述安装的过程
  2. 2. 第二部分我们将看如何使用它
  3. 3. 第三部分中我们将看它如何简化了JavaScript的编写,同时怎么保证了最佳实践模式。

一、安装的过程

要安装CoffeeScript,需要先安装node.js和npm。在mac 和 linux 的环境下,CoffeeScript的安装是相当容易的。比较复杂的呢是在Windows的环境下安装。所以我们着重讲一下在Windows环境下的安装方式。

首先我们先到node.js的网站下载在Windows版本的node.js。下载地址:http://nodejs.org/#download

下载后只要双击msi文件便可安装,整个过程是自动的。最近,npm也被加入到Node的Windows版本中,所以就不用额外再安装了。

安装完Node(包括npm)后,我们到Programm Files下,可以看到nodejs文件夹,在文件夹里,有node.exe, 为了方便,我在桌面上放了个快捷方式。

之后,我们到CoffeeScript的github页面下载CoffeeScript:https://github.com/jashkenas/coffee-script/downloads

下载并解压,我下载的到版本解压缩后的目录是jashkenas-coffee-script-127653b。我们可以把它放到另一个目录中,在这里我们把它放在C盘的dev目录中。

此时,Node和CoffeeScript在硬盘中的目录分别是:

 Node:C:\Program Files\nodejs\

 CoffeeScript:C:\dev\jashkenas-coffee-script-127653b

接下来,在桌面上建一个文件,命名为coffee.cmd。在任何一种编辑器下打开它。在里面打入下面的一段命令:

@echo off
node.exe C:\dev\jashkenas-coffee-script-127653b\bin\coffee %*

保存文件。之后把这个coffee.cmd 放到nodejs目录内。

二、.使用

在dev或任何其他目录中,建立一个目录,比如js。

在这个文档下,我们创建一个文件,命名为test.coffee。

这时打开cmd,打入下面的命令:

> cd C:\dev\js
> coffee -w -c test.coffee

//-w 的全名是 --watch, 是实时编辑的命令
//-c 的全名是 --compile, 是编译的命令

这时,会看到js目录中生成了test.js。在编辑器中打开test.js,可以看到一个匿名函数。这时当我们在test.coffee中打入任何代码,保存后,可以看到test.js被同步更新。

除此之外,我们还可以看到很多其它的命令:

把src目录中的CoffeeScript文件目录树的js在lib目录中编译成平行树

coffee --compile --output lib/ src/

把多个文件串连成一个文件

coffee --join project.js --compile src/*.coffee

三、使用实例

(一) 函数

如果我们输入下面的代码:

square = (x) -> x * x

在保存后,看生成的文件,我们可以看到文件的最上方有 square 变量的声明,下面我们把一个匿名函数分配给了这个变量:

var square;
square = function(x) {
    return x * x;
};

当我们再输入下面的代码时:

cube = (x) -> square(x) * x

会显示为:

var cube, square;
square = function(x) {
    return x * x;
};

cube = function(x) {
    return square(x) * x;
};

函数也可能有参数的默认值。用一个非空的参数覆盖默认值。

fill = (container, liquid = "coffee") ->
"Filling the #{container} with #{liquid}..."

会变为:

var fill;
fill = function(container, liquid) {
if (liquid == null) liquid = "coffee";
return "Filling the " + container + " with " + liquid + "...";
};

(二) 对象

下面的CoffeeScript:

kids =
brother:
name: "Max"
age: 11
sister:
name: "Ida"
age: 9

会显示为:

kids = {
    brother: {
        name: "Max",
        age: 11
    },
    sister: {
        name: "Ida",
        age: 9
    }
};

在JavaScript中,你不能使用保留字(???reserved words)。比如在没有用字符串引用它们的情况下,把class作为对象的属性。

CoffeeScript注意到用作键的对象和他们的报价为您的保留字,所以你不必担心它(例如,当使用jQuery)。下面这段:

$('.account').attr class: 'active'
log object.class

会显示为:

$('.account').attr({
"class": 'active'
});
log(object["class"]);

(三) 词法作用域和变量安全

这种行为实际上是相同于Ruby的局部变量作用域。

outer是不能重新声明在内部函数,因为它是已经在作用域内;

另一方面,inner在函数内部,不应该能够改变同名的外部变量的值,因此有自身的声明。

所以下面一段:

outer = 1
changeNumbers = ->
inner = -1
outer = 10
inner = changeNumbers()

会变成:

var changeNumbers, inner, outer;
outer = 1;
changeNumbers = function() {
    var inner;
    inner = -1;
    return outer = 10;
};
inner = changeNumbers();

【译】在本地存储中保存图片和文件

原文地址:http://hacks.mozilla.org/2012/02/saving-images-and-files-in-localstorage/

你可能已经对本地存储有所了解,本地存储在浏览器中快速存储数据的时候特别强大,并且已经在浏览器中存在多时。但是如何才能在本地存储中保存文件呢?

首先,推荐你先阅读Storing images and files in IndexedDB

使用JSON实现强大的本地存储控制

首先,我们先了解一些基本的本地存储相关的知识。你可以使用键值对的方式往本地存储中存储数据,就像这样:

localStorage.setItem("name", "Robert");

而从本地存储中读取数据的方式如下:

localStorage.getItem("name");

这样的存取方式非常不错,而且最多可以存储5M的数据,给你更多选择的空间。但是由于本地存储是基于字符串的存储,存储一串没有结构的字符串并不是一个理想的选择。因此,我们可以利用浏览器中原生的JSON支持来将JavaScript对象转化成字符串,从而保存到本地数据中,在读取的时候也可以将其转换回JavaScript对象。

图片的存储

我们的想法是做到将已经当前页面中已缓存的图片保存到本地存储中。不过就像我们之前已经确定的,本地存储只支持字符串的存取,那么我们要做的就是将图片转换成Data URI。其中一种实现方式就是用canvas元素来加载图片。然后你可以以Data URI的形式从canvas中读取出当前展示的内容。

让我们看一个例子。

//当图片加载完成的时候触发回调函数
elephant.addEventListener("load", function () {
 var imgCanvas = document.createElement("canvas"),
 imgContext = imgCanvas.getContext("2d");

 // 确保canvas元素的大小和图片尺寸一致
 imgCanvas.width = elephant.width;
 imgCanvas.height = elephant.height;

 // 渲染图片到canvas中
 imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height);

 // 用data url的形式取出
 var imgAsDataURL = imgCanvas.toDataURL("image/png");

 // 保存到本地存储中
 try {
 localStorage.setItem("elephant", imgAsDataURL);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
}, false);

如果我们想要考虑地更长远一些,那么还可以利用JavaScript对象并做一些数据检查。在这个例子中,第一次我们从服务端读取图片,之后每一次页面加载时,我们就可以直接从本地存储中读取已读取过的图片。

HTML部分

<figure>
 <img id="elephant" src="about:blank" alt="A close up of an elephant">
 <noscript>
 <img src="elephant.png" alt="A close up of an elephant">
 </noscript>
 <figcaption>A mighty big elephant, and mighty close too!</figcaption>
</figure>

JavaScript部分

//在本地存储中保存图片
var storageFiles = JSON.parse(localStorage.getItem("storageFiles")) || {},
 elephant = document.getElementById("elephant"),
 storageFilesDate = storageFiles.date,
 date = new Date(),
 todaysDate = (date.getMonth() + 1).toString() + date.getDate().toString();
// 检查数据,如果不存在或者数据过期,则创建一个本地存储
if (typeof storageFilesDate === "undefined" || storageFilesDate < todaysDate) {
 // 图片加载完成后执行
 elephant.addEventListener("load", function () {
 var imgCanvas = document.createElement("canvas"),
 imgContext = imgCanvas.getContext("2d");
// 确保canvas尺寸和图片一致
 imgCanvas.width = elephant.width;
 imgCanvas.height = elephant.height;
// 在canvas中绘制图片
 imgContext.drawImage(elephant, 0, 0, elephant.width, elephant.height);
// 将图片保存为Data URI
 storageFiles.elephant = imgCanvas.toDataURL("image/png");

 storageFiles.date = todaysDate;
// 将JSON保存到本地存储中
 try {
 localStorage.setItem("storageFiles", JSON.stringify(storageFiles));
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 }, false);
// 设置图片
 elephant.setAttribute("src", "elephant.png");
}
else {
 // Use image from localStorage
 elephant.setAttribute("src", storageFiles.elephant);
}

注意:此处需要注意本地存储的容量,最好使用try…catch来控制异常。

保存任意格式的文件

使用canvas将图片转换成Data URI并保存到本地存储中的方式非常好,但是如果我们希望能找到一个可以保存任意格式文件的方式。

那么,这个过程就显的比较有趣了,我们需要用到:

  • XMLHttpRequest Level 2
  • BlobBuilder(提供接口来构建Blob对象,Blob对象是BLOB (binary large object),二进制大对象,是一个可以存储二进制文件的容器。在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型。BLOB是一个大文件,典型的BLOB是一张图片或一个声音文件,由于它们的尺寸,必须使用特殊的方式来处理(例如:上传、下载或者存放到一个数据库)。)
  • FileReader

基本方法是:

  1. 用XMLHttpRequest请求文件,然后将响应头设置为”arraybuffer”。
  2. 将返回数据存放到BlobBuilder中
  3. 获取blob,也就是文件内容
  4. 使用FileReader对象读取文件并加载到文件中,最后保存到本地存储。
// 获取文件
var rhinoStorage = localStorage.getItem("rhino"),
 rhino = document.getElementById("rhino");
if (rhinoStorage) {
 //如果已经存在则直接重用已保存的数据
 rhino.setAttribute("src", rhinoStorage);
}
else {
 // 创建XHR, BlobBuilder 和FileReader 对象
 var xhr = new XMLHttpRequest(),
 blobBuilder = new (window.BlobBuilder || window.MozBlobBuilder || window.WebKitBlobBuilder || window.OBlobBuilder || window.msBlobBuilder),
 blob,
 fileReader = new FileReader();
 xhr.open("GET", "rhino.png", true);
 //将响应头类型设置为“arraybuffer”,也可以使用"blob",这样就不需要使用BlobBuilder来构建数据,但是"blob"的支持程度有限。
 xhr.responseType = "arraybuffer";
 xhr.addEventListener("load", function () {
 if (xhr.status === 200) {
 // 将响应数据放入blobBuilder中
 blobBuilder.append(xhr.response);
 // 用文件类型创建blob对象
 blob = blobBuilder.getBlob("image/png");
 // 由于Chrome不支持用addEventListener监听FileReader对象的事件,所以需要用onload
 fileReader.onload = function (evt) {
 // 用Data URI的格式读取文件内容
 var result = evt.target.result;
 // 将图片的src指向Data URI
 rhino.setAttribute("src", result);
 //保存到本地存储中
 try {
 localStorage.setItem("rhino", result);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 };
 // 以Data URI的形式加载blob
 fileReader.readAsDataURL(blob);
 }
 }, false);
 // 发送异步请求
 xhr.send();
}

使用“blob”作为响应头类型

在上面的例子中,我们使用的是“arraybuffer”作为响应头类型,然后使用BlobBuilder来创建可以由FileReader读取的数据。然而,”blob”作为响应头类型后,会直接返回一个blob对象,从而可以直接由FileReader读取。上面的例子可以改成这样:

// Getting a file through XMLHttpRequest as an arraybuffer and creating a Blob
var rhinoStorage = localStorage.getItem("rhino"),
 rhino = document.getElementById("rhino");
if (rhinoStorage) {
 // Reuse existing Data URL from localStorage
 rhino.setAttribute("src", rhinoStorage);
}
else {
 // Create XHR, BlobBuilder and FileReader objects
 var xhr = new XMLHttpRequest(),
 fileReader = new FileReader();
 xhr.open("GET", "rhino.png", true);
 // Set the responseType to arraybuffer. "blob" is an option too, rendering BlobBuilder unnecessary, but the support for "blob" is not widespread enough yet
 xhr.responseType = "blob";
 xhr.addEventListener("load", function () {
 if (xhr.status === 200) {
 // onload needed since Google Chrome doesn't support addEventListener for FileReader
 fileReader.onload = function (evt) {
 // Read out file contents as a Data URL
 var result = evt.target.result;
 // Set image src to Data URL
 rhino.setAttribute("src", result);
 // Store Data URL in localStorage
 try {
 localStorage.setItem("rhino", result);
 }
 catch (e) {
 console.log("Storage failed: " + e);
 }
 };
 // Load blob as Data URL
 fileReader.readAsDataURL(xhr.response);
 }
 }, false);
 // Send XHR
 xhr.send();
}

浏览器支持情况

  • 本地存储——大部分主流浏览器都支持(在国外=。=), including IE8.
  • 原生的JSON支持——支持情况和本地存储类似
  • canvas元素——大部分主流浏览器都支持,从IE9开始
  • XMLHttpRequest Level 2—— Firefox,Google Chrome, Safari 5+ 并计划在IE10和Opera 12中实现
  • BlobBuilder——Firefox ,Google Chrome,并计划在IE10中实现. Safari和Opera情况不明.
  • FileReader——Firefox ,Google Chrome,Opera 11.1之后的版本, 计划在IE10中实现. Safari情况不明.
  • responseType “blob”——只在Firefox中支持. Google Chrome即将支持,IE10计划支持.  Safari和Opera情况不明。

【译】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核心,介绍原型和对象的概念。

系列其他文章