第八章:物件導向程式設計 (55%) (fango)

fango發表於2012-06-22

九十年代初,物件導向程式設計這事兒在軟體工業騷動起來。其背後的主義在當時都不是新的了,但終於它們還是憋足了勁兒,滾了起來,變得新潮了。很多書出了,課教了,程式語言開發了。突然之間,每個人都歌頌起物件導向的美好,激情澎湃地把它應用於所有問題,滿懷信心地走向他們最終發現的編寫程式的正確道路。

這些事兒發生不止一次了。當一個過程艱鉅費解,人總會留意神奇的解法。當類似那種解法的東西出現時,人已經做好準備成為信徒了。對許多程式設計師來說,即便是到了今天,物件導向(或是他們的看法),就是福音。當一個程式不是‘真正的面朝物件’,不管那代表什麼,它一定是被視為低階。

可還真沒幾個風潮能象它俗這麼久。物件導向的萬壽無疆大抵可以解釋為它的核心主義還是非常堅實有用的。本章,我們就討論這些主義,以及Javascript的採用(相當離經叛道)。上面的言論沒有一點羞辱這些主義的意思。我只是想提醒讀者,不要對它們發展出不健康的依附。


正如其名,物件導向和物件相關。至此,我們用物件來鬆散地聚集一些值,隨意地新增和更改它們的屬性。按物件導向的方式,物件被視為自己的小世界,而外部世界和它們接觸,只能通過有限的仔細定義的介面,就是一些特定的方法和屬性。我們在第七章結束使用的‘接觸名單’就是一例:我們只用三個函式和它交流:makeReachedList, storeReached, 和 findReached。這三個函式構成了此物件的介面。

我們見過的Date, Error, 和BinaryHeap物件也這樣工作。它們並未提供工作於這樣物件上的普通函式,而是提供了一個使用關鍵字new生成物件的方式,以及一系列的方法與屬性作為餘下的介面。


在物件上附一個函式值就可以給它一個方法。

var rabbit = {};
rabbit.speak = function(line) {
  print("The rabbit says '", line, "'");
};

rabbit.speak("Well, now you're asking me.");

大多情況下,方法需要知道它是誰用的。例如,如果有不同的兔子(rabbit),則speak方法就必須指出是哪隻兔子在發言(speak)。為此目的,有個特殊變數叫this,當函式被呼叫時它總在場,並指著此函式作為方法呼叫時的那個相關物件。一個函式作為屬性被查詢時,它是作為方法呼叫的,並被立即呼叫,就象object.method()。

function speak(line) {
  print("The ", this.adjective, " rabbit says '", line, "'");
}
var whiteRabbit = {adjective: "white", speak: speak};
var fatRabbit = {adjective: "fat", speak: speak};

whiteRabbit.speak("Oh my ears and whiskers, how late it's getting!");
fatRabbit.speak("I could sure use a carrot right now.");


現在我可以澄清apply方法的神祕的首參量了,第6章時我們一直用null的那個。這個變數可以用來指定函式必須應用於的物件。這對於非方法的函式是沒有用到的,所以放個null。

speak.apply(fatRabbit, ["Yum."]);

函式也有call方法,類似apply,但你可以單獨列出函式的參量,而不必用一個陣列:

speak.call(fatRabbit, "Burp.");


關鍵字new提供了生成新物件的快捷方式。如果一個函式呼叫前有個new,則它的this變數就指向一個新物件,而且自動被返回(除非它明確返回別的什麼)。這種生成新物件的函式成為創構函式。這裡是兔子們的創構函式(constructor):

function Rabbit(adjective) {
  this.adjective = adjective;
  this.speak = function(line) {
    print("The ", this.adjective, " rabbit says '", line, "'");
  };
}

var killerRabbit = new Rabbit("killer");
killerRabbit.speak("GRAAAAAAAAAH!");

Javascript程式設計師的一個常規是創構函式名的首字母大寫。這樣容易區分它們和其它函式。

為什麼需要關鍵字new?畢竟,我們可以簡單地這樣寫:

function makeRabbit(adjective) {
  return {
    adjective: adjective,
    speak: function(line) {/*etc*/}
  };
}

var blackRabbit = makeRabbit("black");

可這並不全然相同。new在背後做了一些事。之一,我們killerRabbit有個屬性叫constructor,指向生成它的Rabbit函式。blackRabbit也有這個屬性,但指向的是Object函式。

show(killerRabbit.constructor);
show(blackRabbit.constructor);

屬性constructor從何而來?它是兔子原型(prototype)的一部分。儘管有些費解,原型是Javascript物件強有力的工作方式之一。每個物件都基於原型,從而獲得一組繼承的屬性。我們至今用的簡單物件,都基於最基本的原型,它是和Object創構函式相關聯的。其實,鍵入{}就等價與鍵入new Object()。

var simpleObject = {};
show(simpleObject.constructor);
show(simpleObject.toString);

toString是Object原型的方法之一。這意味著所有簡單物件都有toString方法,用來把自己轉化為一個字串。我們的兔子物件是基於與Rabbit創構函式相關聯的原型。你可以使用一個創構函式的prototype屬性來存取,嗯,它們的原型(prototype)。

show(Rabbit.prototype);
show(Rabbit.prototype.constructor);

每個函式都自動得到一個prototype屬性,它的constructor屬性指回這個函式。因為兔子原型自身是一個物件,它基於Object原型,並共享其toString方法。

show(killerRabbit.toString == simpleObject.toString);

儘管物件好像和它們的原型共享屬性,可實際上是單向的。原型的屬性影響基於它的物件,但物件的屬性從來不會更改原型。

此精密規則是:查詢屬性值時,Javascript首先檢視此物件本身所帶的屬性。如果有我們要找的屬性名,則得到其值。如果無此名,則會檢視此物件原型的屬性,然後是此原型的原型,如此這般。如果還是沒有找到,就得出undefined(未定義)的值。另一方面,當設定一個屬性值時,Javascript從不看原型,總是設定此物件自身的屬性。

Rabbit.prototype.teeth = "small";
show(killerRabbit.teeth);
killerRabbit.teeth = "long, sharp, and bloody";
show(killerRabbit.teeth);
show(Rabbit.prototype.teeth);

這確實意味著原型可以隨時新增一個新的屬性和方法給所有基於它的物件。例如,可能需要讓兔子跳舞:

Rabbit.prototype.dance = function() {
  print("The ", this.adjective, " rabbit dances a jig.");
};

killerRabbit.dance();

就像你猜到的,兔子原型是放所有兔子共同值的絕佳位置,例如speak方法。這裡是Rabbit創構函式的新方案:

function Rabbit(adjective) {
  this.adjective = adjective;
}
Rabbit.prototype.speak = function(line) {
  print("The ", this.adjective, " rabbit says '", line, "'");
};

var hazelRabbit = new Rabbit("hazel");
hazelRabbit.speak("Good Frith!");

所有物件都有原型並從這個原型取得一些屬性的事實其實挺繞人的。這意味著像第四章的貓那種使用物件儲存一套東西,可能會出錯。例如,如果我們想知道是否有隻貓叫‘constructor’,我們會這樣檢查:

var noCatsAtAll = {};
if ("constructor" in noCatsAtAll)
  print("Yes, there definitely is a cat called 'constructor'.");

這挺成問題的。一個相關問題是有時需要擴充套件標準創構函式的原型,像給Object和Array加入新的有用函式。例如我們可以給所有物件一個叫properties的方法,來返回一個陣列,包含那個物件所有(未隱藏)的屬性:

Object.prototype.properties = function() {
  var result = [];
  for (var property in this)
    result.push(property);
  return result;
};

var test = {x: 10, y: 3};
show(test.properties());

此時問題立刻出現了。現在Object原型有了個properties屬性,使用for和in迴圈過所有物件的屬性,也會給我們那個共享的屬性,這通常不是我們希望的。我們只關心物件本身的屬性。

萬幸,有個方式可以找出一個屬性是屬於此物件自身,還是屬於它的某一個原型。不幸,它會讓迴圈過一個物件的屬性更難看些。每個物件都有個方法稱為hasOwnProperty,會告訴我們一個物件是否有給定名字的屬性。這樣,我們可以重寫properties方法:

Object.prototype.properties = function() {
  var result = [];
  for (var property in this) {
    if (this.hasOwnProperty(property))
      result.push(property);
  }
  return result;
};

var test = {"Fat Igor": true, "Fireball": true};
show(test.properties());

當然,我們可以把它抽象到一個高階函式。注意action函式呼叫時同時帶有屬性名及其物件的值。

function forEachIn(object, action) {
  for (var property in object) {
    if (object.hasOwnProperty(property))
      action(property, object[property]);
  }
}

var chimera = {head: "lion", body: "goat", tail: "snake"};
forEachIn(chimera, function(name, value) {
  print("The ", name, " of a ", value, ".");
});

但,(你沒法知道),如果我們發現有隻貓就叫hasOwnProperty,怎麼辦?它會存放在物件裡,然後下次我們要檢視貓咪收藏時,呼叫object.hasOwnProperty就會失敗,因為那個屬性已經不再指向一個函式值了。這也可以解決,只是還要再難看些:

function forEachIn(object, action) {
  for (var property in object) {
    if (Object.prototype.hasOwnProperty.call(object, property))
      action(property, object[property]);
  }
}

var test = {name: "Mordecai", hasOwnProperty: "Uh-oh"};
forEachIn(test, function(name, value) {
  print("Property ", name, " = ", value);
});

(注:此例不可正確用於IE8,似其置換內部原型屬性存在問題。)

這裡,與其使用從物件自身找到的方法,我們從Object原型得到方法,然後使用call施用在正確的物件上。除非真有人鼓弄Object.prototype的方法(萬萬不可),這應工作正確。


hasOwnProperty也可以使用的場合,是我們使用in操作符檢視一個物件是否有特定的屬性。只是有個地方要小心。我們在第四章看到一些屬性,例如toString,是‘隱藏’的,不會在使用for/in迴圈時出現。這是由於Gecko族的瀏覽器(最出名的是Firefox),給每個物件一個暗藏的屬性,叫__proto__,指向此物件的原型。對此屬性hasOwnProperty會返回true,儘管程式並沒有明確地新增它。能夠存取一個物件的原型會非常方便,可是像這樣把它做成一個屬性,就不是什麼好主意了。Firefox仍然是廣泛使用的瀏覽器,所以你要為web程式設計就需要小心這一點。有個方法propertyIsEnumerable,會對隱藏屬性返回false,可以用來剔除像__proto__這種奇怪的東西。使用這樣的表示式能可靠地應付:

var object = {foo: "bar"};
show(Object.prototype.hasOwnProperty.call(object, "foo") &&
     Object.prototype.propertyIsEnumerable.call(object, "foo"));

又好看又簡單,是不是?這是Javascript設計的不是很好的一面。物件一人分扮兩角:‘帶方法的值’,原型乾得很好,’一套屬性‘,原型只會擋道。


每次你要檢視一個屬性是否在物件裡,都要寫出上面的表示式,這沒法幹活了。我們可以放它進一個函式,但更好的方案是寫個創構函式,以及一個專為這種事準備的原型,也就是我們只想把物件作為一套屬性。因為你可以按名查詢,我們就叫它字典(Dictionary)。

function Dictionary(startValues) {
  this.values = startValues || {};
}
Dictionary.prototype.store = function(name, value) {
  this.values[name] = value;
};
Dictionary.prototype.lookup = function(name) {
  return this.values[name];
};
Dictionary.prototype.contains = function(name) {
  return Object.prototype.hasOwnProperty.call(this.values, name) &&
    Object.prototype.propertyIsEnumerable.call(this.values, name);
};
Dictionary.prototype.each = function(action) {
  forEachIn(this.values, action);
};

var colours = new Dictionary({Grover: "blue",
                              Elmo: "orange",
                              Bert: "yellow"});
show(colours.contains("Grover"));
show(colours.contains("constructor"));
colours.each(function(name, colour) {
  print(name, " is ", colour);
});

這樣把物件就作為一套屬性的那堆亂麻,就被‘封裝’進一個方便的介面:一個創構函式和四個方法。注意Dictionary物件的values屬性不屬於這個介面,它是內部細節,當你使用Dictionary物件時你不需要直接用到它。

每次你寫介面時,最好加個註釋勾勒一下它作什麼以及怎樣使用。這樣,當別人想用這個介面,他們可以一眼得知怎樣用它,而不需要去研究整個程式。這個別人可能就是寫完三個月之後的你。

大部分時候,你設計介面時,不論怎樣你都會很快發現一些侷限與問題,然後加以修改。不要浪費時間,你最好是在介面真正用在實際情況並證明有效之後,才作此介面的文件。當然,這誘使人完全忘記作文件。我個人而言,寫文件是對一個系統作‘最後修飾’。當它覺得可以了,就是時間寫些什麼了,並且看看是不是英語(或別的語言)寫的和Javascript(或別的程式語言)寫的一樣好。


區分一個物件的外層介面和內部細節是非常重要的。原因有二。其一,一個小而清晰描述的介面使物件更容易使用。你只要腦子裡裝著介面,而不需要擔心其他,除非你是在改這樣物件自身。

其二,有時確有必要,或實際需要修改一個物件型別的內部實現,例如,要提高效率,或者修正一些問題。如果外部的程式碼在使用此物件的每一個屬性和細節,你就無法作任何改動而不去修改大量的其它程式碼。如果外部程式碼只是使用一小點介面,你就可以為所欲為,只需保持介面不變。

一些人這方面走得很遠。他們會,例如,從不在物件的介面裡包括屬性,而只有方法。如果他們的物件型別有個長度,這需要用getLength方法得到,而不是用length屬性。這樣,如果他們需要修改物件,不再具有length屬性,例如由於它現在有些內部陣列的長度必須返回,他們可以更改此函式而不必改動介面。

我個人觀點是大部分情況這樣不值。加個getLength方法而它只帶return this.length幾乎就是加些無意義的程式碼,而大部分情況下,我認為比起偶而改改我的物件的介面,無意義的程式碼是更大的問題。


給現有原型新增新方法真是非常方便。特別是Javascript的Array和String原型可以多用幾個基本方法。例如,我們可以使用陣列的方法來代替forEach和map,並把第4章我們寫的startsWith函式作為字串的一個方法。

可是,如果你的程式所在的同一個網頁上必須一起執行其它的程式(不管是你還是其它人所寫),而它天真地使用了for/in——就是我們一直在用的那樣——則在原型裡新增東西,特別是Object和Array的原型,肯定會毀掉點什麼,因為迴圈突然之間看到了這些新屬性。基於此原因,一些人寧願從不碰這些原型。當然,如果你仔細,而且你不認為你的程式碼必須和寫的不好的程式碼一起用,給標準原型新增方法是個絕好的技術。


本章我們會打造一個虛擬的生態箱(Terrarium),一個蟲子們爬來爬去的箱子。這會涉及到一些東西(畢竟本章是講對著東西程式設計)。我們採用一個很簡單的方案,使生態想成為二維的網格,像第7章的第二張圖那樣。這個網格里有一些蟲子。當生態箱運作時,所有蟲子都有機會每半分鐘動一動,例如移移位。

這樣,我們把時間空間都剁成固定尺寸的單元——空間是方格,時間是半秒。這使得程式通常比較容易設計,當然缺點是太不精密。還好,此生態箱模擬器無需精準,所以我們就將就著吧。


生態箱可以用一個‘平面圖’定義,就是一個字串陣列。我們可以用一個字串,但因為Javascript的字串必須只佔一行,就很難鍵入了。

var thePlan =
  ["############################",
   "#      #    #      o      ##",
   "#                          #",
   "#          #####           #",
   "##         #   #    ##     #",
   "###           ##     #     #",
   "#           ###      #     #",
   "#   ####                   #",
   "#   ##       o             #",
   "# o  #         o       ### #",
   "#    #                     #",
   "############################"];

‘#’字元用來代表生態箱的牆(以及鋪著的裝飾石子),‘o'表示蟲子,空格是,你猜的對,空格子。

此圖陣列可用來生成一個生態箱物件。此物件一直跟蹤生態箱的形態和內容,並讓蟲子在裡面移動。它有四個方法:先是toString,將生態箱轉換回類似它基於的平面圖字串,從而我們能看到裡面在幹什麼。然後是step,使蟲子們可以移動一步——如果它們希望。最後,是start和stop,來控制生態箱是否要’執行‘。執行時,step每個半秒自動呼叫一次,所以蟲子們一直在動。


Ex.8.1,網格中的點也可以表示為物件。在第7章,我們使用了三個函式,point,addPoints和samePoint來處理點。此時,我們要使用一個創構函式和兩個方法。寫這個創構函式Point,給定兩個參量,點的x和y座標,生成一個帶有x和y屬性的物件。給這個創構函式的原型一個方法add,取另一點為參量,返回一個新的點,其x和y是這兩個給定點的x和y的和。再加一個方法isEqualTo,取一點並返回一個布林值,指出this點是否和給定點在同一座標。 除了這兩個方法,x和y屬性也是這種型別物件的介面的一部分:使用點物件的程式碼可以隨意取得和修改x和y。


當寫物件來實現某個特定的程式時,不是總能清楚哪個函式放在哪裡。一些東西最好能寫為物件的方法,而另一些表達為單獨的函式比較恰當,還有一些則最好能夠靠一個新的物件型別實現。要保持事物清晰有條理,重要的是保持一個物件型別的方法於職責越少越好。如果一個物件做太多事,它就成了一大堆函式亂麻,一種可怕的困惑之源。

我上面說過生態箱物件負責存放內容並讓裡面的蟲子移動。首先,注意是‘讓’它們移動,而不是‘使’它們移動。蟲子們也是物件,是這些物件自己負責決定該做什麼。生態箱僅僅是提供一個基礎設施,告訴它們每半秒該做什麼,如果它們決定移動,就確保可以發生。

存放生態箱儲存內容的網格會相當複雜。這需要定義一種表示形式,以及存取這個表示形式的方式,還要能從‘平面圖’陣列初始化網格,把網格內容用toString寫入一個字串,以及網格中蟲子們的移動。最好能把其中的一些放入另一個物件,從而使生態箱物件自身不會太大太複雜。


每當你發現自己在一個物件裡混合了資料表現與特定問題的程式碼的時候,最好把資料表現放入一個單獨的物件。此處,我們需要表現一個網格的數值,因此我們寫個Grid型別,來支援生態箱所需的操作。

存放網格的數值,可以有兩個選擇。一是使用陣列的陣列,像這樣:

var grid = [["0,0", "1,0", "2,0"],
            ["0,1", "1,1", "2,1"]];
show(grid[1][2]);

或者把數值放入一個陣列。此時,x和y所在的單元可以使用x + y * width算出其陣列中的位置,而width是網格的寬度。

var grid = ["0,0", "1,0", "2,0",
            "0,1", "1,1", "2,1"];
show(grid[2 + 1 * 3]);

我選了第二個表示法,因為它讓初始化陣列非常容易。new Array(x)生成一個長度為x的新陣列,填滿了undefined(未定義)的值。

function Grid(width, height) {
  this.width = width;
  this.height = height;
  this.cells = new Array(width * height);
}
Grid.prototype.valueAt = function(point) {
  return this.cells[point.y * this.width + point.x];
};
Grid.prototype.setValueAt = function(point, value) {
  this.cells[point.y * this.width + point.x] = value;
};
Grid.prototype.isInside = function(point) {
  return point.x >= 0 && point.y >= 0 &&
         point.x < this.width && point.y < this.height;
};
Grid.prototype.moveValue = function(from, to) {
  this.setValueAt(to, this.valueAt(from));
  this.setValueAt(from, undefined);
};

Ex.8.2,我們也需要走過網格的所有單元,找到需要移動的蟲子們,或把整個東西換成字串。為了簡單,我們可以使用一個高階函式取一個行動作為參量。把方法each加入原型Grid,把一個使用兩個參量的函式作為參量。對網格的每個點呼叫此函式,使用那一點的點物件作為第一個參量,而那一點在網格上的值作為第二個參量。 從0,0的點開始,每次一行,走過所有點,這樣1,0是在0,1前處理。這會使後面寫生態箱的toString函式容易些。(提示,放x座標的迴圈在y座標的迴圈的內部)。 不建議直接鼓弄網格物件的cells屬性,而是使用valueAt來得到此值。這樣,如果我們決定(出於某些原因)使用一個不同的方法來儲存這些值,我們只需重新寫valueAt和setValueAt,其它的方法都可以不動。


最後,要測試網格:

var testGrid = new Grid(3, 2);
testGrid.setValueAt(new Point(1, 0), "#");
testGrid.setValueAt(new Point(1, 1), "o");
testGrid.each(function(point, value) {
  print(point.x, ",", point.y, ": ", value);
});

我們可以開始寫Terrarium創構函式之前,我們需要多明確一下這些居住在裡面的‘蟲子物件’。之前,我提過生態箱會問蟲子們它們想怎樣。這將如下工作:每隻蟲子都有個act方法,呼叫時返回一個‘行動’。一個行動是一個帶type屬性的物件,此屬性命名蟲子所採取的行動型別,例如‘move’。大部分行動,也包含附加資訊,例如蟲子要去的方向。

蟲子都是大近視,只能看到網格中緊挨著它們的幾個方格。但這些就使它們可以參照行動。當act方法呼叫時,會給它一個物件,帶有此蟲周圍的資訊。這八個方向每個都有一個屬性。此屬性指出在蟲子上方的是‘n',代表北(North),右上方的是’ne',代表東北(North-East),等等。需要查詢這些名字所指方向時,下面的字典物件會有用:

var directions = new Dictionary(
  {"n":  new Point( 0, -1),
   "ne": new Point( 1, -1),
   "e":  new Point( 1,  0),
   "se": new Point( 1,  1),
   "s":  new Point( 0,  1),
   "sw": new Point(-1,  1),
   "w":  new Point(-1,  0),
   "nw": new Point(-1, -1)});

show(new Point(4, 4).add(directions.lookup("se")));

當一隻蟲子要移動時,他靠給出結果行動物件一個代表這些方向之一的direction屬性來指出他所希望移動的方向。我們可以使一隻簡單,愚笨的蟲子總是向南走,’朝光‘,像這樣:

function StupidBug() {};
StupidBug.prototype.act = function(surroundings) {
  return {type: "move", direction: "s"};
};

現在我們可以開始做Terrarium物件型別自己了。首先是它的創構函式,用給定的平面圖參量(一個字串陣列),初始化網格。

var wall = {};

function Terrarium(plan) {
  var grid = new Grid(plan[0].length, plan.length);
  for (var y = 0; y < plan.length; y++) {
    var line = plan[y];
    for (var x = 0; x < line.length; x++) {
      grid.setValueAt(new Point(x, y),
                      elementFromCharacter(line.charAt(x)));
    }
  }
  this.grid = grid;
}

function elementFromCharacter(character) {
  if (character == " ")
    return undefined;
  else if (character == "#")
    return wall;
  else if (character == "o")
    return new StupidBug();
}

wall是個物件,用來標記網格上牆的位置。真正牆該作的,是什麼都不去作,就坐著佔個座。


生態箱物件最簡單的方法是toString,來把一個生態箱轉換為一個字串。為了更簡單,我們讓wall和StupidBuf的原型都標記一個屬性character,用來持有代表它們的字元。

wall.character = "#";
StupidBug.prototype.character = "o";

function characterFromElement(element) {
  if (element == undefined)
    return " ";
  else
    return element.character;
}

show(characterFromElement(wall));

Ex.8.3,現在我們可以用Grid物件的each方法搭建一個字串了。可為了使結果可讀,最好在每個行尾加個換行符。網格的x座標位置可以拿來決定是否到了行尾。加一個方法toString,不用參量,返回一個字串,當用於print時,顯示整齊的生態箱二維圖。


有可能在嘗試解答上面練習時,你想要存取作為網格的each方法的參量傳入的函式中的this.grid。這行不通。呼叫一個函式每次都會導致一個新的this定義在此函式的內部,即使它不是作為方法使用的。因此,函式內的this變數是對外不可見的。

這有時可以直接繞過,只要把所需的資訊存放在一個變數裡,例如endOfLine,在內層函式裡可見。如果需要存取整個this物件,你也是可以把它存入一個變數。通常此變數名是self(或that)。

可這些多出來的變數會變得很亂。另一個不錯的方案是使用類似第6章的partial那種函式。不過不是給函式新增參量,而是加一個this物件,使用函式的apply方法的第一個參量:

function bind(func, object) {
  return function(){
    return func.apply(object, arguments);
  };
}

var testArray = [];
var pushTest = bind(testArray.push, testArray);
pushTest("A");
pushTest("B");
show(testArray);

這樣,你可以bind(繫結)一個內層函式到this,而它是和外層函式同樣的this。


Ex.8.4,表示式bind(testArray.push, testArray)中名字testArray仍舊出現兩次。你是否可以設計一個函式method,來繫結一個物件到它的某個方法,而不需命名物件兩次?


我們需要bind(或method)來實現生態箱的step方法。此方法需要走遍網格中的每隻蟲子,詢問它們的行動,然後執行那個行動。可能你會想用each在網格上,然後碰上蟲子就處理。可是如果一隻蟲走向南或東,我們在同一遍會再次遇到它,並又允許它移動。

相關文章