摘要:不區分類和實例的概念,而是通過原型來實現面向對象編程。新創建的的原型鏈是也就是說,的原型指向函數的原型。最后,創建一個對象代碼和前面章節完全一樣小明繼承用定義對象的另一個巨大的好處是繼承更方便了。
JavaScript不區分類和實例的概念,而是通過原型(prototype)來實現面向對象編程。
原型是指當我們想要創建xiaoming這個具體的學生時,我們并沒有一個Student類型可用。那怎么辦?恰好有這么一個現成的對象:
var robot = { name: "Robot", height: 1.6, run: function () { console.log(this.name + " is running..."); } };
我們看這個robot對象有名字,有身高,還會跑,有點像小明,干脆就根據它來“創建”小明得了!
于是我們把它改名為Student,然后創建出xiaoming:
var Student = { name: "Robot", height: 1.2, run: function () { console.log(this.name + " is running..."); } }; var xiaoming = { name: "小明" }; xiaoming.__proto__ = Student;
注意最后一行代碼把xiaoming的原型指向了對象Student,看上去xiaoming仿佛是從Student繼承下來的:
xiaoming.name; // "小明" xiaoming.run(); // 小明 is running...
xiaoming有自己的name屬性,但并沒有定義run()方法。不過,由于小明是從Student繼承而來,只要Student有run()方法,xiaoming也可以調用:
JavaScript的原型鏈和Java的Class區別就在,它沒有“Class”的概念,所有對象都是實例,所謂繼承關系不過是把一個對象的原型指向另一個對象而已。
如果你把xiaoming的原型指向其他對象:
var Bird = { fly: function () { console.log(this.name + " is flying..."); } }; xiaoming.__proto__ = Bird;
現在xiaoming已經無法run()了,他已經變成了一只鳥:
xiaoming.fly(); // 小明 is flying...
在JavaScrip代碼運行時期,你可以把xiaoming從Student變成Bird,或者變成任何對象。
請注意,上述代碼僅用于演示目的。在編寫JavaScript代碼時,不要直接用obj.__proto__去改變一個對象的原型,并且,低版本的IE也無法使用__proto__。Object.create()方法可以傳入一個原型對象,并創建一個基于該原型的新對象,但是新對象什么屬性都沒有,因此,我們可以編寫一個函數來創建xiaoming:
// 原型對象: var Student = { name: "Robot", height: 1.2, run: function () { console.log(this.name + " is running..."); } }; function createStudent(name) { // 基于Student原型創建一個新對象: var s = Object.create(Student); // 初始化新對象: s.name = name; return s; } var xiaoming = createStudent("小明"); xiaoming.run(); // 小明 is running... xiaoming.__proto__ === Student; // true創建對象
JavaScript對每個創建的對象都會設置一個原型,指向它的原型對象。
當我們用obj.xxx訪問一個對象的屬性時,JavaScript引擎先在當前對象上查找該屬性,如果沒有找到,就到其原型對象上找,如果還沒有找到,就一直上溯到Object.prototype對象,最后,如果還沒有找到,就只能返回undefined。
例如,創建一個Array對象:
var arr = [1, 2, 3];
其原型鏈是:
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype定義了indexOf()、shift()等方法,因此你可以在所有的Array對象上直接調用這些方法。
當我們創建一個函數時:
function foo() { return 0; }
函數也是一個對象,它的原型鏈是:
foo ----> Function.prototype ----> Object.prototype ----> null
由于Function.prototype定義了apply()等方法,因此,所有函數都可以調用apply()方法。
很容易想到,如果原型鏈很長,那么訪問一個對象的屬性就會因為花更多的時間查找而變得更慢,因此要注意不要把原型鏈搞得太長。
構造函數除了直接用{ ... }創建一個對象外,JavaScript還可以用一種構造函數的方法來創建對象。它的用法是,先定義一個構造函數:
function Student(name) { this.name = name; this.hello = function () { alert("Hello, " + this.name + "!"); } }
這確實是一個普通函數,但是在JavaScript中,可以用關鍵字new來調用這個函數,并返回一個對象:
var xiaoming = new Student("小明"); xiaoming.name; // "小明" xiaoming.hello(); // Hello, 小明!
注意,如果不寫new,這就是一個普通函數,它返回undefined。但是,如果寫了new,它就變成了一個構造函數,它綁定的this指向新創建的對象,并默認返回this,也就是說,不需要在最后寫return this;。
新創建的xiaoming的原型鏈是:
xiaoming ----> Student.prototype ----> Object.prototype ----> null
也就是說,xiaoming的原型指向函數Student的原型。如果你又創建了xiaohong、xiaojun,那么這些對象的原型與xiaoming是一樣的:
xiaoming ↘ xiaohong -→ Student.prototype ----> Object.prototype ----> null xiaojun ↗
用new Student()創建的對象還從原型上獲得了一個constructor屬性,它指向函數Student本身:
xiaoming.constructor === Student.prototype.constructor; // true Student.prototype.constructor === Student; // true Object.getPrototypeOf(xiaoming) === Student.prototype; // true xiaoming instanceof Student; // true
看暈了吧?用一張圖來表示這些亂七八糟的關系就是:
紅色箭頭是原型鏈。注意,Student.prototype指向的對象就是xiaoming、xiaohong的原型對象,這個原型對象自己還有個屬性constructor,指向Student函數本身。
另外,函數Student恰好有個屬性prototype指向xiaoming、xiaohong的原型對象,但是xiaoming、xiaohong這些對象可沒有prototype這個屬性,不過可以用__proto__這個非標準用法來查看。
現在我們就認為xiaoming、xiaohong這些對象“繼承”自Student。
不過還有一個小問題,注意觀察:
xiaoming.name; // "小明" xiaohong.name; // "小紅" xiaoming.hello; // function: Student.hello() xiaohong.hello; // function: Student.hello() xiaoming.hello === xiaohong.hello; // false
xiaoming和xiaohong各自的name不同,這是對的,否則我們無法區分誰是誰了。
xiaoming和xiaohong各自的hello是一個函數,但它們是兩個不同的函數,雖然函數名稱和代碼都是相同的!
如果我們通過new Student()創建了很多對象,這些對象的hello函數實際上只需要共享同一個函數就可以了,這樣可以節省很多內存。
要讓創建的對象共享一個hello函數,根據對象的屬性查找原則,我們只要把hello函數移動到xiaoming、xiaohong這些對象共同的原型上就可以了,也就是Student.prototype:
修改代碼如下:
function Student(name) { this.name = name; } Student.prototype.hello = function () { alert("Hello, " + this.name + "!"); };
用new創建基于原型的JavaScript的對象就是這么簡單!
忘記寫new怎么辦
如果一個函數被定義為用于創建對象的構造函數,但是調用時忘記了寫new怎么辦?
在strict模式下,this.name =name將報錯,因為this綁定為undefined,在非strict模式下,this.name =name不報錯,因為this綁定為window,于是無意間創建了全局變量name,并且返回undefined,這個結果更糟糕。
所以,調用構造函數千萬不要忘記寫new。為了區分普通函數和構造函數,按照約定,構造函數首字母應當大寫,而普通函數首字母應當小寫,這樣,一些語法檢查工具如jslint將可以幫你檢測到漏寫的new。
最后,我們還可以編寫一個createStudent()函數,在內部封裝所有的new操作。一個常用的編程模式像這樣:
function Student(props) { this.name = props.name || "匿名"; // 默認值為"匿名" this.grade = props.grade || 1; // 默認值為1 } Student.prototype.hello = function () { alert("Hello, " + this.name + "!"); }; function createStudent(props) { return new Student(props || {}) }
這個createStudent()函數有幾個巨大的優點:一是不需要new來調用,二是參數非常靈活,可以不傳,也可以這么傳:
var xiaoming = createStudent({ name: "小明" }); xiaoming.grade; // 1
如果創建的對象有很多屬性,我們只需要傳遞需要的某些屬性,剩下的屬性可以用默認值。由于參數是一個Object,我們無需記憶參數的順序。如果恰好從JSON拿到了一個對象,就可以直接創建出xiaoming。
原型繼承先回顧Student構造函數:
function Student(props) { this.name = props.name || "Unnamed"; } Student.prototype.hello = function () { alert("Hello, " + this.name + "!"); }
現在,我們要基于Student擴展出PrimaryStudent,可以先定義出PrimaryStudent:
function PrimaryStudent(props) { // 調用Student構造函數,綁定this變量: Student.call(this, props); this.grade = props.grade || 1; }
但是,調用了Student構造函數不等于繼承了Student,PrimaryStudent創建的對象的原型是:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
必須想辦法把原型鏈修改為:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null
這樣,原型鏈對了,繼承關系就對了。新的基于PrimaryStudent創建的對象不但能調用PrimaryStudent.prototype定義的方法,也可以調用Student.prototype定義的方法。
如果你想用最簡單粗暴的方法這么干:
PrimaryStudent.prototype = Student.prototype;
是不行的!如果這樣的話,PrimaryStudent和Student共享一個原型對象,那還要定義PrimaryStudent干啥?
我們必須借助一個中間對象來實現正確的原型鏈,這個中間對象的原型要指向Student.prototype。為了實現這一點,參考道爺(就是發明JSON的那個道格拉斯)的代碼,中間對象可以用一個空函數F來實現:
// PrimaryStudent構造函數: function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; } // 空函數F: function F() { } // 把F的原型指向Student.prototype: F.prototype = Student.prototype; // 把PrimaryStudent的原型指向一個新的F對象,F對象的原型正好指向Student.prototype: PrimaryStudent.prototype = new F(); // 把PrimaryStudent原型的構造函數修復為PrimaryStudent: PrimaryStudent.prototype.constructor = PrimaryStudent; // 繼續在PrimaryStudent原型(就是new F()對象)上定義方法: PrimaryStudent.prototype.getGrade = function () { return this.grade; }; // 創建xiaoming: var xiaoming = new PrimaryStudent({ name: "小明", grade: 2 }); xiaoming.name; // "小明" xiaoming.grade; // 2 // 驗證原型: xiaoming.__proto__ === PrimaryStudent.prototype; // true xiaoming.__proto__.__proto__ === Student.prototype; // true // 驗證繼承關系: xiaoming instanceof PrimaryStudent; // true xiaoming instanceof Student; // true
instanceof
instanceof 用于判斷一個變量是否是某個對象的實例,如 var a=new Array();alert(a instanceof Array); 會返回 true,同時 alert(a instanceof Object) 也會返回 true;這是因為 Array 是 object 的子類。再如:function test(){};var a=new test();alert(a instanceof test) 會返回true.
談到 instanceof 我們要多插入一個問題,就是 function 的 arguments,我們大家也許都認為 arguments 是一個 Array,但如果使用 instaceof 去測試會發現 arguments 不是一個 Array 對象,盡管看起來很像。
另外:
測試 var a=new Array();if (a instanceof Object) alert("Y");else alert("N");
得"Y’.
但 if (window instanceof Object) alert("Y");else alert("N");
得"N".
所以,這里的 instanceof 測試的 object 是指 js 語法中的 object,不是指 dom 模型對象。
使用 typeof 會有些區別.
alert(typeof(window)) 會得 object.
注意,函數F僅用于橋接,我們僅創建了一個new F()實例,而且,沒有改變原有的Student定義的原型鏈。
如果把繼承這個動作用一個inherits()函數封裝起來,還可以隱藏F的定義,并簡化代碼:
function inherits(Child, Parent) { var F = function () {}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; } function Student(props) { this.name = props.name || "Unnamed"; } Student.prototype.hello = function () { alert("Hello, " + this.name + "!"); } function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; } // 實現原型繼承鏈: inherits(PrimaryStudent, Student); // 綁定其他方法到PrimaryStudent原型: PrimaryStudent.prototype.getGrade = function () { return this.grade; };
小結
class繼承JavaScript的原型繼承實現方式就是:
定義新的構造函數,并在內部用call()調用希望“繼承”的構造函數,并綁定this;
借助中間函數F實現原型鏈繼承,最好通過封裝的inherits函數完成;
繼續在新的構造函數的原型上定義新方法。
在上面的章節中我們看到了JavaScript的對象模型是基于原型實現的,特點是簡單,缺點是理解起來比傳統的類-實例模型要困難,最大的缺點是繼承的實現需要編寫大量代碼,并且需要正確實現原型鏈。
有沒有更簡單的寫法?有!
新的關鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單。
我們先回顧用函數實現Student的方法:
function Student(name) { this.name = name; } Student.prototype.hello = function () { alert("Hello, " + this.name + "!"); }
如果用新的class關鍵字來編寫Student,可以這樣寫:
class Student { constructor(name) { this.name = name; } hello() { alert("Hello, " + this.name + "!"); } }
比較一下就可以發現,class的定義包含了構造函數constructor和定義在原型對象上的函數hello()(注意沒有function關鍵字),這樣就避免了Student.prototype.hello = function () {...}這樣分散的代碼。
最后,創建一個Student對象代碼和前面章節完全一樣:
var xiaoming = new Student("小明"); xiaoming.hello();
class繼承
用class定義對象的另一個巨大的好處是繼承更方便了。想一想我們從Student派生一個PrimaryStudent需要編寫的代碼量。現在,原型繼承的中間對象,原型對象的構造函數等等都不需要考慮了,直接通過extends來實現:
class PrimaryStudent extends Student { constructor(name, grade) { super(name); // 記得用super調用父類的構造方法! this.grade = grade; } myGrade() { alert("I am at grade " + this.grade); } }
注意PrimaryStudent的定義也是class關鍵字實現的,而extends則表示原型鏈對象來自Student。子類的構造函數可能會與父類不太相同,例如,PrimaryStudent需要name和grade兩個參數,并且需要通過super(name)來調用父類的構造函數,否則父類的name屬性無法正常初始化。
PrimaryStudent已經自動獲得了父類Student的hello方法,我們又在子類中定義了新的myGrade方法。
ES6引入的class和原有的JavaScript原型繼承有什么區別呢?實際上它們沒有任何區別,class的作用就是讓JavaScript引擎去實現原來需要我們自己編寫的原型鏈代碼。簡而言之,用class的好處就是極大地簡化了原型鏈代碼。
你一定會問,class這么好用,能不能現在就用上?
現在用還早了點,因為不是所有的主流瀏覽器都支持ES6的class。如果一定要現在就用上,就需要一個工具把class代碼轉換為傳統的prototype代碼,可以試試Babel這個工具。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/86945.html
摘要:例如,在一個中,刪掉偶數,只保留奇數,可以這么寫把一個中的空字符串刪掉,可以這么寫注意以下的版本沒有方法可見用這個高階函數,關鍵在于正確實現一個篩選函數。回調函數接收的回調函數,其實可以有多個參數。 1.map 由于map()方法定義在JavaScript的Array中,我們調用Array的map()方法,傳入我們自己的函數,就得到了一個新的Array作為結果: function po...
摘要:對象不但充當全局作用域,而且表示瀏覽器窗口。對象有和屬性,可以獲取瀏覽器窗口的內部寬度和高度。對象表示當前頁面的信息。由于在瀏覽器中以形式表示為樹形結構,對象就是整個樹的根節點。這個行為由瀏覽器實現,主流瀏覽器均支持選項,從開始支持。 瀏覽器 目前主流的瀏覽器: IE 6~11:從IE10開始支持ES6標準; Chrome:基于Webkit內核,內置了非常強悍的JavaScript引...
JSON JSON是JavaScript Object Notation的縮寫,它是一種數據交換格式。 道格拉斯·克羅克福特(Douglas Crockford)--雅虎的高級架構師--發明了JSON這種超輕量級的數據交換格式. 序列化 讓我們先把小明這個對象序列化成JSON格式的字符串: var xiaoming = { name: 小明, age: 14, gender...
摘要:在設計時,有兩種比較運算符第一種是比較,它會自動轉換數據類型再比較,很多時候,會得到非常詭異的結果第二種是比較,它不會自動轉換數據類型,如果數據類型不一致,返回,如果一致,再比較。 數據類型和變量 數據類型計算機顧名思義就是可以做數學計算的機器,因此,計算機程序理所當然地可以處理各種數值。但是,計算機能處理的遠不止數值,還可以處理文本、圖形、音頻、視頻、網頁等各種各樣的數據,不同的數據...
摘要:讓我們拆開寫小明正常結果單獨調用函數怎么返回了請注意,我們已經進入到了的一個大坑里。如果單獨調用函數,比如,此時,該函數的指向全局對象,也就是。 函數 1. arguments JavaScript還有一個免費贈送的關鍵字arguments,它只在函數內部起作用,并且永遠指向當前函數的調用者傳入的所有參數。arguments類似Array但它不是一個Array: function fo...
閱讀 1138·2023-04-26 00:12
閱讀 3292·2021-11-17 09:33
閱讀 1073·2021-09-04 16:45
閱讀 1205·2021-09-02 15:40
閱讀 2192·2019-08-30 15:56
閱讀 2976·2019-08-30 15:53
閱讀 3562·2019-08-30 11:23
閱讀 1942·2019-08-29 13:54