ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
<h2 id="9.1">Promise</h2> Promise是JavaScript异步操作解决方案。介绍Promise之前,先对异步操作做一个详细介绍。 ## JavaScript的异步执行 ### 概述 Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。 这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。 JavaScript语言本身并不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。 为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。"同步模式"就是传统做法,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及读写的操作。 "异步模式"则完全不同,每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了,再由系统通知执行第二段代码。所以,程序的执行顺序与任务的排列顺序是不一致的、异步的。 以下总结了"异步模式"编程的几种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的JavaScript程序。 ### 回调函数 回调函数是异步编程最基本的方法。 假定有两个函数f1和f2,后者等待前者的执行结果。 ```javascript f1(); f2(); ``` 如果`f1`是一个很耗时的任务,可以考虑改写`f1`,把`f2`写成`f1`的回调函数。 ```javascript function f1(callback){ setTimeout(function () { // f1的任务代码 callback(); }, 1000); } ``` 执行代码就变成下面这样: ```javascript f1(f2); ``` 采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。 回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度[耦合](http://en.wikipedia.org/wiki/Coupling_(computer_programming))(Coupling),使得程序结构混乱、流程难以追踪(尤其是回调函数嵌套的情况),而且每个任务只能指定一个回调函数。 ### 事件监听 另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。 还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的[写法](http://api.jquery.com/on/))。 ```javascript f1.on('done', f2); ``` 上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写: ```javascript function f1(){ setTimeout(function () { // f1的任务代码 f1.trigger('done'); }, 1000); } ``` 上面代码中,`f1.trigger('done')`表示,执行完成后,立即触发`done`事件,从而开始执行`f2`。 这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"[去耦合](http://en.wikipedia.org/wiki/Decoupling)"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。 ### 发布/订阅 "事件"完全可以理解成"信号",如果存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"[发布/订阅模式](http://en.wikipedia.org/wiki/Publish-subscribe_pattern)"(publish-subscribe pattern),又称"[观察者模式](http://en.wikipedia.org/wiki/Observer_pattern)"(observer pattern)。 这个模式有多种[实现](http://msdn.microsoft.com/en-us/magazine/hh201955.aspx),下面采用的是Ben Alman的[Tiny Pub/Sub](https://gist.github.com/661855),这是jQuery的一个插件。 首先,f2向"信号中心"jQuery订阅"done"信号。 ```javascript jQuery.subscribe("done", f2); ``` 然后,f1进行如下改写: ```javascript function f1(){ setTimeout(function () { // f1的任务代码 jQuery.publish("done"); }, 1000); } ``` jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。 f2完成执行后,也可以取消订阅(unsubscribe)。 ```javascript jQuery.unsubscribe("done", f2); ``` 这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。 ## 异步操作的流程控制 如果有多个异步操作,就存在一个流程控制的问题:确定操作执行的顺序,以后如何保证遵守这种顺序。 ```javascript function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function() { callback(arg * 2); }, 1000); } ``` 上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。 如果有6个这样的异步任务,需要全部完成后,才能执行下一步的final函数。 ```javascript function final(value) { console.log('完成: ', value); } ``` 请问应该如何安排操作流程? ```javascript async(1, function(value){ async(value, function(value){ async(value, function(value){ async(value, function(value){ async(value, function(value){ async(value, final); }); }); }); }); }); ``` 上面代码采用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。 ### 串行执行 我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。 ```javascript var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function series(item) { if(item) { async( item, function(result) { results.push(result); return series(items.shift()); }); } else { return final(results); } } series(items.shift()); ``` 上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。 ### 并行执行 流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。 ```javascript var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; items.forEach(function(item) { async(item, function(result){ results.push(result); if(results.length == items.length) { final(results); } }) }); ``` 上面代码中,forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。 并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。 ### 并行与串行的结合 所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源。 ```javascript var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2; function launcher() { while(running < limit && items.length > 0) { var item = items.shift(); async(item, function(result) { results.push(result); running--; if(items.length > 0) { launcher(); } else if(running == 0) { final(); } }); running++; } } launcher(); ``` 上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。 ## Promise对象 ### 简介 Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供[统一接口](http://wiki.commonjs.org/wiki/Promises/A)。 那么,什么是Promises? 首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。 简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。 比如,异步操作`f1`返回一个Promise对象,它的回调函数`f2`写法如下。 ```javascript (new Promise(f1)).then(f2); ``` 这种写法对于多层嵌套的回调函数尤其方便。 ```javascript // 传统写法 step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // ... }); }); }); }); // Promises的写法 (new Promise(step1)) .then(step2) .then(step3) .then(step4); ``` 从上面代码可以看到,采用Promises接口以后,程序流程变得非常清楚,十分易读。 注意,为了便于理解,上面代码的Promise对象的生成格式,做了简化,真正的语法请参照下文。 总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,再执行前期寄放在它上面的其他操作。 Promises原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。 ### Promise接口 前面说过,Promise接口的基本思想是,异步任务返回一个Promise对象。 Promise对象只有三种状态。 - 异步操作“未完成”(pending) - 异步操作“已完成”(resolved,又称fulfilled) - 异步操作“失败”(rejected) 这三种的状态的变化途径只有两种。 - 异步操作从“未完成”到“已完成” - 异步操作从“未完成”到“失败”。 这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise对象的最终结果只有两种。 - 异步操作成功,Promise对象传回一个值,状态变为`resolved`。 - 异步操作失败,Promise对象抛出一个错误,状态变为`rejected`。 Promise对象使用`then`方法添加回调函数。`then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`resolved`状态)时的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。 ```javascript // po是一个Promise对象 po.then( console.log, console.error ); ``` 上面代码中,Promise对象`po`使用`then`方法绑定两个回调函数:操作成功时的回调函数`console.log`,操作失败时的回调函数`console.error`(可以省略)。这两个函数都接受异步操作传回的值作为参数。 `then`方法可以链式使用。 ```javascript po .then(step1) .then(step2) .then(step3) .then( console.log, console.error ); ``` 上面代码中,`po`的状态一旦变为`resolved`,就依次调用后面每一个`then`指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个`then`方法的回调函数`console.log`和`console.error`,用法上有一点重要的区别。`console.log`只显示回调函数`step3`的返回值,而`console.error`可以显示`step1`、`step2`、`step3`之中任意一个发生的错误。也就是说,假定`step1`操作失败,抛出一个错误,这时`step2`和`step3`都不会再执行了(因为它们是操作成功的回调函数,而不是操作失败的回调函数)。Promises对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是`console.error`。这就是说,Promises对象的错误有传递性。 从同步的角度看,上面的代码大致等同于下面的形式。 ```javascript try { var v1 = step1(po); var v2 = step2(v1); var v3 = step3(v2); console.log(v3); } catch (error) { console.error(error); } ``` ### Promise对象的生成 ES6提供了原生的Promise构造函数,用来生成Promise实例。 下面代码创造了一个Promise实例。 ```javascript var promise = new Promise(function(resolve, reject) { // 异步操作的代码 if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); ``` Promise构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由JavaScript引擎提供,不用自己部署。 `resolve`函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从`Pending`变为`Resolved`),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;`reject`函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从`Pending`变为`Rejected`),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 Promise实例生成以后,可以用`then`方法分别指定`Resolved`状态和`Reject`状态的回调函数。 ```javascript po.then(function(value) { // success }, function(value) { // failure }); ``` ### 用法辨析 Promise的用法,简单说就是一句话:使用`then`方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里? ```javascript // 写法一 doSomething().then(function () { return doSomethingElse(); }); // 写法二 doSomething().then(function () { doSomethingElse(); }); // 写法三 doSomething().then(doSomethingElse()); // 写法四 doSomething().then(doSomethingElse); ``` 为了便于讲解,这四种写法都再用`then`方法接一个回调函数`finalHandler`。写法一的`finalHandler`回调函数的参数,是`doSomethingElse`函数的运行结果。 ```javascript doSomething().then(function () { return doSomethingElse(); }).then(finalHandler); ``` 写法二的`finalHandler`回调函数的参数是`undefined`。 ```javascript doSomething().then(function () { doSomethingElse(); return; }).then(finalHandler); ``` 写法三的`finalHandler`回调函数的参数,是`doSomethingElse`函数返回的回调函数的运行结果。 ```javascript doSomething().then(doSomethingElse()) .then(finalHandler); ``` 写法四与写法一只有一个差别,那就是`doSomethingElse`会接收到`doSomething()`返回的结果。 ```javascript doSomething().then(doSomethingElse) .then(finalHandler); ``` ## Promise的应用 ### 加载图片 我们可以把图片的加载写成一个`Promise`对象。 ```javascript var preloadImage = function (path) { return new Promise(function (resolve, reject) { var image = new Image(); image.onload = resolve; image.onerror = reject; image.src = path; }); }; ``` ### Ajax操作 Ajax操作是典型的异步操作,传统上往往写成下面这样。 ```javascript function search(term, onload, onerror) { var xhr, results, url; url = 'http://example.com/search?q=' + term; xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function (e) { if (this.status === 200) { results = JSON.parse(this.responseText); onload(results); } }; xhr.onerror = function (e) { onerror(e); }; xhr.send(); } search("Hello World", console.log, console.error); ``` 如果使用Promise对象,就可以写成下面这样。 ```javascript function search(term) { var url = 'http://example.com/search?q=' + term; var xhr = new XMLHttpRequest(); var result; var p = new Promise(function (resolve, reject) { xhr.open('GET', url, true); xhr.onload = function (e) { if (this.status === 200) { result = JSON.parse(this.responseText); resolve(result); } }; xhr.onerror = function (e) { reject(e); }; xhr.send(); }); return p; } search("Hello World").then(console.log, console.error); ``` 加载图片的例子,也可以用Ajax操作完成。 ```javascript function imgLoad(url) { return new Promise(function(resolve, reject) { var request = new XMLHttpRequest(); request.open('GET', url); request.responseType = 'blob'; request.onload = function() { if (request.status === 200) { resolve(request.response); } else { reject(new Error('图片加载失败:' + request.statusText)); } }; request.onerror = function() { reject(new Error('发生网络错误')); }; request.send(); }); } ``` ### 小结 Promise对象的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它的一整套接口,可以实现许多强大的功能,比如为多个异步操作部署一个回调函数、为多个回调函数中抛出的错误统一指定处理方法等等。 而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是,编写和理解都相对比较难。 <h2 id="9.2">JavaScript与有限状态机</h2> ## 概述 有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。 简单说,它有三个特征: - 状态总数(state)是有限的。 - 任一时刻,只处在一种状态之中。 - 某种条件下,会从一种状态转变(transition)到另一种状态。 它对JavaScript的意义在于,很多对象可以写成有限状态机。 举例来说,网页上有一个菜单元素。鼠标点击,菜单显示;鼠标再次点击,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。 代码可以写成下面这样: ```javascript var menu = {      // 当前状态   currentState: 'hide',      // 绑定事件   initialize: function() {     var self = this;     self.on("click", self.transition);   },      // 状态转换   transition: function(event){     switch(this.currentState) {       case "hide":         this.currentState = 'show';         doSomething();         break;       case "show":         this.currentState = 'hide';         doSomething();         break;       default:         console.log('Invalid State!');         break;     }   }    }; ``` 可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。 另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。 ## Javascript Finite State Machine函数库 下面介绍一个有限状态机的函数库[Javascript Finite State Machine](https://github.com/jakesgordon/javascript-state-machine)。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱。 该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例。 ```javascript var fsm = StateMachine.create(); ``` 生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述: ```javascript var fsm = StateMachine.create({      initial: 'green',      events: [     { name: 'warn', from: 'green', to: 'yellow' },     { name: 'stop', from: 'yellow', to: 'red' },     { name: 'ready', from: 'red', to: 'yellow' },     { name: 'go', from: 'yellow', to: 'green' }   ]    }); ``` 交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等。 生成实例以后,就可以随时查询当前状态。 - fsm.current :返回当前状态。 - fsm.is(s) :返回一个布尔值,表示状态s是否为当前状态。 - fsm.can(e) :返回一个布尔值,表示事件e是否能在当前状态触发。 - fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发。 Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例: - onbefore**warn**:在warn事件发生之前触发。 - onafter**warn**(可简写成onwarn) :在warn事件发生之后触发。 同时,它也允许为每个状态指定两个回调函数,以green状态为例: - onleave**green** :在离开green状态时触发。 - onenter**green**(可简写成ongreen) :在进入green状态时触发。 假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbefore**warn** → onleave**green** → onenter**yellow** → onafter**warn**。 除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数。 - onbeforeevent :任一事件发生之前触发。 - onleavestate :离开任一状态时触发。 - onenterstate :进入任一状态时触发。 - onafterevent :任一事件结束后触发。 如果事件的回调函数里面有异步操作(比如与服务器进行Ajax通信),这时我们可能希望等到异步操作结束,再发生状态改变。这就要用到transition方法。 ```javascript fsm.onleavegreen = function(){   light.fadeOut('slow', function() {     fsm.transition();   });   return StateMachine.ASYNC; }; ``` 上面代码的回调函数里面,有一个异步操作(light.fadeOut)。如果不希望状态立即改变,就要让回调函数返回StateMachine.ASYNC,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。 Javascript Finite State Machine还允许指定错误处理函数,当发生了当前状态不可能发生的事件时自动触发。 ```javascript var fsm = StateMachine.create({   // ...   error: function(eventName, from, to, args, errorCode, errorMessage) {     return 'event ' + eventName + ': ' + errorMessage;   },   // ... }); ``` 比如,当前状态是green,理论上这时只可能发生warn事件。要是这时发生了stop事件,就会触发上面的错误处理函数。 Javascript Finite State Machine的基本用法就是上面这些,更详细的介绍可以参见它的[主页](https://github.com/jakesgordon/javascript-state-machine)。 <h2 id="9.3">MVC框架与Backbone.js</h2> ## MVC框架 随着JavaScript程序变得越来越复杂,往往需要一个团队协作开发,这时代码的模块化和组织规范就变得异常重要了。MVC模式就是代码组织的经典模式。 (……MVC介绍。) **(1)Model** Model表示数据层,也就是程序需要的数据源,通常使用JSON格式表示。 **(2)View** View表示表现层,也就是用户界面,对于网页来说,就是用户看到的网页HTML代码。 **(3)Controller** Controller表示控制层,用来对原始数据(Model)进行加工,传送到View。 由于网页编程不同于客户端编程,在MVC的基础上,JavaScript社区产生了各种变体框架MVP(Model-View-Presenter)、MVVM(Model-View-ViewModel)等等,有人就把所有这一类框架的各种模式统称为MV*。 框架的优点在于合理组织代码、便于团队合作和未来的维护,缺点在于有一定的学习成本,且限制你只能采取它的写法。 ## 零框架解决方案 MVC框架(尤其是大型框架)有一个严重的缺点,就是会产生用户的重度依赖。一旦框架本身出现问题或者停止更新,用户的处境就会很困难,维护和更新成本极高。 ES6的到来,使得JavaScript语言有了原生的模块解决方案。于是,开发者有了另一种选择,就是不使用MVC框架,只使用各种单一用途的模块库,组合完成一个项目。下面是可供选择的各种用途的模块列表。 辅助功能库(Helper Libraries) - [moment.js](http://momentjs.com/):日期和时间的标准化 - [underscore.js](http://underscorejs.org/) / [Lo-Dash](https://lodash.com/):一系列函数式编程的功能函数 路由库(Routing) - [router.js](https://github.com/tildeio/router.js/):Ember.js使用的路由库 - [route-recognizer](https://github.com/tildeio/route-recognizer):功能全面的路由库 - [page.js](https://github.com/visionmedia/page.js):类似Express路由的库 - [director](https://github.com/flatiron/director):同时支持服务器和浏览器的路由库 Promise库 - [RSVP.js](https://github.com/tildeio/rsvp.js):ES6兼容的Promise库 - [ES6-Promise](https://github.com/jakearchibald/es6-promise):RSVP.js的子集,但是全面兼容ES6 - [q](https://github.com/kriskowal/q):最常用的Promise库之一,AngularJS用了它的精简版 - [native-promise-only](https://github.com/getify/native-promise-only):严格符合ES6的Promise标准,同时兼容老式浏览器 客户端与服务器的通信库 - [fetch](https://github.com/github/fetch):实现window.fetch功能 - [qwest](https://github.com/pyrsmk/qwest):支持XHR2和Promise的Ajax库 - [jQuery](https://github.com/jquery/jquery):jQuery 2.0支持按模块打包,因此可以创建一个纯Ajax功能库 动画库(Animation) - [cssanimevent](https://github.com/magnetikonline/cssanimevent):兼容老式浏览器的CSS3动画库 - [Velocity.js](http://julian.com/research/velocity/):性能优秀的动画库 辅助开发库(Development Assistance) - [LogJS](https://github.com/bfattori/LogJS):轻量级的logging功能库 - [UserTiming.js](https://github.com/nicjansma/usertiming.js):支持老式浏览器的高精度时间戳库 流程控制和架构(Flow Control/Architecture) - [ondomready](https://github.com/tubalmartin/ondomready):类似jQuery的ready()方法,符合AMD规范 - [script.js](https://github.com/ded/script.js]):异步的脚本加载和依赖关系管理库 - [async](https://github.com/caolan/async):浏览器和node.js的异步管理工具库 - [Virtual DOM](https://github.com/Matt-Esch/virtual-dom):react.js的一个替代方案,参见[Virtual DOM and diffing algorithm](https://gist.github.com/Raynos/8414846) 数据绑定(Data-binding) - Object.observe():Chrome已经支持该方法,可以轻易实现双向数据绑定 模板库(Templating) - [Mustache](http://mustache.github.io/):大概是目前使用最广的不含逻辑的模板系统 微框架(Micro-Framework) 某些情况下,可以使用微型框架,作为项目开发的起点。 - [bottlejs](https://github.com/young-steveo/bottlejs):提供惰性加载、中间件钩子、装饰器等功能 - [Stapes.js](http://hay.github.io/stapes/#top):微型MVC框架 - [soma.js](http://somajs.github.io/somajs/site/):提供一个松耦合、易测试的架构 - [knockout](http://knockoutjs.com/):最流行的微框架之一,主要关注UI ## Backbone的加载 ```html <script src="/javascripts/lib/jquery.js"></script> <script src="/javascripts/lib/underscore.js"></script> <script src="/javascripts/lib/backbone.js"></script> <script src="/javascripts/jst.js"></script> <script src="/javascripts/router.js"></script> <script src="/javascripts/init.js"></script> ``` ## Backbone的用法 Backbone是最早的JavaScript MVC框架,也是最简化的一个框架。它的设计思想是,只提供最基本的功能,给用户提供最大的自由。这意味着,好的一面是它没有一整套规则,强制你接受,坏的一面是很多功能你必须自己实现。Backbone的体积相当小,最小化后只有30多KB。 定义一个对象,表示Web应用。 ```javascript var AppName = { Models :{}, Views :{}, Collections :{}, Controllers :{} }; ``` 上面代码表示,应用由四部分组成:Model、Collection、Controller和View。 定义Model,表示数据的一个基本单位。 ```javascript AppName.Models.Person = Backbone.Model.extend({ urlRoot: "/persons" }); ``` 定义Collection,表示Model的集合。 ```javascript AppName.Collections.Library = Backbone.Collection.extend({ model: AppName.Models.Book }); ``` 上面代码表示,Collection对象必须有model属性,指明由哪一个model构成。 定义一个View。 ```javascript AppName.Views.Modals.AcceptDecline = Backbone.View.Extend({ el: ".modal-accept", events: { "ajax:success .link-accept" :"acceptSuccess", "ajax:error .link-accept" :"acceptError" }, acceptSuccess :function(evt, response) { this.$el.modal("hide"); alert('Cool! Thanks'); }, acceptError :function(evt, response) { var $modalContent = this.$el.find('.panel-modal'); $modalContent.append("Something was wrong!"); } }); ``` View对象必须有el属性,指明当前View绑定的DOM节点,events属性指明事件和对应的方法。 定义一个Controller。 ```javascript AppName.Controllers.Person = {}; AppName.Controllers.Person.show = function(id) { var aMa = new AppName.Models.Person({id: id}); aMa.updateAge(25); aMa.fetch().done(function(){ var view = new AppName.Views.Show({model: aMa}); }); }; ``` 最后,定义路由,启动应用程序。 ```javascript var Workspace = Backbone.Router.extend({ routes: { "*" :"wholeApp", "users/:id" :"usersShow", "users/:id/orders/" :"ordersIndex" }, wholeApp :AppName.Controller.Application.default, usersShow :AppName.Controller.Users.show, ordersIndex :AppName.Controller.Orders.index }); new Workspace(); Backbone.history.start({pushState: true}); ``` ## Backbone.View ### 基本用法 Backbone.View方法用于定义视图类。 ```javascrip var AppView = Backbone.View.extend({ render: function(){ $('main').append('<h1>一级标题</h1>'); } }); ``` 上面代码通过Backbone.View的extend方法,定义了一个视图类AppView。该类内部有一个render方法,用于将视图放置在网页上。 使用的时候,需要先新建视图类的实例,然后通过实例,调用render方法,从而让视图在网页上显示。 ```javascript var appView = new AppView(); appView.render(); ``` 上面代码新建视图类AppView的实例appView,然后调用appView.render,网页上就会显示指定的内容。 新建视图实例时,通常需要指定Model。 ```javascript var document = new Document({ model: doc }); ``` ### initialize方法 视图还可以定义initialize方法,生成实例的时候,会自动调用该方法对实例初始化。 ```javascript var AppView = Backbone.View.extend({ initialize: function(){ this.render(); }, render: function(){ $('main').append('<h1>一级标题</h1>'); } }); var appView = new AppView(); ``` 上面代码定义了initialize方法之后,就省去了生成实例后,手动调用appView.render()的步骤。 ### el属性,$el属性 除了直接在render方法中,指定“视图”所绑定的网页元素,还可以用视图的el属性指定网页元素。 ```javascript var AppView = Backbone.View.extend({ el: $('main'), render: function(){ this.$el.append('<h1>一级标题</h1>'); } }); ``` 上面的代码与render方法直接绑定网页元素,效果完全一样。上面代码中,除了el属性,还是$el属性,前者代表指定的DOM元素,后者则表示该DOM元素对应的jQuery对象。 ### tagName属性,className属性 如果不指定el属性,也可以通过tagName属性和className属性指定。 ```javascript var Document = Backbone.View.extend({ tagName: "li", className: "document", render: function() { // ... } }); ``` ### template方法 视图的template属性用来指定网页模板。 ```javascript var AppView = Backbone.View.extend({ template: _.template("<h3>Hello <%= who %><h3>"), }); ``` 上面代码中,underscore函数库的template函数,接受一个模板字符串作为参数,返回对应的模板函数。有了这个模板函数,只要提供具体的值,就能生成网页代码。 ```javascript var AppView = Backbone.View.extend({ el: $('#container'), template: _.template("<h3>Hello <%= who %><h3>"), initialize: function(){ this.render(); }, render: function(){ this.$el.html(this.template({who: 'world!'})); } }); ``` 上面代码的render就调用了template方法,从而生成具体的网页代码。 实际应用中,一般将模板放在script标签中,为了防止浏览器按照JavaScript代码解析,type属性设为text/template。 ```html <script type="text/template" data-name="templateName"> <!-- template contents goes here --> </script> ``` 可以使用下面的代码编译模板。 ```javascript window.templates = {}; var $sources = $('script[type="text/template"]'); $sources.each(function(index, el) { var $el = $(el); templates[$el.data('name')] = _.template($el.html()); }); ``` ### events属性 events属性用于指定视图的事件及其对应的处理函数。 ```javascript var Document = Backbone.View.extend({ events: { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" } }); ``` 上面代码中一个指定了三个CSS选择器的单击事件,及其对应的三个处理函数。 ### listento方法 listento方法用于为特定事件指定回调函数。 ```javascript var Document = Backbone.View.extend({ initialize: function() { this.listenTo(this.model, "change", this.render); } }); ``` 上面代码为model的change事件,指定了回调函数为render。 ### remove方法 remove方法用于移除一个视图。 ```javascript updateView: function() { view.remove(); view.render(); }; ``` ### 子视图(subview) 在父视图中可以调用子视图。下面就是一种写法。 ```javascript render : function (){ this.$el.html(this.template()); this.child = new Child(); this.child.appendTo($.('.container-placeholder').render(); } ``` ## Backbone.Events `Backbone.Events`是一个事件对象。任何继承了这个对象的对象,都具备了`Backbone.Events`的事件接口,可以调用on和trigger方法,发布和订阅消息。 ```javascript var EventChannel = _.extend({}, Backbone.Events); ``` 下面是一些例子。 ```javascript var channel = $.extend( {}, Backbone.Events ); channel.on('remove-node', function(msg) { // code to remove the node }); channel.trigger( 'remove-node', msg ); // 'msg' can be everything: String, number, object and so forth // also we can pass more than one message like the example below channel.on('add-node', function(node, callback) { // code to add a new node callback(); } ); channel.trigger('add-node', { label: 'I am a new node', color: 'black' }, function() { console.log( 'I am a callback' ); }); ``` ## Backbone.Router Router是Backbone提供的路由对象,用来将用户请求的网址与后端的处理函数一一对应。 首先,新定义一个Router类。 ```javascript Router = Backbone.Router.extend({ routes: { } }); ``` ## routes属性 Backbone.Router对象中,最重要的就是routes属性。它用来设置路径的处理方法。 routes属性是一个对象,它的每个成员就代表一个路径处理规则,键名为路径规则,键值为处理方法。 如果键名为空字符串,就代表根路径。 ```javascript routes: { '': 'phonesIndex', }, phonesIndex: function () { new PhonesIndexView({ el: 'section#main' }); } ``` 星号代表任意路径,可以设置路径参数,捕获具体的路径值。 ```javascript var AppRouter = Backbone.Router.extend({ routes: { "*actions": "defaultRoute" } }); var app_router = new AppRouter; app_router.on('route:defaultRoute', function(actions) { console.log(actions); }) ``` 上面代码中,根路径后面的参数,都会被捕获,传入回调函数。 路径规则的写法。 ```javascript var myrouter = Backbone.Router.extend({ routes: { "help": "help", "search/:query": "search" }, help: function() { ... }, search: function(query) { ... } }); routes: { "help/:page": "help", "download/*path": "download", "folder/:name": "openFolder", "folder/:name-:mode": "openFolder" } router.on("route:help", function(page) { ... }); ``` ## Backbone.history 设置了router以后,就可以启动应用程序。Backbone.history对象用来监控url的变化。 ```javascript App = new Router(); $(document).ready(function () { Backbone.history.start({ pushState: true }); }); ``` 打开pushState方法。如果应用程序不在根目录,就需要指定根目录。 ```javascript Backbone.history.start({pushState: true, root: "/public/search/"}) ``` ## Backbone.Model Model代表单个的对象实体。 ```javascript var User = Backbone.Model.extend({ defaults: { name: '', email: '' } }); var user = new User(); ``` 上面代码使用extend方法,生成了一个User类,它代表model的模板。然后,使用new命令,生成一个Model的实例。defaults属性用来设置默认属性,上面代码表示user对象默认有name和email两个属性,它们的值都等于空字符串。 生成实例时,可以提供各个属性的具体值。 ```javascript var user = new User ({ id: 1, name: 'name', email: 'name@email.com' }); ``` 上面代码在生成实例时,提供了各个属性的具体值。 ### idAttribute属性 Model实例必须有一个属性,作为区分其他实例的主键。这个属性的名称,由idAttribute属性设定,一般是设为id。 ```javascript var Music = Backbone.Model.extend({ idAttribute: 'id' }); ``` ### get方法 get方法用于返回Model实例的某个属性的值。 ```javascript var user = new User({ name: "name", age: 24}); var age = user.get("age"); // 24 var name = user.get("name"); // "name" ``` ### set方法 set方法用于设置Model实例的某个属性的值。 ```javascript var User = Backbone.Model.extend({ buy: function(newCarsName){ this.set({car: newCarsName }); } }); var user = new User({name: 'BMW',model:'i8',type:'car'}); user.buy('Porsche'); var car = user.get("car"); // ‘Porsche’ ``` ### on方法 on方法用于监听对象的变化。 ```javascript var user = new User({name: 'BMW',model:'i8'}); user.on("change:name", function(model){ var name = model.get("name"); // "Porsche" console.log("Changed my car’s name to " + name); }); user.set({name: 'Porsche'}); // Changed my car’s name to Porsche ``` 上面代码中的on方法用于监听事件,“change:name”表示name属性发生变化。 ### urlroot属性 该属性用于指定服务器端对model进行操作的路径。 ```javascript var User = Backbone.Model.extend({ urlRoot: '/user' }); ``` 上面代码指定,服务器对应该Model的路径为/user。 ### fetch事件 fetch事件用于从服务器取出Model。 ```javascript var user = new User ({id: 1}); user.fetch({ success: function (user){ console.log(user.toJSON()); } }) ``` 上面代码中,user实例含有id属性(值为1),fetch方法使用HTTP动词GET,向网址“/user/1”发出请求,从服务器取出该实例。 ### save方法 save方法用于通知服务器新建或更新Model。 如果一个Model实例不含有id属性,则save方法将使用POST方法新建该实例。 ```javascript var User = Backbone.Model.extend({ urlRoot: '/user' }); var user = new User (); var userDetails = { name: 'name', email: 'name@email.com' }; user.save(userDetails, { success: function (user) { console.log(user.toJSON()); } }) ``` 上面代码先在类中指定Model对应的网址是/user,然后新建一个实例,最后调用save方法。它有两个参数,第一个是实例对象的具体属性,第二个参数是一个回调函数对象,设定success事件(保存成功)的回调函数。具体来说,save方法会向/user发出一个POST请求,并将{name: 'name', email: 'name@email.com'}作为数据提供。 如果一个Model实例含有id属性,则save方法将使用PUT方法更新该实例。 ```javascript var user = new User ({ id: 1, name: '张三', email: 'name@email.com' }); user.save({name: '李四'}, { success: function (model) { console.log(user.toJSON()); } }); ``` 上面代码中,对象实例含有id属性(值为1),save将使用PUT方法向网址“/user/1”发出请求,从而更新该实例。 ### destroy方法 destroy方法用于在服务器上删除该实例。 ```javascript var user = new User ({ id: 1, name: 'name', email: 'name@email.com' }); user.destroy({ success: function () { console.log('Destroyed'); } }); ``` 上面代码的destroy方法,将使用HTTP动词DELETE,向网址“/user/1”发出请求,删除对应的Model实例。 ## Backbone.Collection Collection是同一类Model的集合,比如Model是动物,Collection就是动物园;Model是单个的人,Collection就是一家公司。 ```javascript var Song = Backbone.Model.extend({}); var Album = Backbone.Collection.extend({ model: Song }); ``` 上面代码中,Song是Model,Album是Collection,而且Album有一个model属性等于Song,因此表明Album是Song的集合。 ### add方法,remove方法 Model的实例可以直接放入Collection的实例,也可以用add方法添加。 ```javascript var song1 = new Song({ id: 1 ,name: "歌名1", artist: "张三" }); var song2 = new Music ({id: 2,name: "歌名2", artist: "李四" }); var myAlbum = new Album([song1, song2]); var song3 = new Music({ id: 3, name: "歌名3",artist:"赵五" }); myAlbum.add(song3); ``` remove方法用于从Collection实例中移除一个Model实例。 ```javascript myAlbum.remove(1); ``` 上面代码表明,remove方法的参数是model实例的id属性。 ### get方法,set方法 get方法用于从Collection中获取指定id的Model实例。 ```javascript myAlbum.get(2)) ``` ### fetch方法 fetch方法用于从服务器取出Collection数据。 ```javascript var songs = new Backbone.Collection; songs.url = '/songs'; songs.fetch(); ``` ## Backbone.events ```javascript var obj = {}; _.extend(obj, Backbone.Events); obj.on("show-message", function(msg) { $('#display').text(msg); }); obj.trigger("show-message", "Hello World"); ``` <h2 id="9.4">严格模式</h2> ## 概述 ### 设计目的 除了正常运行模式,ECMAScript 5添加了第二种运行模式:“严格模式”(strict mode)。顾名思义,这种模式使得JavaScript在更严格的条件下运行。 设立”严格模式“的目的,主要有以下几个: - 消除JavaScript语法的一些不合理、不严谨之处,减少一些怪异行为; - 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全; - 提高编译器效率,增加运行速度; - 为未来新版本的JavaScript做好铺垫。 “严格模式”体现了JavaScript更合理、更安全、更严谨的发展方向。 同样的代码,在”正常模式“和”严格模式“中,可能会有不一样的运行结果。一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解JavaScript,让你变成一个更好的程序员。 ### 启用方法 进入“严格模式”的标志,是一行字符串`use strict`。 ```javascript 'use strict'; ``` 老版本的浏览器会把它当作一行普通字符串,加以忽略。新版本的浏览器就会进入严格模式。 “严格模式”可以用于整个脚本,也可以只用于单个函数。 **(1) 针对整个脚本文件** 将`use strict`放在脚本文件的第一行,则整个脚本都将以“严格模式”运行。如果这行语句不在第一行就无效,整个脚本会以“正常模式”运行。(严格地说,只要前面不是产生实际运行结果的语句,`use strict`可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。) ```html <script> 'use strict'; console.log('这是严格模式'); </script> <script> console.log('这是正常模式'); </script> ``` 上面的代码表示,一个网页文件中依次有两段JavaScript代码。前一个`<script>`标签是严格模式,后一个不是。 如果字符串`use strict`出现在代码中间,则不起作用,即严格模式必须从代码一开始就生效。 两个不同模式的脚本合并成一个文件,如果严格模式的脚本在前,则合并后的脚本都是”严格模式“;如果正常模式的脚本在前,则合并后的脚本都是”正常模式“。总之,这两种情况下,合并后的结果都是不正确的。因此,建议在多个脚本需要合并的场合,”严格模式“只在函数中打开,不针对整个脚本打开。 **(2)针对单个函数** 将“use strict”放在函数体的第一行,则整个函数以“严格模式”运行。 ```javascript function strict() { 'use strict'; return '这是严格模式'; } function notStrict() { return '这是正常模式'; } ``` **(3)脚本文件的变通写法** 因为在脚本文件第一行放置`use strict`不利于文件合并,所以更好的做法是,借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。 ```javascript (function () { "use strict"; // some code here })(); ``` ## 显式报错 严格模式使得JavaScript的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错。 ### 字符串的length属性不可写 严格模式下,设置字符串的`length`属性,会报错。 ```javascript 'use strict'; 'abc'.length = 5; ``` 实际上,严格模式下,对只读属性赋值,或者删除不可配置(nonconfigurable)属性都会报错。 ### eval、arguments不可用作函数名 使用`eval`,或者在函数内部使用`arguments`,作为标识名,将会报错。 下面的语句都会报错。 ```javascript 'use strict'; eval = 17; arguments++; ++eval; var obj = { set p(arguments) { } }; var eval; try { } catch (arguments) { } function x(eval) { } function arguments() { } var y = function eval() { }; var f = new Function("arguments", "'use strict'; return 17;"); ``` ### 只读属性不可写 正常模式下,对一个对象的只读属性进行赋值,不会报错,只会默默地失败。严格模式下,将报错。 ```javascript 'use strict'; var o = {}; Object.defineProperty(o, 'v', { value: 1, writable: false }); o.v = 2; // 报错 ``` ### 只设置了赋值器的属性不可写 严格模式下,对一个只设置了赋值器(getter)的属性赋值,会报错。 ```javascript "use strict"; var o = { get v() { return 1; } }; o.v = 2; // 报错 ``` ### 禁止扩展的对象不可扩展 严格模式下,对禁止扩展的对象添加新属性,会报错。 ```javascript 'use strict'; var o = {}; Object.preventExtensions(o); o.v = 1; // 报错 ``` ### 禁止删除不可删除的属性 严格模式下,删除一个不可删除的属性,会报错。 ```javascript 'use strict'; delete Object.prototype; // 报错 ``` ### 函数不能有重名的参数 正常模式下,如果函数有多个重名的参数,可以用`arguments[i]`读取。严格模式下,这属于语法错误。 ```javascript function f(a, a, b) { // 语法错误 'use strict'; return a + b; } ``` ### 禁止八进制的前缀0表示法 正常模式下,整数的第一位如果是`0`,表示这是八进制数,比如`0100`等于十进制的64。严格模式禁止这种表示法,整数第一位为`0`,将报错。 ```javascript "use strict"; var n = 0100; // SyntaxError ``` ## 增强的安全措施 严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误。 ### 全局变量显式声明 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。 ```javascript 'use strict'; v = 1; // 报错,v未声明 for (i = 0; i < 2; i++) { // 报错,i未声明 // ... } function f() { x = 123; } f() // 报错,未声明就创建一个全局变量 ``` 因此,严格模式下,变量都必须先用`var`命令声明,然后再使用。 ### 禁止this关键字指向全局对象 正常模式下,函数内部的`this`可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。 ```javascript // 正常模式 function f() { console.log(this === window); } f() // true // 严格模式 function f() { 'use strict'; console.log(this === undefined); } f() // true ``` 这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加`new`,这时`this`不再指向全局对象,而是报错。 ```javascript function f() { 'use strict'; this.a = 1; }; f();// 报错,this未定义 ``` 严格模式下,函数直接调用时(不使用`new`调用),函数内部的`this`表示`undefined`,因此可以用`call`、`apply`和`bind`方法,将任意值绑定在`this`上面。 ```javascript 'use strict'; function fun() { return this; } fun() //undefined fun.call(2) // 2 fun.apply(null) // null fun.call(undefined) // undefined fun.bind(true)() // true ``` ### 禁止使用fn.callee、fn.caller 函数内部不得使用`fn.caller`、`fn.arguments`,否则会报错。这意味着不能在函数内部得到调用栈了。 ```javascript function f1() { 'use strict'; f1.caller; // 报错 f1.arguments; // 报错 } f1(); ``` ### 禁止使用arguments.callee、arguments.caller `arguments.callee`和`arguments.caller`是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用`arguments.callee`、`arguments.caller`将会报错。 ```javascript 'use strict'; var f = function() { return arguments.callee; }; f(); // 报错 ``` ### 禁止删除变量 严格模式下无法删除变量,如果使用`delete`命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的`configurable`属性设置为true,才能被`delete`命令删除。 ```javascript "use strict"; var x; delete x; // 语法错误 var o = Object.create(null, { x: { value: 1, configurable: true } }); delete o.x; // 删除成功 ``` ## 静态绑定 JavaScript语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。 严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。 具体来说,涉及以下几个方面。 ### 禁止使用with语句 严格模式下,使用`with`语句将报错。因为`with`语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果。 ```javascript 'use strict'; var v = 1; with (o) { // SyntaxError v = 2; } ``` ### 创设eval作用域 正常模式下,JavaScript语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:`eval`作用域。 正常模式下,`eval`语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,`eval`语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,`eval`所生成的变量只能用于`eval`内部。 ```javascript (function () { 'use strict'; var x = 2; console.log(eval('var x = 5; x')) // 5 console.log(x) // 2 })() ``` 注意,如果希望`eval`语句也使用严格模式,有两种方式。 ```javascript // 方式一 function f1(str){ 'use strict'; return eval(str); } f1('undeclared_variable = 1'); // 报错 // 方式二 function f2(str){ return eval(str); } f2('"use strict";undeclared_variable = 1') // 报错 ``` 上面两种写法,`eval`内部使用的都是严格模式。 ### arguments不再追踪参数的变化 变量`arguments`代表函数的参数。严格模式下,函数内部改变参数与`arguments`的联系被切断了,两者不再存在联动关系。 ```javascript function f(a) { a = 2; return [a, arguments[0]]; } f(1); // 正常模式为[2, 2] function f(a) { "use strict"; a = 2; return [a, arguments[0]]; } f(1); // 严格模式为[2, 1] ``` 上面代码中,改变函数的参数,不会反应到`arguments`对象上来。 ## 向下一个版本的JavaScript过渡 JavaScript语言的下一个版本是ECMAScript 6,为了平稳过渡,严格模式引入了一些ES6语法。 ### 函数必须声明在顶层 JavaScript的新版本ES6会引入“块级作用域”。为了与新版本接轨,严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。 ```javascript "use strict"; if (true) { function f1() { } // 语法错误 } for (var i = 0; i < 5; i++) { function f2() { } // 语法错误 } ``` 上面代码在`if`代码块和`for`代码块中声明了函数,在严格模式下都会报错。 ### 保留字 为了向将来JavaScript的新版本过渡,严格模式新增了一些保留字:implements, interface, let, package, private, protected, public, static, yield。 使用这些词作为变量名将会报错。 ```javascript function package(protected) { // 语法错误 'use strict'; var implements; // 语法错误 } ``` 此外,ECMAscript第五版本身还规定了另一些保留字(`class`, `enum`, `export`, `extends`, `import`, `super`),以及各大浏览器自行增加的`const`保留字,也是不能作为变量名的。