Using jQuery plugins with npm

上周我们讨论了如何发布jQuery的插件到npm,快速而原始的方式。由于快速和原始,我们的意思是,你可以简单地发布你的模块到npm–没有任何修改–人们就可以使用它。但是一些插件需要做额外的工作,并添加一点点代码,使其可以更容易通过npm基础工具使用。

在这篇文章中,我们将向你展示如何使用快速而原始的方式发布jQuery插件…也就是说,插件无需使用package.json主导文件或者设置一个全局变量,不管他们是否直接在浏览器中使用或者被像Browserify这样的工具捆绑。

在接下来的文章中,我们将向你展示如何更新你的插件来使用npm特有的功能,这使得开发人员更容易使用你的模块(对于你,当你在项目中用你自己的插件上)。 我们还将向您展示如何使之与Node,CommonJS的模块系统结合工作,并充分利用模块化,使你的插件更易于维护,更容易。

阅读详情 -> The npm Blog — Using jQuery plugins with npm.

Categories: JavaScript

延长 XSS 生命期 – EtherDream – lib

XSS 的本质仍是一段脚本。和其他文档元素一样,页面关了一切都销毁。除非能将脚本蔓延到页面以外的地方,那样才能获得更长的生命力。

庆幸的是,从 DOM 诞生的那一天起,就已为我们准备了这个特殊的功能,让脚本拥有突破当前页面的能力。

下面开始我们的续命黑魔法。

反向注入

一个不合理的标准,往往会埋下各种隐患。

按理来说,只有通过脚本弹出的页面,才能拥有 opener 属性。然而事实上,通过超链接点开的页面居然也有。这为 XSS 打开了一扇大门 —— XSS 不仅可以操控当前页面,甚至还能传染给同源的父页面。

XSS 一旦感染到父页面里,战斗力就大幅提升了。

可以想象,只要看了一个带有 XSS 的帖子,即使立即关了,那么帖子列表页也会遭到感染。

更有趣的是,opener 这个属性不受同源策略限制。即使父页面不同源,但父页面的 opener 仍然可以访问。

我们可以顺着 opener.opener.opener… 一直往上试探,只要是和当前页面同源的,仍然能够进行操控 —— 尽管中间隔着其他不同源的页面。

网站的主页面显然比详细页更受用户的信任,停留的时间也会更长,因此攻击力可成倍的增加。

正向注入

如果说反向注入是苟且偷生的话,那么正向注入就是当家做主翻身的机会了。

尽管我们能够控制父页面,但从父页面点开的网页仍然不受操控。如果具有控制子页面的能力,那就更完美了。

不幸的是,我们无法控制超链接打开的新页面。唯一能够操控的新页面,那就是 window.open 的弹框页。幸运的是,在绝大多数浏览器上,它们看起来的效果是一样的。

因此,我们可以在用户的点击瞬间,屏蔽掉默认的超链接行为,用弹框页取而代之,即可把 XSS 注入到 window.open 返回的新页面里了。

类似的,通过子页面递归打开的新页面,同样也无法逃脱。于是子子孙孙尽在我们的掌控之中。

反向注入,让我们占据已有的地盘;正向注入,把我们的势力扩大蔓延出去。两者结合,即可占据半壁江山了。

值得注意的是,正向注入中有个细节问题。并非所有的超链接都是弹出型的(_blank),也有不少是在当前页面跳转的。若是想劫持的狠点,可以忽略这个问题;如果不想被细心的用户发现,那么可以判断下当前超链接以及<base>的 target 属性,决定是否劫持。

页面监督

上面提到,如果是在当前页面里跳转,那么还能继续感染吗?或者说,某个页面刷新之后,是否就丢失了?

答案是肯定的。如果我们不采取一些措施,任凭占据的地盘不断丢失,那么我们的势力范围就会越来越小,直到消亡。

相比进攻,防守则更为困难。我们不知何时会失去,因此必须定时去检查。

一旦发现对方已摆脱我们的控制,那么必须立即重新注入,以恢复我们的势力。

对于新页面的 XSS 来说,当然是注入的越早越好。越前面拥有越高的优先级,甚至可以拦截页面的正常业务功能。

为了能尽快获知页面刷新、跳转等行为,我们还可跟踪 unload 事件,在页面即将丢失的瞬间,将消息通知出去,让对方尽快来拯救自己。

当然,并非任何情况都能收回的。如果跳转到了不同源的页面,那显然是无能为力了 —— 不过,就此而放弃它吗?回答是:决不妥协!

尽管页面已经和我们分道扬镳了,但所在的窗体仍然被我们掌控。我们可以跳转、关闭它,甚至还有可能出现奇迹:只要页面跳转回我们的站点,又可被我们所收复!

互相联结

不难发现,只要还有一个页面存在,就有可能收回曾经被占领的地盘。因此,我们要将可控的页面都联结起来,每个页面都知晓所有成员。

当有新成员加入时,通知给大家,记录在各自的页面里。

这样即使其中一个页面意外关闭了,也不会丢失重要的信息 —— 信息已被分布储存在各个页面里了。

因此,页面开的越多,相互联结就越牢固。

如果只剩最后一个页面,那么一旦刷新之后就没人来拯救了,于是就会消亡。

所以,把超链接都变成新页面中打开,还是有很大的优势的。

降域尝试

一些网站为了方便通信,将 document.domain 降到根域。例如支付宝网站的绝大部分页面,都是 alipay.com。这样原本不同源的子站,这时也能够相互操控了。

因此,遇到不同源的页面,可以尝试降低自身的域,再次发起操作,或许就能成功注入了。

表单劫持

之前说到正向注入,是通过劫持超链接点击实现的。事实上,除了超链接外,还有个进入新页面的方式,那就是表单提交。

相比超链接,表单显得棘手一些。我们不仅得打开一个新页面,还要把表单里的数据也提交上去。如果把整个表单的元素克隆到新页面提交,一些数据又会丢失。

不过,仔细研究一下表单元素,会发现有一个非常简单的方法:原来 window.open 第二个参数可以赋予新窗口一个 name,然后将 name 赋予表单的 target 属性,即可在我们创建的新窗口里提交。这样就可以把 XSS 注入进去了。

框架注入

不同页面之间可以正反注入。同个页面中,也许存在多个框架页,因此还可以尝试框架页之间的上下注入。

也许,XSS 位于页面中某个小框架。如果只局限于自身页面,那么是毫无发展空间的。因此得跳出圈子,向更广阔的 parent 页面注入。

类似的,如果主页面仅仅是个外壳,实际内容运行在某个框架里,那么得注入到子框架中,才能获取更有意义的信息。

同样,我们还可以将上下注入结合,即可让 XSS 从某个框架页里破壳而出,感染到所有的框架页里。

后记

尽管这些特征从 DOM 诞生起就已存在了,不过要写出一个完善的脚本并不容易。直到如今的 IE 11,不同窗体间的操作,仍有各种奇怪问题,更不用说那些非主流 IE 了。

不过好在除 IE 外的其他主流浏览器,都能很好的运行。下面分享一个 Demo,其中实现了上述部分功能:

http://www.etherdream.com/FunnyScript/XSSGhost/

当我们顺着超链接往前点,一旦进入有 XSS 的页面,先前的父页面都遭到感染。更严重的是,被感染的页面打开的子页面,也都无一幸免。即使刷新,也会被其他页面监控到,从而立即恢复。

正如幽灵鬼魂一般挥之不去。

阅读详情 -> 延长 XSS 生命期 – EtherDream – lib.

Categories: 前端资讯

Riot.js — 1Kb 大小的 JavaScript 的 MVP 框架

一个可以构建大型网络应用并令人难以置信快和强大但轻量级的客户端框架

Riot.js是一个客户端模型-视图-呈现(MVP)框架并且它非常轻量级甚至小于1kb.尽管他的大小令人难以置信,所有它能构建的有如下:一个模板引擎,路由,甚至是库和一个严格的并具有组织的MVP模式。当模型数据变化时视图也会自动更新。

Riot.js快而且简单-事实上,是完全不同的规模-而且用它的应用也很快简单。真的。

让我详细解释吧。

最快

Riot.js是目前存在的JavaScript模板引擎中速度最快的。它比Resig的“微模板”快5倍,比Underscore快7倍。

在你应用中的模板影响着渲染时间。模板快可以让你的应用快。在大型应用中影响是很大的,特别是在非Webkit浏览器中。上述在火狐中用100K重复的测试可在Hogan花费1300ms而在Riot中耗时30ms。请在不同的浏览器中做做测试可以得到更好的结果。

最轻

下面是Riot和其它流行的客户端框架文件大小比较:

很明显,Riot的是最小的,而且框架的大小会影响到以下几个方面:

  1. 容易学习。需要看更少的书和指导。这种不同点影响是巨大的:想想看3个API和300个API的区别。你需要更多的时间来构建应用。这基本上是给大团队用的。错误的决定会导致整个工程失败。
  2. 更少专有语法,更多是直接用JavaScript。用通用的编程技巧代替一些框架自己的规则是很重要的。
  3. 更少的问题。更不容易受到攻击和发现弱点。所有的错误都是用户产生的问题点。令人沮丧的是有时候你的应用失败很有可能是因为框架引起的。
  4. 易嵌入。在哪里用Riot.js都不会让人觉得多余。框架不能比应用本身的代码量大。
  5. 更快。根据Google的报告,每增加1kb的JavaScript,浏览器就多花费1ms的时间来解析(1)
  6. 更节约。对于Amazon.com,每增加100ms的加载JavaScript时间会让其税收增加1%(2)。在2012年,Amazon 1%的税收总共$610.9百万。

如果你的工具是其它消费应用而用的,那么其大小可能更加重要。

最强大

这是一个令人震惊的部分:一个仅有1Kb的程序库创建一个待办事项的MVC应用所需的代码量:

应用所需代码量的图片传达出了一个好框架的能力。一个框架的全部意义在于解决公共问题,这样用户就不必重新发明轮子。写尽量少的代码但能实现更多功能。

代码量的大小取决于多种因素,比如编程风格,所以老实说你的目标不应该是代码量小,而是简单。

简洁是Riot真正闪亮的地方。

MVP设计模式

Riot使用Model-View-Presenter (MVP)设计模式来组织代码,这样它能够更模块化、更具可测试性且易于理解。

Model View Presenter

正如在MVC(模型-视图-控制器)或MVVM(模型-视图-视图模型)模式里,其目的是从应用程序的视图中分离逻辑,但MVP更简单。让我们把它和MVC比较一下:

Model View Controller

MVC模式更复杂。许多箭头围成一个圈。控制器的角色不明确,这种模式可以以许多不同的方式解释。事实上,这是造成有太多该模式下客户端框架的根本原因。

MVP则相反,没有太多的解释空间,歧义少。每部分的作用是明确的。它适合大大大小小的工程,是单元测试的最佳模式。

让我们看一下MVP在Riot中是如何工作的。

Model

Riot的models定义了你的应用。 它是你的商业逻辑对外提供的一个深思熟虑后的API。一个完全隔离的、可在浏览器和服务器端(node.js)运行的可测试单元。下面是一个待办事项应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function Todo(store) {
 
   var self = this,
      items = [];
 
   self.add = function(label) {
      var item = { ... };
      items.push(item);
 
      // notify others
      self.emit("add", item);
   }
 
   self.remove = function(id) {
      delete items[id];
      self.emit("remove", item);
   }
 
   // + other public methods ...
 
   // Enable MVP pattern (this is the secret for everything)
   $.observable(self);
 
   // save state
   self.on("add remove edit"function() {
      store.put(items);
   })
 
}

这个Model是一个传统的JavaScript对象(POJO),没有框架风格。在MVC的术语中,这是一个域模型而不仅仅是数据访问层。

你有权使用prototype对象或对象构造器{}。以上只是我个人使用的JavaScript风格。

当设计一个model时,保持思路明确很重要。你需要的最后一件事是用一个框架来聚焦你的应用逻辑。JavaScript本身就具有巨大的表现力。

observable

观察者是从你应用中分离模型的关键:

1
$.observable(object);

当一些重要的事件发生后,上面的函数调用给定的对象并通知给其它对象。视图将会重新绘制或者API使用者也可以用监听改变事件来予以扩展。

观察者是将你的应用分离为可维护的组件的关键。它是一个典型的设计模式来将模型从视图中分离出来。

“构建大应用的秘籍就是永远不要一下子做成大应用。要将你的应用分割为小的模块”。Justin Meyer,JavaScriptMVC的作者 
“如果将不同组件绑的太紧,可重用性将降低,而且很难在改变一些组件时而不影响其它的组件。”–Rebecca Murphey,jQuery Fundamentals作者

在客户端框架中一个好的事件库是分离各组件的很重要的特性。这就是Riot受关注的原因。

观察者可以对给定对象添加如下方法:

  • emit(event_name, args…)— 带有可选参数并能触发一个命名事件
  • on(event_name, fn)— 当一个特定事件发生时调用给定的函数
  • one(event_name, fn)— 当一个特定事件触发时调用给定函数一次,额外的事件将没有作用
  • off(event_name)— 停止监听指定的事件

Riot 事件是基于jQuery事件的,所以它有很强大的特性,如命名空间,一次可以监听多个事件。

最重要的不同是它没有事件对象,因为它在DOM之外就没有相关的事件了。你也能很优雅的发送和接收参数而不用数组来完成。

1
2
3
4
5
6
7
// 发送事件
obj.emit("my-event""1st argument""2nd arg", ... argN);
 
// 接收事件
obj.on("my-event"function(arg1, arg2, ... argN) {
 
});

观察者模式产生于1988年的Smlltalk语言,自从那时起它就被用来构建用户接口了。它是一个设计模式,而不是框架。事件和监听器的本质是被分离关注点的优良代码。这也应该是你理念的核心部分。

换句话说:

你不需要用框架来写一个模块级的客户端应用。

视图

视图就是你的应用中可见的那部分。譬如文本,图片,表格,按钮,链接等等,html代码和css样式文件。

Riot视图尽可能的轻。没有了那些视图中本不该有的条件语句,循环,或者数据绑定,自定义的属性或元素。就是你对所有网页设计师期望的那种视图。

只有”模版”——也就是html代码块能在运行的时候插到视图里面。这些模版包含那些可以用Riot模版驱动很快用数据替换的变量。

下面是一段 待处理的MVC入口:

1
2
3
4
5
6
7
8
<li id="{id}">
   <div class="view">
      <input class="toggle" type="checkbox">
      <label>{name}</label>
      <button class="destroy"/>
   </div>
   <input class="edit" value="{name}">
</li>

这种”不合逻辑的html“ 没有弱点或可测试的界面。 它通过了w3c校验器并且运行更快。

实际逻辑在presenter里面.

呈现器

呈现器监听视图上的事件(点击,滚动,按键,返回按钮等等)以及模型(添加,删除或修改的东西)上的事件,当模型更新后视图和模型都会相应的变化。这种”中间人”显示的定义了用户是怎么使用接口的行为。下面是备忘录应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$(function() {
 
   / 1. 初始化 /
 
   // 创建一个模型对象
   var model = new Todo(),
 
      // 抓出这个视图HTML root实例
      root = $("#todo-list"),
 
      // 单个备忘录 HTML 模板
      template = $("#todo-tmpl").html(),
 
   / 2. 监听用户事件 /
 
   // 点击"清除完毕"
   $("#clear-completed").click(function() {
      todo.remove("completed");
   })
 
   //点击"切换所有"
   $("#toggle-all").click(function() {
      todo.toggle(filter);
   })
 
   // ...
 
   / 3. 监听模型事件 /
 
   // 一个删除入口
   todo.on("remove"function(items) {
      $.each(items, function() {
         $(this.id).remove()
      })
 
   // 一个编辑入口
   }).on("edit"function(item) {
      var el = $(item.id);
      el.removeClass("editing");
      $("label, .edit", el).text(item.name).val(item.name);
   })
 
   // ...
 
})

在你的应用中能决定每一个不同呈现器的感觉非常自由。这是基于UI(边栏,账目,头位置…)上的规则或是基于功能(登陆,添加,创建,删除…)。

呈现器可以用jQuery来使用所有的用户接口逻辑。

jQuery

jQuery绝对是最好的操作DOM的工具,而且它也能很好的创建呈现器。jQuery API设计的很漂亮而且所有的库都非常有用。目前在56%的网站中都有它的身影,92%的网站中用的JavaScript库都被人熟知。Riot将凭借它们引以为豪。

说用jQuery可以让代码更糟的错误观点让人失望。没有什么可以超越真理。人们认为代码很乱是因为代码将模型和视图混合了。为了防止此事件发生,通过观察可以简单的分离模型代码和视图代码。你不需要框架的。

用jQuery构建MVP模式,你所需要做的就是用接口而已。

最糟糕的事情就是因为错误的原因而丢弃jQuery。它可是高水平的生产高雅和具有可读性代码的API。其表达式语法简明而且只需要很少的键盘输入。它拥有很大的社区、大量的文档以及它能在所有浏览器上工作。用jQuery可以让你感受到自由而不是害怕。

目前构建数据的框架是在HTML层次来减少混乱代码的。突然间onclick属性隐藏在后面了(我惊讶的看着你)。编程书这样告诉我们,要将事物分离开来,在web开发中的意思如下:

  1. JavaScript要在CSS之外
  2. CSS要在JavaScript之外
  3. JavaScript要在HTML之外
  4. HTML要在JavaScript之外

Riot是简洁的方法,它不会让你在HTML视图中参杂任何逻辑。

URL的返回按钮

另一个让人困惑的来源是返回按钮。按照一般的约定,你需要一个叫“路由器”的工具。

其实这是不对的。

点击返回按钮或者改变URL是一个你可以监听的全局窗口事件。它应该和按下一个键盘按键事件一样。目前的路由器是一个为很基础功能而产生的巨大解决方案。没有必要将工具和处理URL事件相分离,这就像没有必要将工具和处理按键事件分离一样。

上面的路由器产生了混淆是因为它不符合典型的MVC模式。在之前的网络时代,传统的MVC应用没有URL。

Riot只有一个$.route函数来处理应用的状态和返回按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 点击导航元素
$("#filters a").click(function(e) {
 
   // 跳过链接默认行为
   e.preventDefault();
 
   //改变URL哈希部分
   $.route($(this).attr("href"));
 
})
 
//监听URL改变事件
$.route(function(hash) {
   /*
      待办事件列表处理:
 
      1. 清除列表并添加新项
      2. 高亮导航上激活的元素
      3. 更新视图计数器
   */
})

你只需要一个函数。我把它称作人们所期待的“路由”。它的作用如“url”或“go”。在url参数的基础上,你可以有一个或着多个URL监听器来完成其功能。

如果你的应用严重依靠不同哈希参数,你可能需要一个全面的路由器,但在大多数应用中是不需要的。

这就是Riot!

目前客户端框架都存在以下错误的假设:

  1. 存在一大块常见的问题
  2. MVC(或MVP)需要一个框架
  3. jQuery让代码更加混乱

所有这些陈述都是相当的错误。

目前的框架都会劝说大家相信错误的信条,即在没有易懂的、科学的分析下可以创建好的网站和非常精致的营销。为了解决假设的问题而会产生新的问题。很不幸,大量的通信在处理不相干的问题点。

Riot就是反对现状!

目前的应用可以更加的快、简单和小巧。它们可以让更少的人维护,需要更少的知识和出现更少的错误。

模型应该是普通的JavaScript,视图应该是普通的HTML。它们两都应该独立的发展而互不影响。框架没有必要负担开发和命令事情如何发展。

没有“Backbone的方式”、“Angular 方式”或是“Ember的方式”。框架一直都是经典的编程技巧。

Riot是对平凡的JavaScript和jQuery的一种宣言。

接下来呢?

我 之前的一个博客主题是有关Riot.js和无框架化的JavaScript。我想要用无框架结构来控制应用。事实上,只需要3个函数即可。

我的下一篇博客主题则更深层次的讨论了用Riot构建现代化的JavaScript应用。你可以学习到Riot怎么处理测试,持久化,应用辅助程序,资源分享,以及文件操作。

最后,官方的Riot.js即将出发行版,并且它有为浏览器和nod.js发布了文档和Moot论坛。关注我们的 Facebook页面Twitter公告

浏览源码

Riot.js 
Riot的MVC备忘录 
模板性能测试 

阅读详情 -> Riot.js — 1Kb 大小的 JavaScript 的 MVP 框架 – 技术翻译 – 开源中国社区.

Categories: JavaScript, 项目精选

全栈工程师的武器——MEAN

JavaScript自1995年发布以来,走过了漫长的道路。已经有了几个主要版本的ECMAScript规范,单页Web应用程序也慢慢兴起,还有支持客户端的JavaScript框架。作为一个被绝大多数浏览器支持前台脚本语言,它对浏览器的创新做出了很大的贡献。JavaScript许多很有用的特点(它是无阻塞是,它是事件驱动的,很多程序员熟悉它)可以在浏览器之外的环境中加以利用。这推动了JavaScript社区新一轮的创新,让JavaScript能在服务器和数据库中运行。

一下子,JavaScript工程师认为自己不只是“Web前端开发者”了。如果你对语言很熟悉,就可以自己搭建服务器和数据库。除了交互设计,开发者可以只用JavaScript就能做出一个完整的web应用,这在JavaScript史上是第一次。这种趋势被称为“全栈的JavaScript”或“纯JavaScript的解决方案”。它包含了JavaScript的四种流行技术:MongoDBExpressAngularJS、和 Node.js。这四种技术越来越流行,它们合称为MEAN Stack

clipboard.png

MongoDB

MongoDB也就是常说的NoSQL数据库。可以认为它是文档结构的数据库,而不是由行、列、表组成的数据库。基本的用法是存储JSON数据,这很适合JavaScript程序。它是非关系型、非事务要求严格的系统,所以它很快、可扩展性和易用性强。

Express

Express是一个小型的web服务器框架,能使Node开发的web服务器更易开发和维护。Express有很高的定制性,它能简单快速地使用路由请求、cookie管理、一个静态文件服务器和很多企业级网络服务器所需要HTTP模块。

AngularJS

Angular是一个功能丰富的客户端MVC JavaScript框架。它可用于开发复杂的单页Web应用程序。它具有内置的双向数据绑定功能和它自己的基于HTML的模板。它有个特性称为“directives”,允许开发者给HTML拓展新属性、新元素。Angular也是高度可测试的,这是很重要的。Angular为开发客户端提供了公约和最佳解决方案。

Node

Node基于JavaScript,用于建立服务器和网络应用。它提供所有JavaScript的特性,并外加文件操作、网络I/O的功能。它使用谷歌的V8引擎(与谷歌浏览器中使用的相同)来执行JavaScript。它拥有很活跃的开发者社区和Node模块生态圈(Express是其中一个模块)。虽有JavaScript有其他实现服务器端的方法,但Node是目前个人级和企业级最好的开发方案。

如果你是JavaScript的粉丝,你应该会对只用JavaScript就能开发一个完整的web应用感到兴奋。MEAN Stack是一个很热门的话题,网上有很多关于学习它们的讨论,可以去尝试学习一下。

英文原文 Full Stack JavaScript Development With MEAN

阅读详情 -> 全栈工程师的武器——MEAN – SegmentFault.

Categories: 前端资讯

不依赖jQuery也能做好动画

在开发者社区中有种错误的观念——认为在web中,CSS动画是唯一高性能的动画方式。这使很多开发者放弃基于JavaScript的动画。所以导致——(1)强制使用大量样式表来完成复杂的UI交互,(2)不能很好地支持IE8、9,(3)放弃只有JS才能完成的完美物理运动效果。

事实证明:基于JavaScript的动画和基于CSS的动画一样快,甚至有时更快。CSS动画通常是和非常慢的jQuery$.animate()比较。但是,JavaScript库如果能避免像jQuery那样大量的DOM操作,性能就会非常高。这些库能比jQuery快20倍。

所以,让我们来打破一些神话,通过一些动画实例,并在此过程中提高自己的设计技巧。

为什么是JavaScript

CSS动画能方便地实现渐变效果,也不需要额外地加载库。但是它很难支持复杂的效果。代码不易管理、特性不能满足需求而导致失败。

最终CSS动画无法完成要求。但JavaScript和大多编程语言一样,支持大量的逻辑控制。JavaScript动画引擎用事实证明了这点,这里有些小技巧可以参考:

如果你对性能感兴趣,可以看看Julian Shapiro 的 “CSS vs. JS Animation: Which Is Faster?” 和
Jack Doyle 的 “Myth Busting: CSS Animations vs. JavaScript” 。性能的演示可参考 Velocity 的 “performance pane” 和 GSAP 的 “Library Speed Comparison

Velocity 和 GSAP

两个最流行的JavaScript动画库是Velocity.js 和 GSAP。它们都不依赖于jQuery,使得性能提高。

假如你的网站已经用了 jQuery,那就可以像使用 jQuery 一样使用 Velocity 和 GSAP。例如: $element.animate({ opacity: 0.5 }); 就可以写成 $element.velocity({ opacity: 0.5 })

如果你没有使用 jQuery 也能使用这两个库。但是不能像绑定一系列动画到JQ元素上那样,要把元素传进动画函数里,例如:

/* Working without jQuery */

Velocity(element, { opacity: 0.5 }, 1000); // Velocity

TweenMax.to(element, 1, { opacity: 0.5 }); // GSAP

如上所示,在没有使用JQ的情况下,Velocity 的用法和JQ一样,把目标元素写到第一个参数位置,再把其他参数依次往右写。

GSAP与此不同,采用的是面向对象的API设计,和静态方法一样方便。所以你可以完全掌控整个动画。

在这两种情况下,你不再是使用一个jQuery的动画元素对象,而是一个原始的DOM节点。你可以通过document.getElementByIDdocument.getElementsByTagName、 document.getElementsByClassName 和document.querySelectorAll(和jQuery的选择器很类似)。

不使用jQuery

(注:如果你要学习基本的jQuery $.animate()的用法,可以参考Velocity文档开始的几段)

querySelectorAll是摆脱jQuery的好武器

document.querySelectorAll("body"); // 获取body元素
document.querySelectorAll(".squares"); // 获取所有包含square类的元素
document.querySelectorAll("div"); // 获取所有div
document.querySelectorAll("#main"); // "main"获取含有id为main的元素
document.querySelectorAll("#main div"); // 获取#main里的div

如上面所示,你可以轻松地把CSS选择器传给querySelectorAll,它会返回一个包含所有匹配元素的数组。所以,你可以这样做:

/* 获取所有div */
var divs = document.querySelectorAll("div");

/* 给所有div加上动画 */
Velocity(divs, { opacity: 0.5 }, 1000); // Velocity
TweenMax.to(divs, 1, { opacity: 0.5 }); // GSAP

我们不再将元素绑定为jQuery对象,那如何一个接一个地实现动画呢?

$element // jQuery 对象
    .velocity({ opacity: 0.5 }, 1000)
    .velocity({ opacity: 1 }, 1000);

在Velocity里,你只需一个接一个调用动画:

Velocity(element, { opacity: 0.5 }, 1000);
Velocity(element, { opacity: 1 }, 1000);

这种方式没有性能缺陷。要把做动画的目标元素用变量存储起来使用,而不是每次都用querySelectorAll查找一次。

(提示:使用Velocity的包,你可以创建自己的多操作的动画,并给他们取名字,你就可以像Velocity的第一参数一样操作它。点击这里查看更多。)

这种one-Velocity-call-at-a-time的方式有个好处:如果你使用promises,那么Velocity每次调用后会返回一个可使用的promise对象,这非常有用。你可以通过Jake Archibald的文章了解更多。

GSAP面对对象的方式允许把动画排入时间线,方便调度和同步控制。这样就不局限于一个接一个的链接动画。你可以嵌套时间表、使动画重叠等。

var tl = new TimelineMax();
/* GSAP默认使用tweens链,但你可以在时间线上指定精确的插入点,包括相对偏移 */
tl
  .to(element, 1, { opacity: 0.5 })
  .to(element, 1, { opacity: 1 });

英文原文:Animating Without jQuery

阅读详情 -> 不依赖jQuery也能做好动画 – SegmentFault.

Categories: JavaScript, 前端资讯

JavaScript 错误以及如何修复 | Alon’s Blog

JavaScript 调试是一场噩梦:首先给出的错误非常难以理解,其次给出的行号不总有帮助。有个查找错误含义,及修复措施的列表,是不是很有用?

以下是奇怪的 JavaScript 错误列表。同样的错误,不同的浏览器会给出不同的消息,因此有一些不同的例子。

如何读懂错误?

首先,让我们快速看下错误信息的结构。理解结构有助于理解错误,如果遇到列表之外的错误会减少麻烦。

Chrome 中典型的错误像这样:

1
Uncaught TypeError: undefined is not a function

错误的结构如下:

  1. Uncaught TypeError:这部分信息通常不是很有用。Uncaught 表示错误没有被 catch 语句捕获,TypeError 是错误的名字。

  2. undefined is not a function: 这部分信息,你必须逐字阅读。比如这里表示代码尝试使用 undefined,把它当做一个函数。

其它基于 webkit 的浏览器,比如 Safari ,给出的错误格式跟 Chrome 很类似。Firefox 也类似,但是不总包含第一部分,最新版本的 IE 也给出比 Chrome 简单的错误 – 但是在这里,简单并不总代表好。

以下是真正的错误。

Uncaught TypeError: undefined is not a function

相关错误:number is not a function, object is not a function, string is not a function, Unhandled Error: ‘foo’ is not a function, Function Expected

当尝试调用一个像方法的值时,这个值并不是一个方法。比如:

1
2
var foo = undefined;
foo();

如果你尝试调用一个对象的方法时,你输错了名字,这个典型的错误很容易发生。

1
var x = document.getElementByID('foo');

由于对象的属性不存在,默认是 undefined ,以上代码将导致这个错误。

尝试调用一个像方法的数字,“number is not a function” 错误出现。

如何修复错误:确保方法名正确。这个错误的行号将指出正确的位置。

Uncaught ReferenceError: Invalid left-hand side in assignment

相关错误:Uncaught exception: ReferenceError: Cannot assign to ‘functionCall()’, Uncaught exception: ReferenceError: Cannot assign to ‘this’

尝试给不能赋值的东西赋值,引起这个错误。

这个错误最常见的例子出现在 if 语句使用:

1
if(doSomething() = 'somevalue')

此例中,程序员意外地使用了单个等号,而不是双等号。“left-hand side in assignment” 提及了等号左手边的部分,因此你可以看到以上例子,左手边包含不能赋值的东西,导致这个错误。

如何修复错误:确保没有给函数结果赋值,或者给 this 关键字赋值。

Uncaught TypeError: Converting circular structure to JSON

相关错误:Uncaught exception: TypeError: JSON.stringify: Not an acyclic Object, TypeError: cyclic object value, Circular reference in value argument not supported

把循环引用的对象,传给 JSON.stringify 总会引起错误。

1
2
3
4
var a = { };
var b = { a: a };
a.b = b;
JSON.stringify(a);

由于以上的 a 和 b 循环引用彼此,结果对象无法转换成 JSON。

如何修复错误:移除任何想转换成 JSON 的对象中的循环引用。

Unexpected token ;

相关错误:Expected ), missing ) after argument list

JavaScript 解释器预期的东西没有被包含。不匹配的圆括号或方括号通常引起这个错误。

错误信息可能有所不同 – “Unexpected token ]” 或者 “Expected {” 等。

如何修复错误:有时错误出现的行号并不准确,因此很难修复。

  • [ ] { } ( ) 这几个符号不配对常常导致出错。检查所有的圆括号和方括号是否配对。行号指出的不仅是问题字符。
  • Unexpected / 跟正则表达式有关。此时行号通常是正确的。
  • Unexpected ; 对象或者数组字面量里面有个 ; 通常引起这个错误,或者函数调用的参数列表里有个分号。此时的行号通常也是正确的。

Uncaught SyntaxError: Unexpected token ILLEGAL

相关错误:Unterminated String Literal, Invalid Line Terminator

一个字符串字面量少了结尾的引号。

如何修复错误:确保所有的字符串都有结束的引号。

Uncaught TypeError: Cannot read property ‘foo’ of null, Uncaught TypeError: Cannot read property ‘foo’ of undefined

相关错误:TypeError: someVal is null, Unable to get property ‘foo’ of undefined or null reference

尝试读取 null 或者 undefined ,把它当成了对象。例如:

1
2
var someVal = null;
console.log(someVal.foo);

如何修复错误:通常由于拼写错误导致。检查错误指出的行号附近使用的变量名是否正确。

Uncaught TypeError: Cannot set property ‘foo’ of null, Uncaught TypeError: Cannot set property ‘foo’ of undefined

相关错误:TypeError: someVal is undefined, Unable to set property ‘foo’ of undefined or null reference

尝试写入 null 或者 undefined ,把它当成了一个对象。例如:

1
2
var someVal = null;
someVal.foo = 1;

如何修复错误:也是由于拼写错误所致。检查错误指出的行号附近的变量名。

Uncaught RangeError: Maximum call stack size exceeded

相关错误:Related errors: Uncaught exception: RangeError: Maximum recursion depth exceeded, too much recursion, Stack overflow

通常由程序逻辑 bug 引起,导致函数的无限递归调用。

如何修复错误:检查递归函数中可能导致无限循环 的 bug 。

Uncaught URIError: URI malformed

相关错误:URIError: malformed URI sequence

无效的 decodeURIComponent 调用所致。

如何修复错误:按照错误指出的行号,检查 decodeURIComponent 调用,它是正确的。

XMLHttpRequest cannot load http://some/url/. No ‘Access-Control-Allow-Origin’ header is present on the requested resource

相关错误:Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at
http://some/url/

错误肯定是使用 XMLHttpRequest 引起的。

如何修复:确保请求的 URL 是正确的,它遵循同源策略 。最好的方法是从代码中找到错误信息指出的 URL 。

InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable

相关错误:InvalidStateError, DOMException code 11

代码调用的方法在当前状态无法调用。通常由 XMLHttpRequest 引起,在方法准备完毕之前调用它会引起错误。

1
2
var xhr = new XMLHttpRequest();
xhr.setRequestHeader('Some-Header', 'val');

这时就会出错,因为 setRequestHeader 方法只能在 xhr.open 方法之后调用。

如何修复:查看错误指出的行号,确保代码运行的时机正确,或者在它(例如 xhr.open
)之前添加了不必要的调用

结论

我看过不少无用的 JavaScript 错误,比如 PHP 中声名狼藉的异常 Expected T_PAAMAYIM_NEKUDOTAYIM 。抛出更熟悉的错误才更有意义。现代浏览器不再抛出完全无用的错误,才会更有帮助。

你看到的最令人困惑的错误是什么?在评论中分享一下呗。

阅读详情 -> JavaScript 错误以及如何修复 | Alon’s Blog.

Categories: JavaScript

谈谈 React Native – 唐巧的技术博客

前言

几天前,Facebook 在 React.js Conf 2015 大会上推出了 React Native(视频链接)。我发了一条微博(地址),结果引来了 100 多次转发。为什么 React Native 会引来如此多的关注呢?我在这里谈谈我对 React Native 的理解。

一个新框架的出现总是为了解决现有的一些问题,那么对于现在的移动开发者来说,到底有哪些问题 React Native 能涉及呢?

人才稀缺的问题

首先的问题是:移动开发人才的稀缺。看看那些培训班出来的人吧,经过 3 个月的培训就可以拿到 8K 甚至上万的工作。在北京稍微有点工作经验的 iOS 开发,就要求 2 万一个月的工资。这说明当前移动互联网和创业的火热,已经让业界没有足够的开发人才了,所以大家都用涨工资来抢人才。而由于跨平台的框架(例如 PhoneGap,RubyMotion)都还是不太靠谱,所以对于稍微大一些的公司,都会选择针对 iOS 和 Android 平台分别做不同的定制开发。而 JavaScript 显然是一个群众基础更广的语言,这将使得相关人才更容易获得,同时由于后面提到的代码复用问题得到解决,也能节省一部分开发人员。

代码复用的问题

React Native 虽然强调自己不是 “Write once, run anywhere” 的框架,但是它至少能像 Google 的 j2objc 那样,在 Model 层实现复用。那些底层的、与界面无关的逻辑,相信 React Native 也可以实现复用。这样,虽然 UI 层的工作还是需要做 iOS 和 Android 两个平台,但如果抽象得好,Logic 和 Model 层的复用不但可以让代码复用,更可能实现底层的逻辑的单元测试。这样移动端的代码质量将更加可靠。

其实 React Native 宣传的 “Learning once, write anywhere” 本身也是一种复用的思想。大家厌烦了各种各样的编程语言,如果有一种语言真的能够统一移动开发领域,对于所有人都是好事。

UI 排版的问题

我自己一直不喜欢苹果新推出的 AutoLayout 那套解决方案,其实 HTML 和 CSS 在界面布局和呈现上深耕多年,Android 也是借鉴的 HTML 的那套方案,苹果完全可以也走这套方案的。但是苹果选择发明了一个 Constraint 的东西来实现排版。在企业的开发中,其实大家很少使用 Xib 的,而手写 Constraint 其实是非常痛苦的。所以出现了 Masonry 一类的开源框架来解决这类同行的痛苦。

我一直在寻找使用类似 HTML + CSS 的排版,但是使用原生控件渲染的框架。其实之前 BeeFramework 就做了这方面的事情。所以我还专门代表 InfoQ 对他进行过采访。BeeFramework 虽然开源多年,而且有 2000 多的 star 数,但是受限于它自身的影响力以及框架的复杂性,一直没有很大的成功。至少我不知道有什么大的公司采用。

这次 Facebook 的 React Native 做的事情相比 BeeFramework 更加激进。它不但采用了类似 HTML + CSS 的排版,还把语言也换成了 JavaScript,这下子改变可以称作巨大了。但是 Facebook 有它作为全球互联网企业的光环,相信会有不少开发者跟进采用 React Native。

不过也说回来,Facebook 开源的也不一定都好,比如 three20 就被 Facebook 放弃了,但是不可否认 three20 作为一个框架,在那个时期的特定价值。所以 React Native 即使没有成功,它也将人们关注的焦点放在了移动开发的效率上了。很可能会有越来越多相关的框架因此涌现出来。

MVVM

MVVM 在 Web 开发领域相当火热,而 iOS 领域的 ReactiveCocoa 虽然很火,但是还是非常小众。纠其原因,一方面是 ReactiveCocoa 带来的编程习惯上的改变实在太大,ReactiveCocoa 和 MVVM 的学习成本还是很高。另一方面是 ReactiveCocoa 在代码可读性、可维护性和协作上不太友好。

而 Web 开发领域对 MVVM 编程模式的接受程度就大不相同了,在 Web 开发中有相当多的被广泛使用的 MVVM 的框架,例如 AngularJS。相信 React Native 会推动 MVVM 应用在移动端的开发。

动态更新

终于说到最 “鸡冻人心” 的部分了。你受够了每次发新版本都要审核一个星期吗?苹果的审核团队在效率上的低下,使得我们这一群狠不得每天迭代更新一版的敏捷开发团队被迫每 2 周或 1 个月更新一次版本。很多团队上一个版本还没审核结束,下一个版本就做好了。

React Native 的语言是基于 JavaScript,这必然会使得代码可以从服务器端动态更新成为可能。到时候,每天更新不再是梦想。当然,代码的安全性将更一步受到挑战,如何有效保护核心代码的安全将是一个难题。

总结

不管怎么样,这确确实实是一个移动互联网的时代,我相信随着几年的发展,移动互联网的开发生态也会积累出越来越多宝贵的框架,以支撑出更加伟大的 App 出现。作为一个移动开发者,我很高兴能够成为这个时代的主角,用移动开发技术改变人们的生活。

愿大家珍惜这样的机会,玩得开心~

 

阅读详情 -> 谈谈 React Native – 唐巧的技术博客.

Categories: JavaScript, 前端资讯

详解this

this 虐我千百遍,看完此文效立见!不得不说,这篇文章的总结很地道很全面,适合收藏之用。
原文:all this

习惯了高级语言的你或许觉得JavaScript中的this跟Java这些面向对象语言相似,保存了实体属性的一些值。其实不然。将它视作幻影魔神比较恰当,手提一个装满未知符文的灵龛

以下内容我希望广大同行们能够了解。全是掏箱底的干货,其中大部分占用了我很多时间才掌握。

全局this

浏览器宿主的全局环境中,this指的是window对象。

<script type="text/javascript">
    console.log(this === window); //true
</script>

示例

浏览器中在全局环境下,使用var声明变量其实就是赋值给thiswindow

<script type="text/javascript">
    var foo = "bar";
    console.log(this.foo); //logs "bar"
    console.log(window.foo); //logs "bar"
</script>

示例

任何情况下,创建变量时没有使用var或者let(ECMAScript 6),也是在操作全局this

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();
    console.log(this.foo); //logs "foo"
</script>

示例

Node命令行(REPL)中,this是全局命名空间。可以通过global来访问。

> this
{ ArrayBuffer: [Function: ArrayBuffer],
  Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
  Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
  ...
> global === this
true

在Node环境里执行的JS脚本中,this其实是个空对象,有别于global

console.log(this);
console.log(this === global);
$ node test.js
{}
false

当尝试在Node中执行JS脚本时,脚本中全局作用域中的var并不会将变量赋值给全局this,这与在浏览器中是不一样的。

var foo = "bar";
console.log(this.foo);
$ node test.js
undefined

…但在命令行里进行求值却会赋值到this身上。

> var foo = "bar";
> this.foo
bar
> global.foo
bar

在Node里执行的脚本中,创建变量时没带varlet关键字,会赋值给全局的global但不是this(译注:上面已经提到thisglobal不是同一个对象,所以这里就不奇怪了)。

foo = "bar";
console.log(this.foo);
console.log(global.foo);
$ node test.js
undefined
bar

但在Node命令行里,就会赋值给两者了。

译注:简单来说,Node脚本中globalthis是区别对待的,而Node命令行中,两者可等效为同一对象。

函数或方法里的this

除了DOM的事件回调或者提供了执行上下文(后面会提到)的情况,函数正常被调用(不带new)时,里面的this指向的是全局作用域。

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();
    console.log(this.foo); //logs "foo"
</script>

示例

foo = "bar";

function testThis () {
  this.foo = "foo";
}

console.log(global.foo);
testThis();
console.log(global.foo);
$ node test.js
bar
foo

还有个例外,就是使用了"use strict";。此时thisundefined

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      "use strict";
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    testThis();  //Uncaught TypeError: Cannot set property 'foo' of undefined 
</script>

示例

当用调用函数时使用了new关键字,此刻this指代一个新的上下文,不再指向全局this

<script type="text/javascript">
    foo = "bar";

    function testThis() {
      this.foo = "foo";
    }

    console.log(this.foo); //logs "bar"
    new testThis();
    console.log(this.foo); //logs "bar"

    console.log(new testThis().foo); //logs "foo"
</script>

示例

通常我将这个新的上下文称作实例。

原型中的this

函数创建后其实以一个函数对象的形式存在着。既然是对象,则自动获得了一个叫做prototype的属性,可以自由地对这个属性进行赋值。当配合new关键字来调用一个函数创建实例后,此刻便能直接访问到原型身上的值。

function Thing() {
    console.log(this.foo);
}

Thing.prototype.foo = "bar";

var thing = new Thing(); //logs "bar"
console.log(thing.foo);  //logs "bar"

示例

当通过new的方式创建了多个实例后,他们会共用一个原型。比如,每个实例的this.foo都返回相同的值,直到this.foo被重写。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
    this.foo = newFoo;
}

var thing1 = new Thing();
var thing2 = new Thing();

thing1.logFoo(); //logs "bar"
thing2.logFoo(); //logs "bar"

thing1.setFoo("foo");
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "bar";

thing2.foo = "foobar";
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "foobar";

示例

在实例中,this是个特殊的对象,而this自身其实只是个关键字。你可以把this想象成在实例中获取原型值的一种途径,同时对this赋值又会覆盖原型上的值。完全可以将新增的值从原型中删除从而将原型还原为初始状态。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
    this.foo = newFoo;
}
Thing.prototype.deleteFoo = function () {
    delete this.foo;
}

var thing = new Thing();
thing.setFoo("foo");
thing.logFoo(); //logs "foo";
thing.deleteFoo();
thing.logFoo(); //logs "bar";
thing.foo = "foobar";
thing.logFoo(); //logs "foobar";
delete thing.foo;
thing.logFoo(); //logs "bar";

示例

…或者不通过实例,直接操作函数的原型。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    console.log(this.foo, Thing.prototype.foo);
}

var thing = new Thing();
thing.foo = "foo";
thing.logFoo(); //logs "foo bar";

示例

同一函数创建的所有实例均共享一个原型。如果你给原型赋值了一个数组,那么所有实例都能获取到这个数组。除非你在某个实例中对其进行了重写,实事上是进行了覆盖。

function Thing() {
}
Thing.prototype.things = [];


var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing2.things); //logs ["foo"]

示例

通常上面的做法是不正确的(译注:改变thing1的同时也影响了thing2)。如果你想每个实例互不影响,那么请在函数里创建这些值,而不是在原型上。

function Thing() {
    this.things = [];
}


var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing1.things); //logs ["foo"]
console.log(thing2.things); //logs []

示例

多个函数可以形成原型链,这样this便会在原型链上逐步往上找直到找到你想引用的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";

function Thing2() {
}
Thing2.prototype = new Thing1();


var thing = new Thing2();
console.log(thing.foo); //logs "bar"

示例

很多人便是利用这个特性在JS中模拟经典的对象继承。

注意原型链底层函数中对this的操作会覆盖上层的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";

function Thing2() {
    this.foo = "foo";
}
Thing2.prototype = new Thing1();

function Thing3() {
}
Thing3.prototype = new Thing2();


var thing = new Thing3();
console.log(thing.foo); //logs "foo"

示例

我习惯将赋值到原型上的函数称作方法。上面某些地方便使用了方法这样的字眼,比如logFoo方法。这些方法中的this同样具有在原型链上查找引用的魔力。通常将最初用来创建实例的函数称作构造函数。

原型链方法中的this是从实例中的this开始住上查找整个原型链的。也就是说,如果原型链中某个地方直接对this进行赋值覆盖了某个变量,那么我们拿到 的是覆盖后的值。

function Thing1() {
}
Thing1.prototype.foo = "bar";
Thing1.prototype.logFoo = function () {
    console.log(this.foo);
}

function Thing2() {
    this.foo = "foo";
}
Thing2.prototype = new Thing1();


var thing = new Thing2();
thing.logFoo(); //logs "foo";

示例

在JavaScript中,函数可以嵌套函数,也就是你可以在函数里面继续定义函数。但内层函数是通过闭包获取外层函数里定义的变量值的,而不是直接继承this

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    var info = "attempting to log this.foo:";
    function doIt() {
        console.log(info, this.foo);
    }
    doIt();
}


var thing = new Thing();
thing.logFoo();  //logs "attempting to log this.foo: undefined"

示例

上面示例中,doIt 函数中的this指代是全局作用域或者是undefined如果使用了"use strict";声明的话。对于很多新手来说,理解这点是非常头疼的。

还有更奇葩的。把实例的方法作为参数传递时,实例是不会跟着过去的。也就是说,此时方法中的this在调用时指向的是全局this或者是undefined在声明了"use strict";时。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {  
    console.log(this.foo);   
}

function doIt(method) {
    method();
}


var thing = new Thing();
thing.logFoo(); //logs "bar"
doIt(thing.logFoo); //logs undefined

示例

所以很多人习惯将this缓存起来,用个叫self或者其他什么的变量来保存,以将外层与内层的this区分开来。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    var self = this;
    var info = "attempting to log this.foo:";
    function doIt() {
        console.log(info, self.foo);
    }
    doIt();
}


var thing = new Thing();
thing.logFoo();  //logs "attempting to log this.foo: bar"

示例

…但上面的方式不是万能的,在将方法做为参数传递时,就不起作用了。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    var self = this;
    function doIt() {
        console.log(self.foo);
    }
    doIt();
}

function doItIndirectly(method) {
    method();
}


var thing = new Thing();
thing.logFoo(); //logs "bar"
doItIndirectly(thing.logFoo); //logs undefined

示例

解决方法就是传递的时候使用bind方法显示指明上下文,bind方法是所有函数或方法都具有的。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    console.log(this.foo);
}

function doIt(method) {
    method();
}


var thing = new Thing();
doIt(thing.logFoo.bind(thing)); //logs bar

示例

同时也可以使用applycall 来调用该方法或函数,让它在一个新的上下文中执行。

function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () { 
    function doIt() {
        console.log(this.foo);
    }
    doIt.apply(this);
}

function doItIndirectly(method) {
    method();
}


var thing = new Thing();
doItIndirectly(thing.logFoo.bind(thing)); //logs bar

示例

使用bind可以任意改变函数或方法的执行上下文,即使它没有被绑定到一个实例的原型上。

function Thing() {
}
Thing.prototype.foo = "bar";


function logFoo(aStr) {
    console.log(aStr, this.foo);
}


var thing = new Thing();
logFoo.bind(thing)("using bind"); //logs "using bind bar"
logFoo.apply(thing, ["using apply"]); //logs "using apply bar"
logFoo.call(thing, "using call"); //logs "using call bar"
logFoo("using nothing"); //logs "using nothing undefined"

示例

避免在构造函数中返回作何东西,因为返回的东西可能覆盖本来该返回的实例。

function Thing() {
    return {};
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing = new Thing();
thing.logFoo(); //Uncaught TypeError: undefined is not a function

示例

但,如果你在构造函数里返回的是个原始值比如字符串或者数字什么的,上面的错误就不会发生了,返回语句将被忽略。所以最好别在一个将要通过new来调用的构造函数中返回作何东西,即使你是清醒的。如果你想实现工厂模式,那么请用一个函数来创建实例,并且不通过new来调用。当然这只是个人建议。

诚然,你也可以使用Object.create从而避免使用new。这样也能创建一个实例。

function Thing() {
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing =  Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"

示例

这种方式不会调用该构造函数。

function Thing() {
    this.foo = "foo";
}
Thing.prototype.foo = "bar";


Thing.prototype.logFoo = function () {
    console.log(this.foo);
}


var thing =  Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"

示例

正因为Object.create没有调用构造函数,这在当你想实现一个继承时是非常有用的,随后你可能想要重写构造函数。

function Thing1() {
    this.foo = "foo";
}
Thing1.prototype.foo = "bar";

function Thing2() {
    this.logFoo(); //logs "bar"
    Thing1.apply(this);
    this.logFoo(); //logs "foo"
}
Thing2.prototype = Object.create(Thing1.prototype);
Thing2.prototype.logFoo = function () {
    console.log(this.foo);
}

var thing = new Thing2();

示例

对象中的this

可以在对象的任何方法中使用this来访问该对象的属性。这与用new得到的实例是不一样的。

var obj = {
    foo: "bar",
    logFoo: function () {
        console.log(this.foo);
    }
};

obj.logFoo(); //logs "bar"

示例

注意这里并没有使用new,也没有用Object.create,更没有函数的调用来创建对象。也可以将函数绑定到对象,就好像这个对象是一个实例一样。

var obj = {
    foo: "bar"
};

function logFoo() {
    console.log(this.foo);
}

logFoo.apply(obj); //logs "bar"

示例

此时使用this没有向上查找原型链的复杂工序。通过this所拿到的只是该对象身上的属性而以。

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(this.foo);
        }
    }
};

obj.deeper.logFoo(); //logs undefined

示例

也可以不通过this,直接访问对象的属性。

var obj = {
    foo: "bar",
    deeper: {
        logFoo: function () {
            console.log(obj.foo);
        }
    }
};

obj.deeper.logFoo(); //logs "bar"

示例

DOM 事件回调中的this

在DOM事件的处理函数中,this指代的是被绑定该事件的DOM元素。

function Listener() {
    document.getElementById("foo").addEventListener("click",
       this.handleClick);
}
Listener.prototype.handleClick = function (event) {
    console.log(this); //logs "<div id="foo"></div>"
}

var listener = new Listener();
document.getElementById("foo").click();

示例

…除非你通过bind人为改变了事件处理器的执行上下文。

function Listener() {
    document.getElementById("foo").addEventListener("click", 
        this.handleClick.bind(this));
}
Listener.prototype.handleClick = function (event) {
    console.log(this); //logs Listener {handleClick: function}
}

var listener = new Listener();
document.getElementById("foo").click();

示例

HTML中的this

HTML标签的属性中是可能写JS的,这种情况下this指代该HTML元素。

<div id="foo" onclick="console.log(this);"></div>
<script type="text/javascript">
document.getElementById("foo").click(); //logs <div id="foo"...
</script>

示例

重写this

无法重写this,因为它是一个关键字。

function test () {
    var this = {};  // Uncaught SyntaxError: Unexpected token this 
}

示例

eval中的this

eval 中也可以正确获取当前的 this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    eval("console.log(this.foo)"); //logs "bar"
}

var thing = new Thing();
thing.logFoo();

示例

这里存在安全隐患。最好的办法就是避免使用eval

使用Function关键字创建的函数也可以获取this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = new Function("console.log(this.foo);");

var thing = new Thing();
thing.logFoo(); //logs "bar"

示例

使用with时的this

使用with可以将this人为添加到当前执行环境中而不需要显示地引用this

function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
    with (this) {
        console.log(foo);
        foo = "foo";
    }
}

var thing = new Thing();
thing.logFoo(); // logs "bar"
console.log(thing.foo); // logs "foo"

示例

正如很多人认为的那样,使用with是不好的,因为会产生歧义。

jQuery中的this

一如HTML DOM元素的事件回调,jQuery库中大多地方的this也是指代的DOM元素。页面上的事件回调和一些便利的静态方法比如$.each 都是这样的。

<div class="foo bar1"></div>
<div class="foo bar2"></div>
<script type="text/javascript">
$(".foo").each(function () {
    console.log(this); //logs <div class="foo...
});
$(".foo").on("click", function () {
    console.log(this); //logs <div class="foo...
});
$(".foo").each(function () {
    this.click();
});
</script>

示例

传递 this

如果你用过underscore.js或者lo-dash你便知道,这两个库中很多方法你可以传递一个参数来显示指定执行的上下文。比如_.each。自ECMAScript 5 标准后,一些原生的JS方法也允许传递上下文,比如forEach。事实上,上文提到的bindapply还有call 已经给我们手动指定函数执行上下文的能力了。

function Thing(type) {
    this.type = type;
}
Thing.prototype.log = function (thing) {
    console.log(this.type, thing);
}
Thing.prototype.logThings = function (arr) {
   arr.forEach(this.log, this); // logs "fruit apples..."
   _.each(arr, this.log, this); //logs "fruit apples..."
}

var thing = new Thing("fruit");
thing.logThings(["apples", "oranges", "strawberries", "bananas"]);

示例

这样可以使得代码简洁些,不用层层嵌套bind,也不用不断地缓存this

一些编程语言上手很简单,比如Go语言手册可以被快速读完。然后你差不多就掌握这门语言了,只是在实战时会有些小的问题或陷阱在等着你。

而JavaScript不是这样的。手册难读。非常多缺陷在里面,以至于人们抽离出了它好的部分The Good Parts)。最好的文档可能是MDN上的了。所以我建议你看看他上面关于this的介绍,并且始终在搜索JS相关问题时加上”mdn” 来获得最好的文档资料。静态代码检查也是个不错的工具,比如jshint

欢迎勘误及讨论,我的推特@bjorntipling

阅读详情 -> 详解this – 刘哇勇 – 博客园.

Categories: JavaScript