摘要:也就是說,所有的函數(shù)和構(gòu)造函數(shù)都是由生成,包括本身。如果只考慮構(gòu)造函數(shù)和及其關(guān)聯(lián)的原型對(duì)象,在不解決懸念的情況下,圖形是這樣的可以看到,每一個(gè)構(gòu)造函數(shù)和它關(guān)聯(lián)的原型對(duì)象構(gòu)成一個(gè)環(huán),而且每一個(gè)構(gòu)造函數(shù)的屬性無所指。
前言
JavaScript 是我接觸到的第二門編程語言,第一門是 C 語言。然后才是 C++、Java 還有其它一些什么。所以我對(duì) JavaScript 是非常有感情的,畢竟使用它有十多年了。早就想寫一篇關(guān)于 JavaScript 方面的東西,但是在博客園中,寫 JavaScript 的文章是最多的,從入門的學(xué)習(xí)筆記到高手的心得體會(huì)一應(yīng)俱全,不管我怎么寫,都難免落入俗套,所以遲遲沒有動(dòng)筆。另外一個(gè)原因,也是因?yàn)樵?Ubuntu 環(huán)境中一直沒有找到很好的 JavaScript 開發(fā)工具,這種困境直到 Node.js 和 Visual Studio Code 的出現(xiàn)才完全解除。
十多年前,對(duì) JavaScript 的介紹都是說他是基于對(duì)象的編程語言,而從沒有哪本書會(huì)說 JavaScript 是一門面向?qū)ο蟮木幊陶Z言?;趯?duì)象很好理解,畢竟在 JavaScript 中一切都是對(duì)象,我們隨時(shí)可以使用點(diǎn)號(hào)操作符來調(diào)用某個(gè)對(duì)象的方法。但是十多年前,我們編寫 JavaScript 程序時(shí),都是像 C 語言那樣使用函數(shù)來組織我們的程序的,只有在論壇的某個(gè)角落中,有少數(shù)的高手會(huì)偶爾提到你可以通過修改某個(gè)對(duì)象的prototype來讓你的函數(shù)達(dá)到更高層次的復(fù)用,直到 Flash 的 ActionScript 出現(xiàn)時(shí),才有人系統(tǒng)介紹基于原型的繼承。十余年后的現(xiàn)在,使用 JavaScript 的原型鏈和閉包來模擬經(jīng)典的面向?qū)ο蟪绦蛟O(shè)計(jì)已經(jīng)是廣為流傳的方案,所以,說 JavaScript 是一門面向?qū)ο蟮木幊陶Z言也絲毫不為過。
我喜歡 JavaScript,是因?yàn)樗浅>哂斜憩F(xiàn)力,你可以在其中發(fā)揮你的想象力來組織各種不可思議的程序?qū)懛?。也許 JavaScript 語言并不完美,它有很多缺陷和陷阱,而正是這些很有特色的語言特性,讓 JavaScript 的世界出現(xiàn)了很多奇技淫巧。
回到頂部
對(duì)象和原型鏈
JavaScript 是一門基于對(duì)象的編程語言,在 JavaScript 中一切都是對(duì)象,包括函數(shù),也是被當(dāng)成第一等的對(duì)象對(duì)待,這正是 JavaScript 極其富有表現(xiàn)力的原因。在 JavaScript 中,創(chuàng)建一個(gè)對(duì)象可以這么寫:
var someThing = new Object();
這和在其它面向?qū)ο蟮恼Z言中使用某個(gè)類的構(gòu)造函數(shù)創(chuàng)建一個(gè)對(duì)象是一模一樣的。但是在 JavaScript 中,這不是最推薦的寫法,使用對(duì)象字面量來定義一個(gè)對(duì)象更簡潔,如下:
var anotherThing = {};
這兩個(gè)語句其本質(zhì)是一樣的,都是生成一個(gè)空對(duì)象。對(duì)象字面量也可以用來寫數(shù)組以及更加復(fù)雜的對(duì)象,這樣:
var weekDays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
這樣:
var person = {
name : "youxia", age : 30, gender : "male", sayHello : function(){ return "Hello, my name is " + this.name; }
}
甚至這樣數(shù)組和對(duì)象互相嵌套:
var workers = [{name : "somebody", speciality : "Java"}, {name : "another", speciality : ["HTML", "CSS", "JavaScript"]}];
需要注意的是,對(duì)象字面量中的分隔符都是逗號(hào)而不是分號(hào),而且即使 JavaScript 對(duì)象字面量的寫法和 JSON 的格式相似度很高,但是它們還是有本質(zhì)的區(qū)別的。
在我們搗鼓 JavaScript 的過程中,工具是非常重要的。我這里介紹的第一個(gè)工具就是 Chromium 瀏覽器中自帶的 JavaScript 控制臺(tái)。在 Ubuntu 中安裝 Chromium 瀏覽器只需要一個(gè)命令就可以搞定,如下:
sudo apt-get install chromium
啟動(dòng) Chromium 瀏覽器后,只需要按 F12 就可以調(diào)出 JavaScript 控制臺(tái)。當(dāng)然,在菜單中找出來也可以。下面,讓我把上面的示例代碼輸入到 JavaScript 控制臺(tái)中,一是可以看看我們寫的代碼是否有語法錯(cuò)誤,二是可以看看 JavaScript 對(duì)象的真面目。如下圖:
對(duì)于博客園中廣大的前端攻城獅來講,Chromium 的 JavaScript 控制臺(tái)已經(jīng)是一個(gè)爛大街的工具了,在控制臺(tái)中寫console.log("Hello, World!");就像是在 C 語言中寫printf("Hello, World!");一樣成為了入門標(biāo)配。在控制臺(tái)中輸入 JavaScript 語句后,一按 Enter 該行代碼就立即執(zhí)行,如果要輸入多行代碼怎么辦呢?一個(gè)辦法就是按 Shift+Enter 進(jìn)行換行,另外一個(gè)辦法就是在別的編輯器中寫好然后復(fù)制粘貼。其實(shí)在 Chromium 的 JavaScript 控制臺(tái)中還有一些不那么廣泛流傳的小技巧,比如使用console.dir()函數(shù)輸出 JavaScript 對(duì)象的內(nèi)部結(jié)構(gòu),如下圖:
從圖中,可以很容易看出每一個(gè)對(duì)象的屬性、方法和原型鏈。
和其它的面向?qū)ο缶幊陶Z言不同, JavaScript 不是基于類的代碼復(fù)用體系,它選擇了一種很奇特的基于原型的代碼復(fù)用機(jī)制。通俗點(diǎn)說,如果你想創(chuàng)建很多對(duì)象,而這些對(duì)象有某些相同的屬性和行為,你為每一個(gè)對(duì)象編寫多帶帶的代碼肯定是不合算的。在其它的面向?qū)ο缶幊陶Z言中,你可以先設(shè)計(jì)一個(gè)類,然后再以這個(gè)類為模板來創(chuàng)建對(duì)象。我這里稱這種方式為經(jīng)典的面向?qū)ο篌w系。而在 JavaScript 中,解決這個(gè)問題的方式是把一個(gè)對(duì)象作為另外一個(gè)對(duì)象的原型,擁有相同原型的對(duì)象自然擁有了相同的屬性和行為。對(duì)象擁有原型,原型又有原型的原型,最終構(gòu)成一個(gè)原型鏈。當(dāng)訪問一個(gè)對(duì)象的屬性或方法的時(shí)候,先在對(duì)象本身中查找,如果找不到,則到原型中查找,如果還是找不到,則進(jìn)一步在原型的原型中查找,一直到原型鏈的最末端。在現(xiàn)代 JavaScript 模式中,硬是用函數(shù)、閉包和原型鏈模擬了經(jīng)典的面向?qū)ο篌w系。
原型這個(gè)概念本身并不復(fù)雜,復(fù)雜的是 JavaScript 中的隱式原型和函數(shù)對(duì)象。什么是隱式原型,就是說在 JavaScript 中不管你以什么方式創(chuàng)建一個(gè)對(duì)象,它都會(huì)自動(dòng)給你生成一個(gè)原型對(duì)象,我們的對(duì)象中,有一個(gè)隱藏的__proto__屬性,它指向這個(gè)自動(dòng)生成的原型對(duì)象;并且在 JavaScript 中不管你以什么方式創(chuàng)建一個(gè)對(duì)象,它最終都是從構(gòu)造函數(shù)生成的,以對(duì)象字面量構(gòu)造的對(duì)象也有構(gòu)造函數(shù),它們分別是Object()和Array(),每一個(gè)構(gòu)造函數(shù)都有一個(gè)自動(dòng)生成的prototype屬性,它也指向那個(gè)自動(dòng)生成的原型對(duì)象。而且在 JavaScript 中一切都是對(duì)象,構(gòu)造函數(shù)也不例外,所以構(gòu)造函數(shù)既有prototype屬性,又有__proto__屬性。再而且,自動(dòng)生成的原型對(duì)象也是對(duì)象,所以它也應(yīng)該有自己的原型對(duì)象。你看,說起來都這么拗口,理解就更加不容易了,更何況 JavaScript 中還內(nèi)置了Object()、Array()、String()、Number()、Boolean()、Function()這一系列的構(gòu)造函數(shù)??磥聿划媯€(gè)圖是真的理不順了。下面我們來抽絲剝繭。
先考察空對(duì)象someThing,哪怕它是以對(duì)象字面量的方式創(chuàng)建的,它也是從構(gòu)造函數(shù)Object()構(gòu)造出來的。這時(shí),JavaScript 會(huì)自動(dòng)創(chuàng)建一個(gè)原型對(duì)象,我們稱這個(gè)原型對(duì)象為Object.prototype,構(gòu)造函數(shù)Object()的prototype屬性指向這個(gè)對(duì)象,對(duì)象someThing的__proto__屬性也指向這個(gè)對(duì)象。也就是說,構(gòu)造函數(shù)Object()的prototype屬性和對(duì)象someThing的__proto__屬性指向的是同一個(gè)原型對(duì)象。而且,這個(gè)原型對(duì)象中有一個(gè)constructor屬性,它又指回了構(gòu)造函數(shù)Object(),這樣形成了一個(gè)環(huán)形的連接。如下圖:
要注意的是,這個(gè)圖中所顯示的關(guān)系是對(duì)象剛創(chuàng)建出來的時(shí)候的情況,這些屬性的指向都是可以隨意修改的,改了就不是這個(gè)樣子了。下面在 JavaScript 控制臺(tái)中驗(yàn)證一下上圖中的關(guān)系:
請(qǐng)注意,構(gòu)造函數(shù)Object()的prototype屬性和__proto__屬性是不同的,只有函數(shù)對(duì)象才同時(shí)具有這兩個(gè)屬性,普通對(duì)象只有__proto__屬性,而且這個(gè)__proto__屬性是隱藏屬性,不是每個(gè)瀏覽器都允許訪問的,比如 IE 瀏覽器。下面,我們來看看 IE 瀏覽器的開發(fā)者工具:
這是一個(gè)反面教材,它既不支持console.dir()來查看對(duì)象,也不允許訪問__proto__內(nèi)部屬性。所以,在后面我講到繼承時(shí),需要使用特殊的技巧來避免在我們的代碼中使用__proto__內(nèi)部屬性。上面的例子和示意圖中,都只說構(gòu)造函數(shù)Object()的prototype屬性指向原型對(duì)象,沒有說構(gòu)造函數(shù)Object()的__proto__屬性指向哪里,那么它究竟指向哪里呢?這里先留一點(diǎn)懸念。
下一步,我們自己創(chuàng)建一個(gè)構(gòu)造函數(shù),然后使用這個(gè)構(gòu)造函數(shù)創(chuàng)建一個(gè)對(duì)象,看看它們之間原型的關(guān)系,代碼是這樣的:
function Person(name, age, gender){
this.name = name; this.age = age; this.gender = gender;
}
Person.prototype.sayHello = function(){ return "Hello, my name is " + this.name; };
var somebody = new Person("youxia", 30, "male");
輸入到 Chromium 的 JavaScript 控制臺(tái)中,然后使用console.dir()分別查看構(gòu)造函數(shù)Person()和對(duì)象somebody,如下兩圖:
用圖片來表示它們之間的關(guān)系,應(yīng)該是這樣的:
我使用藍(lán)色表示構(gòu)造函數(shù),黃色表示對(duì)象,如果是 JavaScript 自帶的構(gòu)造函數(shù)和 prototype 對(duì)象,則顏色深一些。從上圖中可以看出,構(gòu)造函數(shù)Person()有一個(gè)prototype屬性和一個(gè)__proto__屬性,__proto__屬性的指向依然留懸念,prototype屬性指向Person.prototype對(duì)象,這是系統(tǒng)在我們定義構(gòu)造函數(shù)Person()的時(shí)候,自動(dòng)創(chuàng)建的一個(gè)和構(gòu)造函數(shù)Person()相關(guān)聯(lián)的原型對(duì)象,請(qǐng)注意,這個(gè)原型對(duì)象是和構(gòu)造函數(shù)Person()相關(guān)聯(lián)的原型對(duì)象,而不是構(gòu)造函數(shù)Person()的原型對(duì)象。當(dāng)我們使用構(gòu)造函數(shù)Person()創(chuàng)建對(duì)象somebody時(shí),somebody的原型就是這個(gè)系統(tǒng)自動(dòng)創(chuàng)建的原型對(duì)象Person.prototype,就是說對(duì)象somebody的__proto__屬性指向原型對(duì)象Person.prototype。而這個(gè)原型對(duì)象中有一個(gè)constructor屬性,又指回構(gòu)造函數(shù)Person(),形成一個(gè)環(huán)。這和空對(duì)象和構(gòu)造函數(shù)Object()是一樣的。而且原型對(duì)象Person.prototype的__proto__屬性指向Object.prototype。如果在這個(gè)圖中把空對(duì)象和構(gòu)造函數(shù)Object()加進(jìn)去的話,看起來是這樣的:
有點(diǎn)復(fù)雜了,是嗎?不過這還不算最復(fù)雜的,想想看,如果把JavaScript 內(nèi)置的Object()、Array()、String()、Number()、Boolean()、Function()這一系列的構(gòu)造函數(shù)以及與它們相關(guān)聯(lián)的原型對(duì)象都加進(jìn)去,會(huì)是什么情況?每一個(gè)構(gòu)造函數(shù)都有一個(gè)和它相關(guān)聯(lián)的原型對(duì)象,Object()有Object.prototype,Array()有Array.prototype,依此類推。其中最特殊的是Function()和Function.prototype,因?yàn)樗械暮瘮?shù)和構(gòu)造函數(shù)都是對(duì)象,所以所有的函數(shù)和構(gòu)造函數(shù)都有構(gòu)造函數(shù),而這個(gè)構(gòu)造函數(shù)就是Function()。也就是說,所有的函數(shù)和構(gòu)造函數(shù)都是由Function()生成,包括Function()本身。所以,所有的構(gòu)造函數(shù)的__proto__屬性都應(yīng)該指向Function.prototype,前面留的懸念終于有答案了。如果只考慮構(gòu)造函數(shù)Person()、Object()和Function()及其關(guān)聯(lián)的原型對(duì)象,在不解決懸念的情況下,圖形是這樣的:
可以看到,每一個(gè)構(gòu)造函數(shù)和它關(guān)聯(lián)的原型對(duì)象構(gòu)成一個(gè)環(huán),而且每一個(gè)構(gòu)造函數(shù)的__proto__屬性無所指。通過前面的分析我們知道,每一個(gè)函數(shù)和構(gòu)造函數(shù)的__proto__屬性應(yīng)該都指向Function.prototype。我用紅線標(biāo)出這個(gè)關(guān)系,結(jié)果應(yīng)該如下圖:
如果我們畫出前面提到過的所有構(gòu)造函數(shù)、對(duì)象、原型對(duì)象的全家福,會(huì)是個(gè)什么樣子呢?請(qǐng)看下圖:
暈菜了沒?歡迎指出錯(cuò)誤。把圖一畫,就發(fā)現(xiàn)其實(shí) JavaScript 中的原型鏈沒有那么復(fù)雜,有幾個(gè)內(nèi)置構(gòu)造函數(shù)就有幾個(gè)配套的原型對(duì)象而已。我這里只畫了六個(gè)內(nèi)置構(gòu)造函數(shù)和一個(gè)自定義構(gòu)造函數(shù),還有幾個(gè)內(nèi)置構(gòu)造函數(shù)沒有畫,比如Date()、Math()、Error()、RegExp(),但是這不影響我們理解。寫到這里,是不是應(yīng)該介紹一下我使用的畫圖工具了?
回到頂部
我使用的畫圖工具Graphviz
在我的 Linux 系列中,有一篇介紹畫圖工具的文章,不過我這次使用的工具是另辟蹊徑的 Graphviz,據(jù)說這是一個(gè)由貝爾實(shí)驗(yàn)室的幾個(gè)牛人開發(fā)和使用的畫流程圖的工具,它使用一種腳本語言定義圖形元素,然后自動(dòng)進(jìn)行布局和生成圖片。首先,在 Ubuntu 中安裝 Graphiz 非常簡單,一個(gè)命令的事兒:
sudo apt-get install graphviz
然后,創(chuàng)建一個(gè)文本文件,我這里把它命名為sample.gv,其內(nèi)容如下:
digraph GraphvizDemo{
Alone_Node; Node1 -> Node2 -> Node3;
}
這是一個(gè)最簡單的圖形定義文件了,在 Graphviz 中圖形僅僅由三個(gè)元素組成,它們分別是:1、Graph,代表整個(gè)圖形,上面源代碼中的digraph GraphvizDemo{}就定義了一個(gè) Graph,我們還可以定義 SubGraph,代表子圖形,可以用 SubGraph 將圖形中的元素分組;2、Node,代表圖形中的一個(gè)節(jié)點(diǎn),可以看到 Node 的定義非常簡單,上面源碼中的Alone_Node;就是定義了一個(gè)節(jié)點(diǎn);3、Edge,代表連接 Node 的邊,上面源碼中的Node1 -> Node2 -> Node3;就是定義了三個(gè)節(jié)點(diǎn)和兩條邊,可以先定義節(jié)點(diǎn)再定義邊,也可以直接在定義邊的同時(shí)定義節(jié)點(diǎn)。然后,調(diào)用 Graphviz 中的dot命令,就可以生成圖形了:
dot -Tpng sample.gv > sample.png
生成的圖形如下:
上面的圖形中都是用的默認(rèn)屬性,所以看起來效果不咋地。我們可以為其中的元素定義屬性,包括定義節(jié)點(diǎn)的形狀、邊的形狀、節(jié)點(diǎn)之間的距離、字體的大小和顏色等等。比如下面是一個(gè)稍微復(fù)雜點(diǎn)的例子:
digraph GraphvizDemo{
nodesep=0.5; ranksep=0.5; node [shape="record",style="filled",color="black",fillcolor="#f4a582",fontname="consolas",fontsize=15]; edge [style="solid",color="#053061"]; root [label="left| right"]; left [label=" left| right"]; right [label=" left| right"]; leaf1 [label=" left| right"]; leaf2 [label=" left| right"]; leaf3 [label=" left| right"]; leaf4 [label=" left| right"]; root:l:s -> left:n; root:r:s -> right:n; left:l:s -> leaf1:n; left:r:s -> leaf2:n; right:l:s -> leaf3:n; right:r:s -> leaf4:n;
}
在這個(gè)例子中,我們使用了nodesep=0.5;和ranksep=0.5設(shè)置了 Graph 的全局屬性,使用了node [shape=...];和[edge [style=...];這樣的語句設(shè)置了 Node 和 Edge 的全局屬性,并且在每一個(gè) Node 和 Edge 后面分別設(shè)置了它們自己的屬性。在這些屬性中,比較特別的是 Node 的shape屬性,我將它設(shè)置為record,這樣就可以很方便地利用 Node 的label屬性來繪制出類似表格的效果了。同時(shí),在定義 Edge 的時(shí)候還可以指定箭頭的起始點(diǎn)。
執(zhí)行dot命令,可以得到這樣的圖形:
是不是漂亮了很多?雖然以上工作使用任何文本編輯器都可以完成,但是為了提高工作效率,我當(dāng)然要祭出我的神器 Eclipse 了。在 Eclipse 中可以定義外部工具,所以我寫一個(gè) shell 腳本,將它定義為一個(gè)外部工具,這樣,每次編寫完圖形定義文件,點(diǎn)一下鼠標(biāo),就可以自動(dòng)生成圖片了。使用 Eclipse 還可以解決預(yù)覽的問題,只需要編寫一個(gè) html 頁面,該頁面中只包含生成的圖片,就可以利用 Eclipse 自帶的 Web 瀏覽器預(yù)覽圖片了。這樣,每次改動(dòng)圖形定義文件后,只需要點(diǎn)一下鼠標(biāo)生成圖片,再點(diǎn)一下鼠標(biāo)刷新瀏覽器就可以實(shí)時(shí)預(yù)覽圖片了。雖然不是所見即所得,但是工作效率已經(jīng)很高了。請(qǐng)看動(dòng)畫:
Graphviz 中可以設(shè)置的屬性很多,具體內(nèi)容可以查看 Graphviz官網(wǎng) 上的文檔。
回到頂部
作用域鏈、上下文環(huán)境和閉包
關(guān)于變量的作用域這個(gè)問題應(yīng)該不用多講,凡是接觸編程的童鞋,無不都要從這個(gè)基礎(chǔ)的概念開始。變量作用域的通用規(guī)則其實(shí)很簡單,無非三條:1.內(nèi)層的代碼可以訪問外層代碼定義的變量,外層代碼不能訪問內(nèi)層代碼定義的變量;2.變量要先定義后使用;3.退出代碼的作用域時(shí),變量會(huì)被銷毀。以 C 語言代碼為例:
int a0 = 0;
{
int a1 = 1; printf("%d ", a0); //可以訪問外層變量,打印 0 printf("%d ", a2); //錯(cuò)誤,變量 a2 還沒定義呢 int a2 = 2; //變量要先定義后使用
}
/ 而且,退出作用域后,變量 a1 和 a2 會(huì)被自動(dòng)銷毀 /
printf("%dn", a1); //錯(cuò)誤,外層代碼不能訪問內(nèi)層變量
但是在 JavaScript 中,以上三條規(guī)則都有可能會(huì)被打破。從現(xiàn)在開始,我們就要開始踩坑了,在 JavaScript 語言滿滿的陷阱中,關(guān)于變量這一塊的最多。首先第一個(gè)坑, JavaScript 中沒有塊作用域,只有函數(shù)作用域。也就是說,要在 JavaScript 中實(shí)現(xiàn)以上類似 C 語言的效果,我們的代碼應(yīng)該這樣寫:
var a0 = 0;
function someFunc(){
var a1 = 1; console.log(a1); //可以訪問外層變量,打印 0 console.log(a2); //你以為會(huì)出現(xiàn)錯(cuò)誤,因?yàn)樽兞繘]有定義,但是你錯(cuò)了,這里不會(huì)發(fā)生錯(cuò)誤,而是打印 undefined var a2 = 2;
}
someFunc();
/ someFunc()執(zhí)行完之后,變量 a1 和 a2 會(huì)被自動(dòng)銷毀 /
console.log(a1); //錯(cuò)誤,外層代碼不能訪問內(nèi)層變量
把這段代碼復(fù)制到控制臺(tái)中驗(yàn)證一下,我就不截圖了,畢竟我這是一篇超長的熊文,圖片太多會(huì)被罵的,大家自己驗(yàn)證就可以了。注意,定義函數(shù)后需要調(diào)用它,函數(shù)內(nèi)的代碼才會(huì)執(zhí)行,為了方便,我以后把它寫成定義完后立即調(diào)用的自執(zhí)行格式。這里碰到的第二個(gè)坑就是變量提升,在 JavaScript 中,你本以為沒有定義變量 a2 就使用會(huì)出現(xiàn)錯(cuò)誤,哪知道定義在后面的var a2 = 2;被提升到代碼塊的前面了,結(jié)果就輸出 undefined。把上面的例子稍微改一改,就可以看到經(jīng)典的變量提升的坑,如下:
var a0 = 0;
(function (){
var a1 = 1; console.log(a0); //本以為會(huì)訪問外層變量a0,打印 0,哪知道定義在后面的 var a0 = 1; 被提升了,所以打印 undefined var a0 = 1;
})(); //為了省事,寫成匿名函數(shù)自執(zhí)行格式
console.log(a1); //錯(cuò)誤,外層代碼不能訪問內(nèi)層變量
本以為這里會(huì)訪問外層變量a0,打印 0,哪知道定義在后面的 var a0 = 1; 被提升了,所以打印 undefined。為什么是 undefined 而不是 1 呢?那是因?yàn)樽兞刻嵘皇翘嵘俗兞康亩x,沒有提升變量的賦值。不僅變量定義會(huì)被提升,函數(shù)定義也會(huì)被提升,這也是一個(gè)經(jīng)典的坑。如下代碼:
if(true){ //因?yàn)闂l件恒為true,所以肯定會(huì)執(zhí)行這個(gè)分支
function someFunc(){ console.log("true"); }
}else{
function someFunc(){ console.log("false"); }
}
someFunc(); //本以為會(huì)輸出 true,結(jié)果卻輸出 false,就是因?yàn)槎x在 else 分支中的函數(shù)被提升了,覆蓋了定義在 true 分支中的函數(shù)
當(dāng)然,以上 Bug 只會(huì)在部分瀏覽器中出現(xiàn),在 Chromium 和 FireFox 中還是能正確輸出 true 的。為了避免函數(shù)定義的提升造成的問題,在這種情況下,我們可以使用函數(shù)表達(dá)式而不是函數(shù)定義,代碼如下:
if(true){ //因?yàn)闂l件恒為true,所以肯定會(huì)執(zhí)行這個(gè)分支
var someFunc = function(){ console.log("true"); }
}else{
var someFunc = function(){ console.log("false"); }
}
someFunc();
關(guān)于函數(shù)定義和函數(shù)表達(dá)式的區(qū)別,我這里就不深入討論了。
內(nèi)層代碼可以訪問外層變量,所以內(nèi)層代碼在訪問一個(gè)變量的時(shí)候,會(huì)從內(nèi)層到外層逐層搜索該變量,這就是變量作用域鏈,理解這一點(diǎn)有時(shí)有助于我們優(yōu)化 JavaScript 代碼的執(zhí)行速度,對(duì)變量的搜索的路徑越短,代碼執(zhí)行就越快。另外,除了全局變量外,定義在函數(shù)內(nèi)部的變量只有在函數(shù)執(zhí)行的時(shí)候后,這個(gè)變量才會(huì)被創(chuàng)建,這就是執(zhí)行上下文,裝逼說法叫 context,每一個(gè)函數(shù)執(zhí)行的時(shí)候就會(huì)創(chuàng)建一個(gè) context。前面提過,在 C 語言中,一個(gè)代碼塊退出的時(shí)候,這個(gè)代碼塊的 context 和里面的變量也會(huì)被銷毀,但是在 JavaScript 函數(shù)執(zhí)行結(jié)束后,函數(shù)的 context 和里面的變量會(huì)被銷毀嗎?那可不一定哦。如果一個(gè)函數(shù)中定義的變量被捕獲,那么這個(gè)函數(shù)的 context 和里面的變量就會(huì)保留,比如閉包。這個(gè)不叫坑,叫語言特性。
在博客園中,有很多人寫閉包,但是都寫得無比復(fù)雜,定義也不是很準(zhǔn)確。其實(shí)閉包就是定義在內(nèi)層的函數(shù)捕獲了定義在外層函數(shù)中的變量,并把內(nèi)層函數(shù)傳遞到外層函數(shù)的作用域之外執(zhí)行,則外層函數(shù)的 context 不能銷毀,就形成了閉包。把內(nèi)層函數(shù)傳遞到外層函數(shù)的作用域之外有很多方法,最常見的是使用return,其它的方法還有把內(nèi)層函數(shù)賦值給全局對(duì)象的屬性,或者設(shè)置為某個(gè)控件的事件處理程序,甚至使用setTimeout和setInterval都可以。
其實(shí)閉包并不是 JavaScript 語言特有的概念,只要是把函數(shù)當(dāng)成頭等對(duì)象的語言都有。C 語言和早期的 C++ 和 Java 沒有,想想看,我們根本就沒辦法在上述語言中定義函數(shù)內(nèi)部的函數(shù)。不過自從 C++ 和 Java 引入了 lambda 表達(dá)式之后,就有了閉包的概念了。
下面,我們來探索 JavaScript 中的函數(shù)執(zhí)行上下文和閉包。為了印象深刻,我這里定義了一個(gè)嵌套四層的函數(shù),函數(shù)first()返回定義在first()內(nèi)的second(),second()返回定義在second()內(nèi)的third(),third()再返回一個(gè)匿名函數(shù),代碼如下:
var a0 = 0;
var b0 = "Global context";
function first(){
var a1 = 1; var b1 = "first() context"; function second(){ var a2 = 2; var b2 = "second() context"; function third(){ var a3 = 3; var b3 = "third() context"; return function(){ var a4 = 4; var b4 = "what"s matter, can I see it?"; console.log([ a1, a2, a3, a4 ]); console.log([ b1, b2, b3, b4]); } } return third; } return second;
}
然后,調(diào)用var what = first()()();返回最內(nèi)層的匿名函數(shù),使用console.dir(what);來查看這個(gè)匿名函數(shù),如下圖:
從圖中可以看到,返回的最內(nèi)層函數(shù)被命名為function anonymous(),其中有一個(gè)
下面問題來了,為什么我們看不到我們定義的變量a4和b4呢?因?yàn)閍4和b4只有在function anonymous()被執(zhí)行后才會(huì)產(chǎn)生。我們這里只是返回了function anonymous(),還沒有執(zhí)行它呢。其實(shí)就算執(zhí)行它我們也看不到變量a4和b4所在的 context,因?yàn)楹瘮?shù)的執(zhí)行總是一閃而過,如果沒有形成閉包,函數(shù)一執(zhí)行完該 context 就銷毀了。除非我們能讓該函數(shù)執(zhí)行到快完的時(shí)候定住。有什么辦法呢?你是不是想到了調(diào)試器?只要我們在這個(gè)函數(shù)中設(shè)置一個(gè) breakpoint,是不是就可以看到它的 context 了呢?
Chromium 當(dāng)然是自帶調(diào)試功能的。不過要想在 Chromium 中調(diào)試代碼就得把以上 JavaScript 代碼加到 HTML 頁面中。我懶得這么做。這里,我就要祭出 Node.js 和 Visual Studio Code 了。在 Ubuntu 中安裝 Node.js 非常方便,只需要使用如下命令:
sudo apt-get install nodejs
sudo apt-get install nodejs-legacy
為什么要安裝nodejs-legacy呢?那是因?yàn)閚odejs中的命令是nodejs,而nodejs-legacy中的命令是node,同時(shí)安裝這兩個(gè)包可以兼容不同的命令調(diào)用方式,其實(shí)它們本質(zhì)是一樣的。而編輯器技術(shù)哪家強(qiáng)?自從有了 Visual Studio Code 自然就不考慮其它的了。不過 Visual Studio Code 需要自己去它的 官網(wǎng) 下載。
把上面的代碼寫成一個(gè).js文件,然后在編輯器中每個(gè)函數(shù)的返回點(diǎn)設(shè)置斷點(diǎn),直接使用 Node.js 的調(diào)試功能,就可以查看所有的函數(shù)執(zhí)行時(shí)的 context 了,如下動(dòng)圖:
把斷點(diǎn)設(shè)置在每一個(gè)函數(shù)的最后一條語句,按 F5 開始調(diào)試,每次暫停都可以看到這個(gè)函數(shù)執(zhí)行時(shí)產(chǎn)生的 context,在這個(gè) context 中,可以看到該函數(shù)中定義的變量和函數(shù),也就是其中顯示的Local范圍的變量,以及該函數(shù)可以訪問的外層變量,也就是其中顯示的Closure和Global范圍的變量。使用調(diào)試功能,我們終于可以看到a4和b4了,同時(shí)還可以發(fā)現(xiàn),在每一個(gè)函數(shù)的 context 中,都有一個(gè)特殊的變量this,下一節(jié),我們來討論函數(shù)和this,函數(shù)、原型、閉包和this是使用 JavaScript 模擬經(jīng)典的基于類的面向?qū)ο缶幊痰幕疽?。不過在進(jìn)入下一節(jié)之前,我還要來展示一下 Eclipse。
Eclipse 的最新版本 neon 終于改進(jìn)了,在前一個(gè)版本中,它只支持 ECMAScript 3,而且其網(wǎng)頁預(yù)覽還是使用的 Webkit-1.0,在今年發(fā)布的這個(gè)新版本中,終于支持 ECMAScript 5了,Webkit 也用到了最新版。還加入了對(duì) Node.js 的支持。不過 Eclipse 中關(guān)于 JavaScript 的智能提示似乎還是很差勁。Eclipse 的更新速度實(shí)在是太慢了。不過用 Eclipse 配合 Node.js 調(diào)試 JavaScript 也還不錯(cuò),下面直接上圖:
還有 Eclipse 的死對(duì)頭,IntelliJ IDEA 和 WebStorm 調(diào)試 JavaScript 也是不錯(cuò)的,我就不多說了。
關(guān)于內(nèi)層函數(shù)怎么捕獲變量的問題,在編程語言界還有一個(gè)經(jīng)典的爭議,那就是關(guān)于詞法作用域和動(dòng)態(tài)作用域的爭議。所謂詞法作用域,就是在函數(shù)定義時(shí)的環(huán)境中去尋找外層變量,而動(dòng)態(tài)作用域,就是在函數(shù)運(yùn)行時(shí)的環(huán)境中去尋找外層變量。大多數(shù)現(xiàn)在程序設(shè)計(jì)語言都是采用詞法作用域規(guī)則,而只有為數(shù)不多的幾種語言采用動(dòng)態(tài)作用域規(guī)則,包括APL、Snobol和Lisp的某些方言,還有 C 語言中的宏定義。很顯然, JavaScript 采用的是詞法作用域,變量的作用域鏈?zhǔn)窃诤瘮?shù)定義的時(shí)候就決定了的。而對(duì)于動(dòng)態(tài)作用域的例子,我們可以看看如下的用 LISP 語言定義的一個(gè)函數(shù):
(let ((y 7))
(defun scope-test (x) (list x y)))
這個(gè)函數(shù)調(diào)用時(shí),如果是采用動(dòng)態(tài)作用域的語言中,如 emacs lisp,它不是在定義它的環(huán)境中去尋找自由變量y,也就是說y的值不是7,而是在它運(yùn)行的環(huán)境中向前回溯,尋找變量y的值,所以這樣的代碼:
(let ((y 5))
(scope-test 3))
在 emacs lisp 的運(yùn)行結(jié)果為(3 5),而在采用詞法作用域規(guī)則的編程語言中,如 common lisp,它會(huì)在定義函數(shù)的環(huán)境中尋找自由變量y的值,所以這段代碼的運(yùn)行結(jié)果為(3 7)。
另外,還有一個(gè)關(guān)于閉包和循環(huán)的一個(gè)經(jīng)典的坑,當(dāng)閉包遇到循環(huán)的時(shí)候,如下代碼:
(function(){
var i; for(i = 1; i <= 10; i++){ setTimeout(function(){console.log(i);}, 500); //本以為會(huì)輸出數(shù)字 1-10,結(jié)果輸出了 10 次 11 }
})();
在上面代碼中,我為了簡潔,都使用了匿名函數(shù)。之所以會(huì)出現(xiàn)這樣意想不到的結(jié)果,就是因?yàn)槎x在內(nèi)層的匿名函數(shù)都捕獲了外層函數(shù)中的變量i,所以當(dāng)它們運(yùn)行的時(shí)候,都是輸出的這個(gè)i的最終的值,那就是11。如果要想得到預(yù)期的輸出 1-10 這樣的結(jié)果,就應(yīng)該在定義內(nèi)層函數(shù)的時(shí)候讓它接受一個(gè)參數(shù),然后把i當(dāng)做參數(shù)傳遞給它。代碼改成這樣就行:
(function(){
var i; for(i = 1; i <= 10; i++){ setTimeout((function(a){console.log(a);})(i), 500); }
})();
全部寫成匿名函數(shù)自調(diào)用格式簡潔是簡潔了不少,但是可讀性就差了許多。網(wǎng)上的關(guān)于這個(gè)坑的描述所用的示例代碼往往是將內(nèi)層函數(shù)設(shè)置為某個(gè)按鈕的onClick事件處理程序,而我不想在我的示范中和 BOM、DOM 產(chǎn)生太多的耦合,所以我選擇了setTimeout()。如果不信,可以自己在 Chromium 的 JavaScript 控制臺(tái)中驗(yàn)證效果。
回到頂部
函數(shù)和this
從前面的調(diào)試過程中我們可以看出,每一個(gè)函數(shù)執(zhí)行的 context 中都有一個(gè)特殊的變量this。對(duì)this大家都不會(huì)陌生,很多面向?qū)ο蟮木幊陶Z言中都有,但是在 JavaScript 中,this會(huì)稍有不同,它的取值會(huì)隨著函數(shù)的調(diào)用方式不同而變化。JavaScript 中函數(shù)的調(diào)用方式多種多樣,總結(jié)起來主要有四種:
做為構(gòu)造函數(shù)調(diào)用,比如前面的new Person();、new Object();;
做為對(duì)象的方法調(diào)用,比如前面的somebody.sayHello();;
做為普通函數(shù)調(diào)用,這是用得最多的,比如前面的first();、what();;
通過apply、call、bind方式調(diào)用,這種調(diào)用方式我后面會(huì)舉例。
在第一種調(diào)用方式中,this的取值就是該構(gòu)造函數(shù)即將創(chuàng)建的對(duì)象。在第二種方式中,this的取值就是該方法所在的對(duì)象。這兩種調(diào)用方式和經(jīng)典的面向?qū)ο缶幊陶Z言沒有什么不同,非常容易理解。第三種方式,做為普通函數(shù)調(diào)用,這時(shí),函數(shù)中的this永遠(yuǎn)都指向全局對(duì)象,不管函數(shù)的定義嵌套得有多深,切記切記。而第四中調(diào)用方法最特別,它可以改變函數(shù)中this的取值,因此,這種方式調(diào)用最靈活,妙用最多,這個(gè)需要幾個(gè)例子才能說明。先回顧一下我前面定義的Person()構(gòu)造函數(shù)以及somebody對(duì)象:
function Person(name, age, gender){
this.name = name; this.age = age; this.gender = gender;
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
如果我們調(diào)用:
somebody.sayHello(); //sayHello()中的this指向somebody,所以輸出"Hello, my name is youxia"
那么這個(gè)sayHello();方法中的this指向somebody對(duì)象,所以輸出結(jié)果很符合預(yù)期。但是,如果該函數(shù)不是通過對(duì)象的方法調(diào)用,結(jié)果就會(huì)大不相同。比如這樣:
var sayHi = somebody.sayHello;
sayHi(); //做為普通函數(shù)調(diào)用,該函數(shù)中的this指向全局變量所以輸出"Hello, my name is "
在上面的例子中,因?yàn)槿肿兞恐袥]有name屬性,所以輸出的結(jié)果中就沒有名字了。
然后,我為了偷懶,不想定義一個(gè)構(gòu)造函數(shù),只使用對(duì)象字面量定義了一個(gè)對(duì)象worker,代表一個(gè)具有Java技術(shù)的程序員,如下:
var worker = {name:"javaer", speciality:"Java"};
這個(gè)對(duì)象沒有sayHello()方法,但是我們可以這樣借用somebody的sayHello()方法:
somebody.sayHello.call(worker); //輸出"Hello, my name is javaer"
所有的函數(shù)都可以通過.call()、.apply()、.bind()的形式調(diào)用,因?yàn)檫@三個(gè)方法是定義在Function.prototype中的,而所有的函數(shù)的原型鏈中都有Function.prototype。這三個(gè)函數(shù)都會(huì)把調(diào)用函數(shù)的this設(shè)置為這幾個(gè)方法的第一個(gè)參數(shù)。所不同者,.call()是接受任意多個(gè)參數(shù),而.apply()只接受兩個(gè)參數(shù),其第二個(gè)參數(shù)必須是一個(gè)數(shù)組,而.bind()返回另外一個(gè)函數(shù),這個(gè)函數(shù)的this綁定到.bind()的參數(shù)所指定的對(duì)象。
可以看到,如果某個(gè)對(duì)象具有和其它對(duì)象相同的屬性,比如這里的name屬性,就通過.call()的方式借用別的對(duì)象的方法。由于.apply()接受的第二個(gè)參數(shù)是一個(gè)數(shù)組,所以,如果有某個(gè)函數(shù)本身只接受不定數(shù)量的參數(shù),而要操作的確是一個(gè)數(shù)組的時(shí)候,就可以用.apply()來在它們之間適配。最常見的例子就是Math.max()方法,該方法接受的是不定數(shù)量的參數(shù),假如我們手頭只有一個(gè)數(shù)組,比如這樣:
var numbers = [3, 2, 5, 1, 7, 9, 8, 2];
而我們又要找出數(shù)組中的最大值的話,可以這樣調(diào)用:
Math.max.apply(null, numbers);
把第一個(gè)參數(shù)設(shè)置為null,則Math.max()中的this就會(huì)自動(dòng)指向全局對(duì)象。不過在這個(gè)例子中,this的值不重要。這里只是改變了Math.max()方法接受參數(shù)的形式。
在 JavaScript 中經(jīng)常使用.call()調(diào)用來借用內(nèi)置對(duì)象的方法,最常見的是借用Object.prototype.toString()方法。雖然我們所有的對(duì)象都是從Object繼承,所有的對(duì)象都有從Object繼承的toString()方法,但是,這些方法可以隨時(shí)被重寫。比如在我們前面定義的Person類中,我們可以重寫它的toString()方法,如下:
Person.prototype.toString = function(){
return "Person {name: "" + this.name + "", age: " + this.age + ", gender: "" + this.gender + ""}";
}
這時(shí),調(diào)用somebody的toString()方法,會(huì)得到這樣的輸出:
somebody.toString();
//輸出 "Person {name: "youxia", age: 30, gender: "male"}"
但是如果借用Object.prototype.toString()方法,則會(huì)得到另外一種輸出:
Object.prototype.toString.call(somebody);
//輸出 "[object Object]"
所以這種技術(shù)常被各種庫用來判斷對(duì)象的類型。如下:
Object.prototype.toString.call(somebody);
//輸出 "[object Object]"
Object.prototype.toString.call(Person);
//輸出 "[object Function]"
Object.prototype.toString.call("Hello, World!");
//輸出 "[object String]"
Object.prototype.toString.call(["one", "two", "three"]);
//輸出 "[object Array]"
Object.prototype.toString.call(3.14);
//輸出 "[object Number]"
從上面可以看出,使用.call()借用別的對(duì)象中的方法,不會(huì)受到本對(duì)象中重寫的同名方法的影響。所以,也可以在子類中使用此技巧調(diào)用父類中的方法,后面我講面向?qū)ο蠛屠^承的時(shí)候會(huì)用到這個(gè)技巧。
下面又要開始踩坑了,這個(gè)坑是關(guān)于this的。上面提到過,凡是作為普通函數(shù)調(diào)用的函數(shù),其 context 中的this都是指向全局對(duì)象的。所以,如果我們在某個(gè)對(duì)象的構(gòu)造函數(shù)或方法中定義了內(nèi)部函數(shù),本以為使用this可以訪問這個(gè)新構(gòu)造的對(duì)象,結(jié)果會(huì)事與愿違。如下代碼:
function Worker(name, speciality){
this.name = name; this.speciality = speciality; this.doWork = function(){ function work(){console.log(this.name + " is working with " + this.speciality);} work(); }
}
var worker = new Worker("youxia", "Java");
worker.doWork();
本以為會(huì)輸出"youxia is working with Java",但是由于其中定義的work()是一個(gè)普通函數(shù),所以其中的this指向全局對(duì)象,而全局對(duì)象的name和speciality屬性是沒有定義的,所以會(huì)輸出"is working with undefined"。如果要解決這個(gè)問題,可以在構(gòu)造函數(shù)中先臨時(shí)保存this的值,在網(wǎng)絡(luò)中,大家一般喜歡用that這個(gè)詞。更改后的代碼如下:
function Worker(name, speciality){
this.name = name; this.speciality = speciality; var that = this; this.doWork = function(){ function work(){console.log(that.name + " is working with " + that.speciality);} work(); }
}
var worker = new Worker("youxia", "Java");
worker.doWork();
這回輸出就完全正確了。同時(shí),這里也提示出一個(gè)小技巧,那就是當(dāng)我們位于一個(gè)閉包中時(shí),如果想訪問全局對(duì)象,只需要定義一個(gè)普通函數(shù),然后訪問這個(gè)普通函數(shù)的this即可。
回到頂部
用JavaScript模擬經(jīng)典的面向?qū)ο缶幊?br> 經(jīng)典的面向?qū)ο缶幊陶Z言比如 C++、C#、Java 等都是基于類的,它們都有一套成熟的體系,包括對(duì)象的構(gòu)造、類的繼承、對(duì)象的多態(tài)、對(duì)象屬性的訪問控制等。在 JavaScript 中,多態(tài)這個(gè)問題可以不用考慮,因?yàn)?JavaScript 語言本身就是動(dòng)態(tài)的,所以不存在類型不符合就編譯不通過這樣的問題。在 JavaScript 中主要考慮的問題就是對(duì)象的構(gòu)造和繼承的問題。
對(duì)象的構(gòu)造是需要首先考慮的問題,其目標(biāo)就是要獲得一個(gè)合理的對(duì)象內(nèi)存布局。在 JavaScript 中沒有類的概念,但是有構(gòu)造函數(shù)和this就足夠了,所以我們可以這樣簡單地創(chuàng)建對(duì)象:
function Person(name, age, gender){
this.name = name; this.age = age; this.gender = gender; this.sayHello = function(){ console.log("Hello, my name is " + this.name); };
}
var somebody = new Person("somebody", 30, "male");
var another = new Person("another", 20, "female");
這和經(jīng)典的面向?qū)ο缶幊陶Z言在形式上是很像的,經(jīng)典的面向?qū)ο蟮木幊陶Z言是在類里面定義屬性和方法,而這里是在構(gòu)造函數(shù)中定義屬性和方法。然而,仔細(xì)分析的話,其在內(nèi)存布局上還有不合理的地方,經(jīng)典的面向?qū)ο缶幊陶Z言中每個(gè)對(duì)象的屬性是多帶帶的,但是方法在內(nèi)存中只有一個(gè)拷貝,而上述 JavaScript 代碼每構(gòu)建一個(gè)對(duì)象,都會(huì)為每個(gè)對(duì)象定義一個(gè)方法,如果對(duì)象數(shù)量很大的話,就會(huì)浪費(fèi)很多內(nèi)存。
根據(jù)所有對(duì)象共享方法的原則,以及 JavaScript 的語言特色,我們應(yīng)該把方法放到其原型中,所以代碼應(yīng)更改如下:
function Person(name, age, gender){
this.name = name; this.age = age; this.gender = gender;
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");
同時(shí),如果為了防止別人在調(diào)用構(gòu)造函數(shù)的時(shí)候忘記使用new而踩入this的陷阱的話,該代碼還可以繼續(xù)這樣完善:
function Person(name, age, gender){
if(!(this instanceof Person)){ return new Person(name, age, gender); } this.name = name; this.age = age; this.gender = gender;
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");
下面再來看繼承。假設(shè)我們每個(gè)人都有一個(gè)工作者身份,我們會(huì)使用我們掌握的某項(xiàng)技能進(jìn)行工作,這里用 Worker 代表工作者,而 Worker 從 Person 繼承。我們先來寫 Worker,由于 JavaScript 是一個(gè)基于原型的語言,所以理論上講,要讓 Worker 繼承自 Person,只需要把 Person 類的一個(gè)對(duì)象加入到 Worker 的原型鏈中即可,如下:
function Worker(name, age, gender, speciality){
this.name = name; this.age = age; this.gender = gender; this.speciality = speciality;
}
Worker.prototype = new Person(name, age, gender);
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}
很顯然,這也不是很合理的,在這里需要構(gòu)建一個(gè) Person 類的對(duì)象(這里暫且這么稱呼吧,雖然 JavaScript 中沒有類),而構(gòu)建 Person 類的對(duì)象時(shí)又要傳遞參數(shù),這些參數(shù)哪里來呢?很顯然編碼不是很方便。同時(shí),既然在 Person 類的對(duì)象中構(gòu)造了name、age、gender等屬性,再在 Worker 類的對(duì)象中構(gòu)建一次就重復(fù)了。而且修改了Worker.prototype后,constructor屬性也變了,還要一條語句改回來。如果從經(jīng)典的面向?qū)ο缶幊陶Z言的角度來考慮,我們需要繼承的僅僅只是 Person 類中的方法而已。如果從 JavaScript 語言的角度分析,我們只需要把Person.prototype加入到 Worker 類的對(duì)象的原型鏈中即可。代碼是這樣:
Worker.prototype.__proto__ = Person.prototype;
我們還可以使用前面提到的.call()來借用 Person 類的構(gòu)造函數(shù)讓代碼更簡潔。完整的繼承代碼如下:
function Worker(name, age, gender, speciality){
Person.call(this, name, age, gender); this.speciality = speciality;
}
Worker.prototype.__proto__ = Person.prototype;
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}
這樣使用它:
var worker = new Worker("youxia", 30, "male", ["JavaScript","HTML","CSS"]);
worker.sayHello(); //從Person類繼承的
worker.doWork(); //Worker類中自己定義的
這是目前最接近經(jīng)典面向?qū)ο笳Z言的 JavaScript 模擬了。不過還有一個(gè)小小的問題,在 JavaScript 中,__proto__是一個(gè)隱藏屬性,不是所有的 JavaScript 平臺(tái)都支持的,比如前面展示的 IE 瀏覽器這個(gè)反面教材。這時(shí),還是要把Worker.prototype設(shè)置為一個(gè) Person 類的對(duì)象,但是,構(gòu)建一個(gè) Person 類的對(duì)象是個(gè)浪費(fèi),所以我們可以借助一個(gè)空構(gòu)造函數(shù)來完成這個(gè)事情:
function EmptyFunc(){}
EmptyFunc.prototype = Person.prototype;
Worker.prototype = new EmptyFunc();
當(dāng)然,別忘了把constructor改回來:
Worker.prototype.constructor = Worker;
這幾條語句有點(diǎn)多,所以可以寫一個(gè)輔助函數(shù)來解決這個(gè)問題:
function inherit(Sub, Super){
function F(){} F.prototype = Super.prototype; Sub.prototype = new F(); Sub.constructor = Sub;
}
完整代碼如下:
function inherit(Sub, Super){
function F(){} F.prototype = Super.prototype; Sub.prototype = new F(); Sub.constructor = Sub;
}
function Person(name, age, gender){
if(!(this instanceof Person)){ return new Person(name, age, gender); } this.name = name; this.age = age; this.gender = gender;
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");
function Worker(name, age, gender, speciality){
if(!(this instanceof Person)){ return new Worker(name, age, gender, speciality); } Person.call(this, name, age, gender); this.speciality = speciality;
}
inherit(Worker, Person);
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}
var worker = new Worker("youxia", 30, "male", ["JavaScript","HTML","CSS"]);
worker.sayHello(); //從Person類繼承的
worker.doWork(); //Worker類中自己定義的
上面的代碼運(yùn)行結(jié)果如下圖:
回到頂部
JavaScript的模塊化寫法
更進(jìn)一步,還要解決一個(gè)問題,那就是怎么把這么大一坨代碼封裝起來,專業(yè)的說法,那叫模塊化寫法。
JavaScript 的一個(gè)缺陷就是它沒有模塊化的機(jī)制,像前文中我所寫的所有構(gòu)造函數(shù)都是直接暴露在全局作用域中的,這很不科學(xué),一是污染了全局作用域,二是容易和別人寫的代碼發(fā)生沖突。當(dāng)代碼量增大的時(shí)候,肯定要考慮將我們自己的代碼組織成一個(gè)模塊。怎么辦呢?很顯然,在 JavaScript 中只能用自執(zhí)行函數(shù)和閉包來模擬。比如這樣:
(function(){
function inherit(Sub, Super){ ... } function Person(name, age, gender){ ... } Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); }; function Worker(name, age, gender, speciality){ ... } inherit(Worker, Person); Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);} window.myModule = { inherit : inherit, Person : Person, Worker : Worker };
})();
代碼比較長,我省略了一部分。最關(guān)鍵的代碼其實(shí)是最后的幾句,我們通過window對(duì)象的myModule屬性來暴露我們想暴露的函數(shù)和構(gòu)造函數(shù)。然后,我們可以在 HTML 頁面中這樣使用:
下面是我在 FireFox 瀏覽器的開發(fā)者工具中的截圖,在調(diào)試器中可以看到源文件,添加一個(gè)斷點(diǎn),就可以在右側(cè)看到定義在myModule中的函數(shù)和構(gòu)造函數(shù)了。Chromium 很牛,F(xiàn)ireFox 也不錯(cuò)。
當(dāng)然,這只是一個(gè)最簡單的前端模塊。我是直接把模塊的 JavaScript 文件路徑寫死在 HTML 中的。我在 HTML 中直接引用我自己的模塊是沒有問題的。但是在使用別人寫的模塊的時(shí)候就不一定有這么簡單了,因?yàn)閯e人的模塊中可能會(huì)引用更多另外的模塊,而這些互相引用的模塊我們不可能全部都寫死在 HTML 中,我們更加不可能控制這些模塊的加載順序。因此,需要有統(tǒng)一的模塊規(guī)范來解決模塊加載和依賴的問題。目前,在瀏覽器中常用的規(guī)范有 AMD 規(guī)范和 CMD 規(guī)范。
AMD 規(guī)范是指異步模塊定義,它是 RequireJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。在 AMD 中,所有的模塊將被異步加載,模塊加載不影響后面語句運(yùn)行。所有依賴某些模塊的語句均放置在回調(diào)函數(shù)中。AMD規(guī)范定義了一個(gè)全局 define 的函數(shù):
define( id?, dependencies?, factory );
第一個(gè)參數(shù) id 為字符串類型,表示了模塊標(biāo)識(shí),為可選參數(shù)。第二個(gè)參數(shù),dependencies 是一個(gè)數(shù)組,表示當(dāng)前模塊依賴的模塊。第三個(gè)參數(shù),factory,就是用來定義我們自己模塊的工廠方法, factory 接受的參數(shù)和 dependencies 完全一致。加入我們自己的模塊依賴于模塊module1、module2、module3的話,我們的模塊定義就應(yīng)該這樣寫:
define("myModule", ["module1", "module2", "module3"], function(module1, module2, module3){
// 這里定義我們自己的模塊的功能,可以使用 module1、module2、module3中提供的功能 var myModule = { a: function(){...}, b: function(){...} } return myModule; //必須return個(gè)什么才能被別人使用
}
如果要使用 myModule 就應(yīng)該這么寫:
define("", ["myModule"], function(myModule){
myModule.a(); myModule.b();
}
CMD 規(guī)范是指通用模塊定義,它是是 SeaJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。和 AMD 比較類似的是,它也定義了一個(gè)全局 define 函數(shù),而且其形式也很類似,都是
define( id?, dependencies?, factory );
所不同者,是它的 factory 接受的參數(shù)必須是 require、exports 和 moudle,在 factory 內(nèi)部,可以使用 require 引用依賴的模塊,可以使用 exports 導(dǎo)出自己的功能,這和 Node.js 自帶的 CommonJS 規(guī)范是比較相似的,其用法如下:
define( "module", ["module1", "module2"], function( require, exports, module ){
var a = require("./a"); a.doSomething(); exports.do = function(){...};
} );
AMD 和 CMD 的區(qū)別就是:1.對(duì)于依賴的模塊,AMD 是提前執(zhí)行,CMD 是延遲執(zhí)行。2.CMD 推崇依賴就近,AMD 推崇依賴前置。示例代碼是這樣:
// CMD
define(function(require, exports, module) {
var a = require("./a") a.doSomething() ... var b = require("./b") //依賴可以就近書寫 b.doSomething() ...
})
// AMD 默認(rèn)推薦的是
define(["./a", "./b"], function(a, b) { //依賴必須一開始就寫好
a.doSomething() ... b.doSomething() ...
})
當(dāng)然,這里面都有一個(gè)約定俗成的規(guī)則,那就是每一個(gè)模塊都是一個(gè)同名的.js文件,我們在寫模塊名的時(shí)候,可以省略這個(gè)文件的擴(kuò)展名。以上規(guī)范都是定義在前端的瀏覽器中的,而在后端的 Node.js 中就簡單多了。Node.js 采用的是 CommonJS 模塊規(guī)范,每一個(gè)文件就是一個(gè)模塊,也不需要定義 define 什么的,也不需要定義自執(zhí)行函數(shù)。在這個(gè)文件中,可以直接使用 exports 和 module。
有時(shí),我們需要讓我們編寫的模塊在前后端都能使用,這個(gè)要求不過分哦,比如我們想在 Node.js 中對(duì)模塊進(jìn)行單元測試,然后再發(fā)布到瀏覽器執(zhí)行。利用之前提到的每種模塊定義規(guī)范的特點(diǎn),我們可以寫出前后端通用的模塊,代碼片段如下:
var hasDefine = (typeof define !== "undefined");
var hasModule = (typeof module !== "undefined" && typeof module.exports !== "undefined");
if(hasDefine){ //運(yùn)行在符合 AMD 或 CMD 規(guī)范的環(huán)境中
define("myModule",function(){ return { inherit : inherit, Person : Person, Worker : Worker }; });
}else if(hasModule){ //運(yùn)行在Node.js中
module.exports = { inherit : inherit, Person : Person, Worker : Worker };
}else{ //否則直接加入到全局對(duì)象中
window.myModule = { inherit : inherit, Person : Person, Worker : Worker };
}
下面測試一下我們寫的模塊是否能在前后端通用。先在 Node.js 中測試,寫一個(gè)main.js,其內(nèi)容如下:
var myModule = require("./myModule");
var worker = new myModule.Worker("youxia", 30, "male", ["HTML","CSS","JavaScript"]);
worker.doWork();
運(yùn)行結(jié)果如下圖:
如果直接寫死在 HTML 中呢?運(yùn)行結(jié)果如下圖:
最后,我們看看使用 RequireJS 的情況。先到 RequireJS 的官網(wǎng)下載require.js文件,然后編寫一個(gè)main_in_amd.js文件,內(nèi)容如下:
requirejs(["./myModule"], function(myModule){
var worker = new myModule.Worker("youxia", 30, "male", ["HTML","CSS","JavaScript"]); worker.doWork();
});
再然后,寫一個(gè) HTML 文件,這樣引用require.js和main_in_amd.js文件:
最后的運(yùn)行結(jié)果如下圖:
從圖中可以看出,我們的模塊確確實(shí)實(shí)是前后端可以通用的。該模塊的完整代碼如下:
(function(){
function inherit(Sub, Super){ function F(){} F.prototype = Super.prototype; Sub.prototype = new F(); Sub.constructor = Sub; } function Person(name, age, gender){ if(!(this instanceof Person)){ return new Person(name, age, gender); } this.name = name; this.age = age; this.gender = gender; } Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); }; function Worker(name, age, gender, speciality){ if(!(this instanceof Person)){ return new Worker(name, age, gender, speciality); } Person.call(this, name, age, gender); this.speciality = speciality; } inherit(Worker, Person); Worker.prototype.doWork = function(){ console.log(this.name + " is working with " + this.speciality); } var hasDefine = (typeof define !== "undefined"); var hasModule = (typeof module !== "undefined" && typeof module.exports !== "undefined"); if(hasDefine){ //運(yùn)行在符合 AMD 或 CMD 規(guī)范的環(huán)境中 define("myModule", function(){ return { inherit : inherit, Person : Person, Worker : Worker }; }); }else if(hasModule){ //運(yùn)行在Node.js中 module.exports = { inherit : inherit, Person : Person, Worker : Worker }; }else{ //否則直接加入到全局對(duì)象中 window.myModule = { inherit : inherit, Person : Person, Worker : Worker }; }
})();
回到頂部
關(guān)于函數(shù)的更多探討
前面探討了 JavaScript 模擬經(jīng)典面向?qū)ο蟮膶懛?,而?shí)際應(yīng)用中,也有不少場景會(huì)使用 JavaScript 模擬函數(shù)式編程語言的寫法,比如偏函數(shù)和柯里化什么的。JavaScript 之父本身是一個(gè) Scheme(一種 LISP 方言) 高手,所以他在創(chuàng)建 JavaScript 之初就從函數(shù)式編程語言中吸收了很多東西,將函數(shù)當(dāng)成一等公民對(duì)待就是其證明。在 JavaScript 中,函數(shù)是一等公民,可以把函數(shù)當(dāng)參數(shù)傳遞給另外的函數(shù),也可以從函數(shù)中返回函數(shù),所以在 JavaScript 中使用高階函數(shù)是一個(gè)很簡單很常見的事情。
在前面的示例的截圖中可以看到,每一個(gè)函數(shù)中都包含幾個(gè)特殊的變量。前面已經(jīng)介紹了其中一個(gè)特殊變量this,另外還有一個(gè)特殊變量arguments,它代表了傳遞給函數(shù)的所有參數(shù),它是一個(gè)類數(shù)組的對(duì)象(說明它不是數(shù)組,但是可以利用前面介紹的.call()借用數(shù)組的方法)。什么時(shí)候會(huì)用到arguments呢?就是當(dāng)函數(shù)的形參個(gè)數(shù)和實(shí)參個(gè)數(shù)不一樣的時(shí)候,可以使用arguments變量訪問傳遞給函數(shù)的所有參數(shù)。這使得定義可變數(shù)量參數(shù)的函數(shù)成為可能。如下示例,隨便定義一個(gè)函數(shù),沒有定義形參,但是調(diào)用時(shí)可以指定任意的實(shí)參:
function someFunc(){
console.log("arguments count: " + arguments.length); for(var i = 0; i < arguments.length; i++){ console.log(arguments[i]); }
}
不管用多少個(gè)參數(shù)調(diào)用該函數(shù),都可以輸出參數(shù)的個(gè)數(shù)和所有的參數(shù),如下:
someFunc(1, 2, 3);
//輸出以下內(nèi)容
// arguments count: 3
// 1
// 2
// 3
someFunc("one", "two", "three", "four");
//輸出以下內(nèi)容
// arguments count: 4
// one
// two
// three
// four
在 JavaScript 中,常使用一種叫偏函數(shù)的技巧來簡化某些需要太多參數(shù)的函數(shù)的使用。有時(shí)有些函數(shù)需要接受很多參數(shù),但是其中一些參數(shù)是經(jīng)常重復(fù)的,但是調(diào)用時(shí)又必須輸入這些參數(shù),比較麻煩。這時(shí),就可以創(chuàng)建這些函數(shù)的偏函數(shù)版本,把那些經(jīng)常重復(fù)的參數(shù)預(yù)先設(shè)定為固定的值,調(diào)用這些偏函數(shù)時(shí),只需要傳遞少量參數(shù)就行了。舉例說明,可以利用前面提到的判斷對(duì)象類型的方法定義一個(gè)isType()函數(shù),如下:
function isType(obj, type){
if(Object.prototype.toString.call(obj) === "[object " + type + "]"){ return true; }else{ return false; }
}
可以這樣調(diào)用該函數(shù):
var msg = "Hello, World!";
var obj = {};
var arr = [];
isType(msg, "String");
isType(obj, "Object");
isType(arr, "Array");
每次調(diào)用都需要輸入兩個(gè)參數(shù),第二個(gè)參數(shù)通過字符串來指定需要判斷的類型,如果需要判斷類型的對(duì)象特別多,這樣調(diào)用就特別麻煩,而且容易出錯(cuò)。所以可以創(chuàng)建這個(gè)函數(shù)的偏函數(shù)版本,比如isFunction()、isString()、isArray()、isObject()等,這些函數(shù)只需要接受一個(gè)參數(shù)就可以了。怎么創(chuàng)建呢?先要寫一個(gè)返回偏函數(shù)的函數(shù):
function partial(fn, type){ //接受需要偏函數(shù)化的原函數(shù)和需要預(yù)先設(shè)置的參數(shù)作為參數(shù)
return function(obj){ //返回偏函數(shù) return fn(obj, type); }
}
然后利用這個(gè)函數(shù)返回isType()的各個(gè)偏函數(shù)版本,如下:
var isFunction = partial(isType, "Function");
var isString = partial(isType, "String");
var isArray = partial(isType, "Array");
var isObject = partial(isType, "Object");
然后這樣調(diào)用這些函數(shù):
isString(msg);
isObject(obj);
isArray(arr);
isFunction(Person);
是不是簡單多了,也不容易出錯(cuò)。但是上面只是把本來需要兩個(gè)參數(shù)的函數(shù)節(jié)約了一個(gè)參數(shù),收益并不是很大。其實(shí),可以使用偏函數(shù)的理論創(chuàng)建一個(gè)將任意函數(shù)進(jìn)行偏函數(shù)化的函數(shù)partialAny(),如下:
function partialAny(fn){ //接受原函數(shù)
var originalArgs = Array.prototype.slice.call(arguments, 1); //獲得原始參數(shù),其參數(shù)個(gè)數(shù)和原函數(shù)需要的個(gè)數(shù)相同,其中有占位符 return function(){ //返回偏函數(shù) var partialArgs = Array.prototype.slice.call(arguments); //獲得偏函數(shù)的參數(shù),其個(gè)數(shù)應(yīng)該和占位符的個(gè)數(shù)相同 var newArgs = []; for(var i=0; i < originalArgs.length; i++){ if(originalArgs[i] === "_"){ //如果碰到占位符,則用偏函數(shù)的參數(shù)填補(bǔ) newArgs[i] = partialArgs.shift(); }else{ newArgs[i] = originalArgs[i]; } } // 如果有任何多余的參數(shù),則添加到尾部 return fn.apply(this, newArgs.concat(partialArgs)); }
}
先用它了創(chuàng)建一個(gè)前面的isType()函數(shù)的偏函數(shù)試一試:
var isString = partialAny(isType, "_", "String");
isString("abc"); // 返回 true
我這里選擇了下劃線做為占位符,大家可以根據(jù)自己的情況酌情選擇。下面來個(gè)更復(fù)雜的例子,比如經(jīng)常需要?jiǎng)?chuàng)建 RGB 顏色的函數(shù):
function makeColor(r, g, b){
return "#" + r + g + b;
}
該函數(shù)一般情況下需要三個(gè)參數(shù),但是可以通過偏函數(shù)的方式讓 R、G、B 的任何一個(gè)分量固定,比如這樣:
var redMax = partialAny(makeColor, "ff", "_", "_");
var blueMax = partialAny(makeColor, "_", "_", "ff");
var greenMax = partialAny(makeColor, "_", "ff", "_");
var magentaMax = partialAny(makeColor, "ff", "_", "ff");
然后這樣調(diào)用它們:
redMax("33", "44"); // 輸出"#ff3344"
blueMax("55", "66"); // 輸出"#5566ff"
greenMax("77", "88"); // 輸出"#77ff88"
magentaMax("99"); // 輸出"#ff99ff"
函數(shù)也是對(duì)象,所以可以重寫函數(shù)對(duì)象的toString()和valueOf()方法來達(dá)到意想不到的效果。舉例說明,假如我們想創(chuàng)建一個(gè)add()函數(shù),它既可以這樣調(diào)用:
add(1,2);
又可以這樣調(diào)用:
add(1,2)(3);
add(1)(2)(3)(4);
可以看出,這很類似于函數(shù)式編程語言中的柯里化(currying)。仔細(xì)觀察可以發(fā)現(xiàn),要實(shí)現(xiàn)以上效果,add()必須返回一個(gè)函數(shù),這樣才能繼續(xù)后面的調(diào)用,如下:
function add(){
var result = 0; for(var i=0; i < arguments.length; i++){ //先計(jì)算第一層調(diào)用 result += arguments[i]; } function temp(){ for(var i=0; i < arguments.length; i++){ //再計(jì)算后續(xù)的調(diào)用 result += arguments[i]; } return temp; //返回函數(shù),所以可以無限調(diào)用下去 } return temp; //返回函數(shù),所以可以無限調(diào)用下去
}
這樣很方便就解決了函數(shù)連續(xù)調(diào)用的問題,但是又引出了新問題:該函數(shù)永遠(yuǎn)返回的是函數(shù),那怎樣才能得到求和的值呢?這時(shí)就該toString()和valueOf()上場了,我們只需要重寫函數(shù)temp()的toString()和valueOf()方法,就可以在函數(shù)調(diào)用結(jié)束后,獲得該表達(dá)式的值,如下:
temp.toString = temp.valueOf = function(){ return result; };
完整代碼如下:
function add(){
var result = 0; for(var i=0; i < arguments.length; i++){ //先計(jì)算第一層調(diào)用 result += arguments[i]; } function temp(){ for(var i=0; i < arguments.length; i++){ //再計(jì)算后續(xù)的調(diào)用 result += arguments[i]; } return temp; //返回函數(shù),所以可以無限調(diào)用下去 } temp.toString = temp.valueOf = function(){ return result; }; //這樣可以獲得函數(shù)的值 return temp; //返回函數(shù),所以可以無限調(diào)用下去
}
然后,就可以這樣隨意調(diào)用add()了:
add(1,2); //得到3
add(1,2)(3); //得到6
add(1,2)(3,4)()(5); //得到15
add(1,2)(3)(4)(5)()(6,7,8,9); //得到45
當(dāng)然,這并不是最嚴(yán)格的柯里化,柯里化只是函數(shù)式編程語言中的一個(gè)特性,嚴(yán)格的柯里化每次只接受一個(gè)參數(shù),直到總共接受了指定數(shù)量的參數(shù)后函數(shù)才執(zhí)行。使用柯里化的好處是參數(shù)復(fù)用和延遲執(zhí)行。所以,從本質(zhì)上講,前面提到的偏函數(shù)更接近函數(shù)式編程語言中柯里化的功能。上面的例子雖然形式上類似柯里化,但是比柯里化更靈活,可以接受無限多個(gè)參數(shù)和無限次調(diào)用,然而,這并沒有什么卵用,炫耀技巧而已。
回到頂部
總結(jié)
好吧,就寫這么多吧,這篇文章已經(jīng)夠長了。但是仍然不可能覆蓋 JavaScript 的方方面面。在我這篇文章中,主要關(guān)注的是 JavaScript 語言本身,而沒有涉及瀏覽器中的 BOM、DOM 操作,也沒有涉及 Node.js 的 API。
這篇文章中的內(nèi)容都是我根據(jù)自己的理解寫成的,我沒有《JavaScript 權(quán)威指南》和《JavaScript 高級(jí)程序設(shè)計(jì)》那么全面和啰嗦,我創(chuàng)建對(duì)象和實(shí)現(xiàn)繼承的方式也許和網(wǎng)絡(luò)上那些流行的做法不一樣,但是,Whatever,這就是我的理解,歡迎大家不服來辯。如果你能堅(jiān)持讀到這里,請(qǐng)不要吝嗇點(diǎn)個(gè)贊。謝謝!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/110098.html
摘要:采用完全獨(dú)立于任何程序語言的文本格式,使成為理想的數(shù)據(jù)交換語言為什么需要提到,我們就應(yīng)該和來進(jìn)行對(duì)比。也是一種存儲(chǔ)和交換文本信息的手段。那么好在哪里呢比更小更快,更易解析。使用的時(shí)候,也支持將轉(zhuǎn)成但是,我們不一定使用框架來做開發(fā)呀。 什么是JSON JSON:JavaScript Object Notation 【JavaScript 對(duì)象表示法】 JSON 是存儲(chǔ)和交換文本信息的語法...
摘要:前言上一次我們對(duì)的應(yīng)用進(jìn)行了一次全面的分析,這一次我們來聊聊。 showImg(https://segmentfault.com/img/remote/1460000020077803?w=1280&h=853); 前言 上一次我們對(duì)Paging的應(yīng)用進(jìn)行了一次全面的分析,這一次我們來聊聊WorkManager。 如果你對(duì)Paging還未了解,推薦閱讀這篇文章: Paging在Recy...
摘要:如果我們想要多次輸出類中的成員信息,就需要多次書寫方法每用一次就得寫而調(diào)用就簡單多了補(bǔ)充兩者等價(jià)輸出結(jié)果。注一般選擇重寫方法,比較對(duì)象的成員變量值是否相同,不過一般重寫都是自動(dòng)生成。 第三階段 JAVA常見對(duì)象的學(xué)習(xí) 第一章 常見對(duì)象——Object類 引言: 在講解Object類之前,我們不得不簡單的提一下什么是API,先貼一組百度百科的解釋: API(Application Pro...
摘要:目錄前言架構(gòu)安裝第一個(gè)爬蟲爬取有道翻譯創(chuàng)建項(xiàng)目創(chuàng)建創(chuàng)建解析運(yùn)行爬蟲爬取單詞釋義下載單詞語音文件前言學(xué)習(xí)有一段時(shí)間了,當(dāng)時(shí)想要獲取一下百度漢字的解析,又不想一個(gè)個(gè)漢字去搜,復(fù)制粘貼太費(fèi)勁,考慮到爬蟲的便利性,這篇文章是介紹一個(gè)爬蟲框架, 目錄 前言 架構(gòu) 安裝 第一個(gè)爬蟲:爬取有道翻譯 創(chuàng)建項(xiàng)目 創(chuàng)建Item 創(chuàng)建Spider 解析 運(yùn)行爬蟲-爬取單詞釋義 下載單詞語音文件 ...
閱讀 2849·2021-11-19 09:40
閱讀 3709·2021-11-15 18:10
閱讀 3289·2021-11-11 16:55
閱讀 1246·2021-09-28 09:36
閱讀 1663·2021-09-22 15:52
閱讀 3376·2019-08-30 14:06
閱讀 1171·2019-08-29 13:29
閱讀 2318·2019-08-26 17:04