Node.js 中的未捕获异常怎么解决

JavaScript08

Node.js 中的未捕获异常怎么解决,第1张

1. 未捕获异常带来的问题

由于 Node.js 的单线程特性,未捕获异常是一个应用开发过程中值得注意的问题。Node.js 遵循错误第一,数据第二的回调模式。我们经常在看到这样的例子:当回调函数返回一个错误对象,那就立即抛出这个错误。

var fs = require('fs') fs.readFile('somefile.txt', function (err, data) {  if (err) throw err  console.log(data)})

如果你运行这个程序,而且假设你没有 somefile.txt 这个文件,一个错误将被抛出。

Error: ENOENT, open 'somefile.txt'

这将导致进程崩溃进而影响到整个APP。 

这是有意为之,Node.js 不打算把你的应用和服务分隔开。

2. 怎样处理未捕获异常

处理未捕获异常的最佳方式是什么呢?有非常多方法:

你的应用不应该有未捕获错误,这很疯狂。

你应该让你的应用在崩溃后找到未捕获异常,然后修复它们,这也很疯狂。

对错误视而不见,不处理它—这是大多数人做的,然而这糟透了。

你应该让你的应用在崩溃后打印出错误日志,然后借用 upstart , forever , monit 之类的东西重启进程。这个方法很实用。

【译者注:现已废弃】你应该开始使用 Domains 模块来处理错误,这是必由之路,虽然这还是 Node.js 的一个试验性功能。

现在我们来详细展开这些方法。

3. 一个没有未捕获异常的应用

「没有未捕获异常的应用」这个概念对我来说很怪异,任何应用在某个时刻都会有异常而且可能是未捕获的异常。如果你坚持这个观点并把错误扔给用户,那么我觉得你要做好半夜接到电话被告知服务崩溃掉了的准备。

4. 使你的应用崩溃

The only defence I can find in this opinion is the fail fast argument. You are going to fix your application quickly if it unavailable. If an application without uncaught exceptions is denial letting your application crash is acceptance. But you are still pushing exception handling onto your users.(原谅我实在想不出怎么翻译这段,如果你有好的想法,请尽快联系我!)

5. 假装没看见错误?

很多人这样做:

1   <p style="margin-bottom: 7px">process.on('uncaughtException', function (err) {<br>  console.log(err)<br>})<br></p>   

这很糟糕,当一个未捕获异常被抛出,你应该意识到你的应用处在一个不正常的状态,这种情况下你无法可靠地运行你的程序。

最初提出 process.on 事件的 Felix Geisendörfer 现在倡议去除它。

6. 应用崩溃,打印日志,然后重启

通过这个方法你可以让你的应用在发生未捕获异常时立即崩溃,然后利用 forever 或 upstart 这样的工具(几乎可以)立即重启。Node.js 将会把异常写入 STERR 所以你可以把异常重定向到一个日志文件稍晚再通过它拿到错误。这种方法的缺点是,对于错误发生在你的代码之外的 i/o ,不能提供一种优雅的方法来处理临时停电或者网络 i/o 出错的场景。这真是一个利器!— 重启应用并重试。如果你把这种策略与 cluster module 相结合,node 可以自动重启任何抛出错误的 children 并且打印出错误。

   var cluster = require('cluster')var workers = process.env.WORKERS || require('os').cpus().lengthif(cluster.isMaster) {  console.log('start cluster with %s workers', workers)  for (var i = 0i <workers++i) {    varworker = cluster.fork().process    console.log('worker %s started.', worker.pid)  }  cluster.on('exit', function(worker) {    console.log('worker %s died. restart...', worker.process.pid)    cluster.fork()  })} else {  var http = require('http')  http.createServer(function (req, res) {    res.end("Look Mum! I'm a server!\n")  }).listen(3000, "127.0.0.1")}process.on('uncaughtException', function (err) {  console.error((new Date).toUTCString() + ' uncaughtException:', err.message)  console.error(err.stack)  process.exit(1)})   

7. 使用 Domains 模块【译者注:现已废弃】

Domains 是 Node.js v0.8 版本中新增的一个试验性特性,它使得异常处理变得更加灵活和精确。下面是刚才那个文件不存在的例子,通过使用 domains 你可以为一个特定的 domain 触发 error 事件,你还可以针对不同的场景使用不同的异常处理。这使得你根据异常的发生地点来对应地处理它们。如果退出进程像是用榔头敲碎坚果,那么这就像一把精确的手术刀为你提供对程序完全的控制。

   var domain = require('domain')var d = domain.create()var fs = require('fs')d.on('error', function(err) {  console.error(err)})d.run(function() {  fs.readFile('somefile.txt', function (err, data) {    if (err) throw err    console.log(data)  })})   

8. 结论

如果你在产品环境运行 Node.js 你起码应该对如何处理异常有一个想法。目前为止我相信当异常被抛出时,大多数人只是重启应用(也许是优雅地重启),Domains 为应用提供了一种更聪明的面对异常的能力,异常处理器可能会选择简单的清理、关闭某些连接,最坏的情况下,退出进程。关键点就在于你有了选择。

一、JavaScript异步编程的两个核心难点

异步I/O、事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络、文件访问功能,且使之在后端实现了较高的性能。然而异步风格也引来了一些麻烦,其中比较核心的问题是:

1、函数嵌套过深

JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成

金字塔型结构。这不仅使得代码变难看难懂,更使得调试、重构的过程充满风险。

2、异常处理

回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂。这里主要讲讲异常处理。

二、异常处理

像很多时髦的语言一样,JavaScript 也允许抛出异常,随后再用一个try/catch

语句块捕获。如果抛出的异常未被捕获,大多数JavaScript环境都会提供一个有用的堆栈轨迹。举个例子,下面这段代码由于'{'为无效JSON

对象而抛出异常。

?

12345678

function JSONToObject(jsonStr) { return JSON.parse(jsonStr)}var obj = JSONToObject('{')//SyntaxError: Unexpected end of input//at Object.parse (native)//at JSONToObject (/AsyncJS/stackTrace.js:2:15)//at Object.<anonymous>(/AsyncJS/stackTrace.js:4:11)

堆栈轨迹不仅告诉我们哪里抛出了错误,而且说明了最初出错的地方:第4 行代码。遗憾的是,自顶向下地跟踪异步错误起源并不都这么直截了当。

异步编程中可能抛出错误的情况有两种:回调函数错误、异步函数错误。

1、回调函数错误

如果从异步回调中抛出错误,会发生什么事?让我们先来做个测试。

?

1234567

setTimeout(function A() { setTimeout(function B() { setTimeout(function C() { throw new Error('Something terrible has happened!')}, 0)}, 0)}, 0)

上述应用的结果是一条极其简短的堆栈轨迹。

?

12

Error: Something terrible has happened!at Timer.C (/AsyncJS/nestedErrors.js:4:13)

等等,A 和B 发生了什么事?为什么它们没有出现在堆栈轨迹中?这是因为运行C 的时候,异步函数的上下文已经不存在了,A 和B 并不在内存堆栈里。这3

个函数都是从事件队列直接运行的。基于同样的理由,利用try/catch

语句块并不能捕获从异步回调中抛出的错误。另外回调函数中的return也失去了意义。

?

1234567

try { setTimeout(function() { throw new Error('Catch me if you can!')}, 0)} catch (e) {console.error(e)}

看到这里的问题了吗?这里的try/catch 语句块只捕获setTimeout函数自身内部发生的那些错误。因为setTimeout

异步地运行其回调,所以即使延时设置为0,回调抛出的错误也会直接流向应用程序。

总的来说,取用异步回调的函数即使包装上try/catch 语句块,也只是无用之举。(特例是,该异步函数确实是在同步地做某些事且容易出错。例如,Node

的fs.watch(file,callback)就是这样一个函数,它在目标文件不存在时会抛出一个错误。)正因为此,Node.js

中的回调几乎总是接受一个错误作为其首个参数,这样就允许回调自己来决定如何处理这个错误。

2、异步函数错误

由于异步函数是立刻返回的,异步事务中发生的错误是无法通过try-catch来捕捉的,只能采用由调用方提供错误处理回调的方案来解决。

例如Node中常见的function (err, ...)

{...}回调函数,就是Node中处理错误的约定:即将错误作为回调函数的第一个实参返回。再比如HTML5中FileReader对象的onerror函数,会被用于处理异步读取文件过程中的错误。

举个例子,下面这个Node 应用尝试异步地读取一个文件,还负责记录下任何错误(如“文件不存在”)。

?

1234567

var fs = require('fs')fs.readFile('fhgwgdz.txt', function(err, data) { if (err) { return console.error(err)}console.log(data.toString('utf8'))})

客户端JavaScript 库的一致性要稍微差些,不过最常见的模式是,针对成败这两种情形各规定一个单独的回调。jQuery 的Ajax

方法就遵循了这个模式。

?

1234

$.get('/data', { success: successHandler, failure: failureHandler})

不管API 形态像什么,始终要记住的是,只能在回调内部处理源于回调的异步错误。

三、未捕获异常的处理

如果是从回调中抛出异常的,则由那个调用了回调的人负责捕获该异常。但如果异常从未被捕获,又会怎么样?这时,不同的JavaScript环境有着不同的游戏规则……

1. 在浏览器环境中

现代浏览器会在开发人员控制台显示那些未捕获的异常,接着返回事件队列。要想修改这种行为,可以给window.onerror

附加一个处理器。如果windows.onerror 处理器返回true,则能阻止浏览器的默认错误处理行为。

?

123

window.onerror = function(err) { return true//彻底忽略所有错误}

在成品应用中, 会考虑某种JavaScript 错误处理服务, 譬如Errorception。Errorception

提供了一个现成的windows.onerror 处理器,它向应用服务器报告所有未捕获的异常,接着应用服务器发送消息通知我们。

2. 在Node.js 环境中

在Node 环境中,window.onerror 的类似物就是process 对象的uncaughtException 事件。正常情况下,Node

应用会因未捕获的异常而立即退出。但只要至少还有一个uncaughtException 事件处理

器,Node 应用就会直接返回事件队列。

?

123

process.on('uncaughtException', function(err) { console.error(err)//避免了关停的命运!})

但是,自Node 0.8.4 起,uncaughtException 事件就被废弃了。据其文档所言,对异常处理而言,uncaughtException

是一种非常粗暴的机制,请勿使用uncaughtException,而应使用Domain 对象。

Domain 对象又是什么?你可能会这样问。Domain 对象是事件化对象,它将throw 转化为'error'事件。下面是一个例子。

?

123456789

var myDomain = require('domain').create()myDomain.run(function() { setTimeout(function() { throw new Error('Listen to me!') }, 50)})myDomain.on('error', function(err) { console.log('Error ignored!')})

源于延时事件的throw 只是简单地触发了Domain 对象的错误处理器。

Error ignored!

很奇妙,是不是?Domain 对象让throw

语句生动了很多。不管在浏览器端还是服务器端,全局的异常处理器都应被视作最后一根救命稻草。请仅在调试时才使用它。

四、几种解决方案

下面对几种解决方案的讨论主要集中于上面提到的两个核心问题上,当然也会考虑其他方面的因素来评判其优缺点。

1、Async.js

首先是Node中非常著名的Async.js,这个库能够在Node中展露头角,恐怕也得归功于Node统一的错误处理约定。

而在前端,一开始并没有形成这么统一的约定,因此使用Async.js的话可能需要对现有的库进行封装。

Async.js的其实就是给回调函数的几种常见使用模式加了一层包装。比如我们需要三个前后依赖的异步操作,采用纯回调函数写法如下:

?

12345678910111213141516

asyncOpA(a, b, (err, result) =>{ if (err) { handleErrorA(err)} asyncOpB(c, result, (err, result) =>{ if (err) { handleErrorB(err)} asyncOpB(d, result, (err, result) =>{ if (err) { handlerErrorC(err) } finalOp(result)})})})

如果我们采用async库来做:

?

12345678910111213141516171819202122

async.waterfall([ (cb) =>{ asyncOpA(a, b, (err, result) =>{ cb(err, c, result)})}, (c, lastResult, cb) =>{ asyncOpB(c, lastResult, (err, result) =>{ cb(err, d, result)}) }, (d, lastResult, cb) =>{ asyncOpC(d, lastResult, (err, result) =>{ cb(err, result)})}], (err, finalResult) =>{ if (err) { handlerError(err)} finalOp(finalResult)})

可以看到,回调函数由原来的横向发展转变为纵向发展,同时错误被统一传递到最后的处理函数中。

其原理是,将函数数组中的后一个函数包装后作为前一个函数的末参数cb传入,同时要求:

每一个函数都应当执行其cb参数cb的第一个参数用来传递错误。我们可以自己写一个async.waterfall的实现:

?

12345678910111213141516171819202122

let async = { waterfall: (methods, finalCb = _emptyFunction) =>{ if (!_isArray(methods)) { return finalCb(new Error('First argument to waterfall must be an array of functions'))} if (!methods.length) { return finalCb()} function wrap(n) { if (n === methods.length) { return finalCb } return function (err, ...args) { if (err) { return finalCb(err) } methods[n](...args, wrap(n + 1)) } } wrap(0)(false)}}

Async.js还有series/parallel/whilst等多种流程控制方法,来实现常见的异步协作。

Async.js的问题:

在外在上依然没有摆脱回调函数,只是将其从横向发展变为纵向,还是需要程序员熟练异步回调风格。

错误处理上仍然没有利用上try-catch和throw,依赖于“回调函数的第一个参数用来传递错误”这样的一个约定。

2、Promise方案

ES6的Promise来源于Promise/A+。使用Promise来进行异步流程控制,有几个需要注意的问题,

把前面提到的功能用Promise来实现,需要先包装异步函数,使之能返回一个Promise:

?

12345678910

function toPromiseStyle(fn) { return (...args) =>{ return new Promise((resolve, reject) =>{ fn(...args, (err, result) =>{ if (err) reject(err) resolve(result) }) })}}

这个函数可以把符合下述规则的异步函数转换为返回Promise的函数:

回调函数的第一个参数用于传递错误,第二个参数用于传递正常的结果。接着就可以进行操作了:

?

123456789101112131415

let [opA, opB, opC] = [asyncOpA, asyncOpB, asyncOpC].map((fn) =>toPromiseStyle(fn)) opA(a, b) .then((res) =>{ return opB(c, res)}) .then((res) =>{ return opC(d, res)}) .then((res) =>{ return finalOp(res)}) .catch((err) =>{ handleError(err)})

通过Promise,原来明显的异步回调函数风格显得更像同步编程风格,我们只需要使用then方法将结果传递下去即可,同时return也有了相应的意义:

在每一个then的onFullfilled函数(以及onRejected)里的return,都会为下一个then的onFullfilled函数(以及onRejected)的参数设定好值。

如此一来,return、try-catch/throw都可以使用了,但catch是以方法的形式出现,还是不尽如人意。

3、Generator方案

ES6引入的Generator可以理解为可在运行中转移控制权给其他代码,并在需要的时候返回继续执行的函数。利用Generator可以实现协程的功能。

将Generator与Promise结合,可以进一步将异步代码转化为同步风格:

?

1234567891011

function* getResult() { let res, a, b, c, dtry { res = yield opA(a, b)res = yield opB(c, res)res = yield opC(d)return res} catch (err) { return handleError(err)}}

然而我们还需要一个可以自动运行Generator的函数:

?

123456789101112131415161718192021222324252627282930

function spawn(genF, ...args) { return new Promise((resolve, reject) =>{ let gen = genF(...args) function next(fn) { try { let r = fn() if (r.done) { resolve(r.value) } Promise.resolve(r.value) .then((v) =>{ next(() =>{return gen.next(v) }) }).catch((err) =>{ next(() =>{return gen.throw(err) }) }) } catch (err) { reject(err) } } next(() =>{ return gen.next(undefined)})})}

用这个函数来调用Generator即可:

?

1234567

spawn(getResult) .then((res) =>{ finalOp(res)}) .catch((err) =>{ handleFinalOpError(err)})

可见try-catch和return实际上已经以其原本面貌回到了代码中,在代码形式上也已经看不到异步风格的痕迹。

类似的功能有co/task.js等库实现。

4、ES7的async/await

ES7中将会引入async function和await关键字,利用这个功能,我们可以轻松写出同步风格的代码,

同时依然可以利用原有的异步I/O机制。

采用async function,我们可以将之前的代码写成这样:

?

12345678910111213

async function getResult() { let res, a, b, c, dtry { res = await opA(a, b)res = await opB(c, res)res = await opC(d)return res} catch (err) { return handleError(err)}} getResult()

和Generator &Promise方案看起来没有太大区别,只是关键字换了换。

实际上async

function就是对Generator方案的一个官方认可,将之作为语言内置功能。

async function的缺点:

await只能在async function内部使用,因此一旦你写了几个async function,或者使用了依赖于async

function的库,那你很可能会需要更多的async function。

目前处于提案阶段的async

function还没有得到任何浏览器或Node.JS/io.js的支持。Babel转码器也需要打开实验选项,并且对于不支持Generator的浏览器来说,还需要引进一层厚厚的regenerator

runtime,想在前端生产环境得到应用还需要时间。

以上就是本文的全部内容,希望对大家的学习有所帮助。

<head>

<meta http-equiv="Content-Type" content="text/htmlcharset=utf-8" />

<title>禅棋传说</title>

<style type="text/css">

div { position: absolutewidth: 23pxheight: 23px}

.B0 { background-image: url('B0.gif')}

.B1 { background-image: url('B1.gif')}

.B2 { background-image: url('B2.gif')}

.B3 { background-image: url('B3.gif')}

.B4 { background-image: url('B4.gif')}

.B5 { background-image: url('B5.gif')}

.B6 { background-image: url('B6.gif')}

.B7 { background-image: url('B7.gif')}

.B8 { background-image: url('B8.gif')}

.BX { background-image: url('BX.gif')}

.D0 { background-image: url('D0.gif')}

.D1 { background-image: url('D1.gif')}

.C0 { background-image: url('C0.gif')}

.C1 { background-image: url('C1.gif')}

</style>

</head>

<body>

<script type="text/javascript">

//<![CDATA[

Array.prototype.indexOf = function (item) //给数组扩展一个indexOf方法,用来检索是否为"已知元素"

{

for ( var i=0i<this.lengthi++)

if (this[i] == item)

return i

return -1

}

var Site = //定义一个棋位类

{

Create: function(x, y) //棋位类的构造函数

{

var me = document.createElement("div")//建一个div对象,将其扩展并封装成棋位。

document.body.appendChild(me) //附加到DOM树,实现棋位的呈现。

me.x = x //记录棋位的X坐标

me.y = y //记录棋位的Y坐标

me.style.left = x * 23 + "px" //设置棋位水平方向的绝对位置

me.style.top = y * 23 + "px" //设置棋位垂直方向的绝对位置

//var s = ((x-9)%9?0:(x-9)/9)+1+(((y-9)%9?0:(y-9)/9)+1)*3 //计算背景式样,这个算法有冗余,简化为下面的算法.

var s = parseInt((x-9)/9)+1+(parseInt((y-9)/9)+1)*3 //计算背景式样

//me._backStyle = "B" + ((s==4&&(x/3)%2==1&&(y/3)%2==1) ? "X" : s)

me._backStyle = "B" + (((x/3)%2==1&&(y/3)%2==1) ? "X" : s)

me.Fill = this.Fill //关联一个填充棋位的方法。

me.Tight = this.Tight //关联计算紧气方法。

me.Kill = this.Kill //关联计算死子方法。

me.onclick = this.Play//绑定onclick事件到Play方法。

me.Fill() //初始填充空子。

return me //返回棋位对象,其实是一个封装了的div对象。

},

Fill: function(dot, going) //填充棋子的方法,going~~是否"当前步".

{

if ( dot == undefined )

this.className = this._backStyle//无子,就设置为背景式样。

else

this.className = (going ? "C" : "D") + dot //有子,区别对待"当前步"

this.dot = dot//保存棋子状态

},

Play: function()//行棋方法,由onclick事件触发

{

if ( this.dot == undefined ) //落子点必须为无子状态,否则不处理.

{

var deads = this.Kill(current^1)//计算可以杀死的子,current为当前颜色,则current^1为对方颜色

if (deads.length == 1 &&this == rob) return//确认的打劫状态后,直接拒绝如果此步落子可以提不止一子,则不是打劫

for(var i=0i<deads.lengthi++)

deads[i].Fill() //按照死子列清空,one by one

if(i==1)

rob = deads[0]//如果此步只提了一个子,记录被提位置为打劫位置.

else if (i>0 || !this.Tight(current)) //这里有一个影响代码效率的判断,对于能产生提子的操做,不需要再判断是否"紧气禁入"此外,如果要引入"变穷为禁"的规则,这里需要改变算法

rob = null//清打劫位,这个条件是判断,如果能够提多子,或者不是禁止着手,则解除"打劫"状态

else return //这里对应的返回,应该是只有无气禁入点的情况.

sound.play() //落子有声!

var step = Tracks[Tracks.length-1]

if(step) step.site.Fill(step.site.dot) //更新此前一"步"的子的位图

this.Fill(current, true)//填入当前"步"的子

Tracks.push( new Step(this, deads) )

current ^= 1 //用1来异或,正好反转黑白棋子。

var disline = document.getElementById('list')

disline.value += ((current ? '黑:': '白:')+Tracks[Tracks.length-1].site.x+' '+Tracks[Tracks.length-1].site.y+'\n')

disline.scrollTop = disline.scrollHeight

}

},

Tight: function (dot) //计算紧气的块,此时如果dot==undefined,则对应提子的遍历,this指向当前点.

{

var life = this.dot == undefined ? this : undefined//life为"气"的定义当前位无子则算一口气,对应落子的遍历,当前位置必然无子.

dot = dot == undefined ? this.dot : dot//这个逻辑是对应提子的遍历

if (dot == undefined) return undefined

var block = this.dot == undefined ? [] : [this]//定义的"块",在提子的遍历中,发现子就放入块,再判断有多少"气".

var i = this.dot == undefined ? 0 : 1

var site = this

while (true)

{

for(var dx=-1dx<=1dx++) for(var dy=-1dy<=1dy++) if(!dx^!dy)

{

link = GetSite(site.x + dx, site.y + dy)

if (link) //判断目标位置的上下左右,有位则继续,"无位"对应棋盘以外的区域.

if (link.dot != undefined) //有子,则判断是否为同色,连"块"

{

if (link.dot == dot &&block.indexOf(link) <0 )

block.push(link) //此一段为"块"的遍历,条件是找四周的同色子,找到后判断是否为新"知道","新"则放入"块"

}

else if (!life) //无子,则更新"气"

life = link

else if (life != link)

return undefined //在提子的遍历中,如果发现有两"气"了,则无须再算

}

if ( i >= block.length) break //"块"的遍历结束条件为,列表的最后一个对象没有产生新的相邻子.

site = block[i]

i ++

}

return block //返回只有一口气的块,在提子的遍历中,这对应了可能被提掉的子列,再去判断是否为"打劫"

},

Kill: function(dot) //计算杀死的子,目前this指向落子点,dot指向需要判断的死子颜色.

{

var deads = []//定义死子列

for(var dx=-1dx<=1dx++) for(var dy=-1dy<=1dy++) if(!dx^!dy) //异或,有且只有一个为真,可对应到相邻子.

{

var site = GetSite(this.x + dx, this.y + dy)

if (site &&(site.dot == dot))

{

var block = site.Tight() //分别对上下左右进行遍历,查找被紧气的"块",然后合并成为"死子列"

if (block) deads = deads.concat(block)//concat 合并操作,此操作并未识别"同项",仅以围棋规则判断,对"打劫没有影响".

//如果需要准确的提子数目统计,这里的算法需要更新.

}

}

return deads //返回可以提子的死子块

}

}//棋位类 Site

var Board = new Array(19) //全局的Board数组,表示棋盘。

var Tracks = [] //行棋线索数组,数组元素是Step对象。

var current = 0 //当前要下的子,0表示黑子,1表示白子,交替。

var rob = null//如果有打劫时,记录打劫位置。

for(var x = 0 x <19x++)

{

Board[x] = new Array(19)

for(var y = 0y <19y++)

Board[x][y] = Site.Create(x, y) //按位置创建棋位对象。

}

if (navigator.userAgent.indexOf(' MSIE ') >-1) //为IE浏览器构造声音对象

{

var sound = document.body.appendChild(document.createElement("bgsound"))

sound.play = function(){this.src = "play.wav"}

}

else//为Firefox等其他浏览器构造声音对象

{

var sound = document.body.appendChild(document.createElement("span"))

sound.play = function(){this.innerHTML = "<bgsound src='play.wav'>"}

}

document.body.oncontextmenu = function() //右键驱动悔棋事件

{

var step = Tracks.pop()

if (step)

{

step.site.Fill()

for (var i=0i<step.deads.lengthi++)

step.deads[i].Fill(current)

step = Tracks[Tracks.length-1]

if (step) step.site.Fill(current, true)

current ^= 1 //反转黑白棋子。

}

return false //不弹出菜单。

}

function GetSite(x, y) //从棋盘取棋位的函数,越界不抛出异常。

{

if (x>=0 &&x<19 &&y>=0 &&y<19)

return Board[x][y]

}

function Step(site, deads) //棋步类,记录每一步棋的状态

{

this.site = site //记录棋步的位置

this.deads = deads//记录被当前棋步杀死的棋子集合

}

function PrintWay() //行棋路线

{

var str='', coler=''

for (var i=0i <Tracks.lengthi++)

{

step = Tracks[i]

coler = (i%2) ? "白" : "黑"

str=str+"第"+(i+1)+"步"+coler+"方 X"+step.site.x+" Y"+step.site.y+" \n"

}

alert(str)

}

document.body.ondblclick = PrintWay

document.onkeypress = function(event)

{

var k = (window.event ? window.event.keyCode : event.which) - 49//按'1'可以进入自动摆棋操作,'1'=0x31=49

if(k<0 || k>1) return

for(var x=0x<19x++) for(var y=0y<19y++) Board[x][y].Fill()

Tracks.length = 0

current = 0

with(goes[k]) for(var i=0i<lengthi+=3)

Board[charCodeAt(i+1)-65][charCodeAt(i)-65].Fill(charCodeAt(i+2)-48)

}

</body>

</html>