摘要:對象對象創建繼承早期創建對象的方式對象字面量創建方式亦可換成因為指向當前對象的兩種屬性數據屬性和訪問器屬性數據屬性數據屬性包含個數據值的位置這個位置可以讀取和寫入值名稱描述表示能否通過刪除屬性而重新定義屬性能否修改屬性的特性或者能否把屬性修
JavaScript OOP, 對象, 對象創建, 繼承
//早期創建對象的方式 var jonslike = new Object(); jonslike.name = "jon"; jonslike.like = "wow"; jonslike.saylike = function(){ console.log(this.name); }; //對象字面量創建方式 var jonslike = { name : "jon", like : "wow", saylike : function(){ console.log(jonslike.like); //亦可換成this, 因為指向當前對象 console.log(this.like); //wow } };
ES的兩種屬性, 數據屬性和訪問器屬性
數據屬性數據屬性包含1個數據值的位置, 這個位置可以讀取和寫入值
名稱 | 描述 |
---|---|
[[ Configurable ]] | 表示能否通過delete刪除屬性而重新定義屬性, 能否修改屬性的特性, 或者能否把屬性修改為訪問器屬性, 默認值true |
[[ Enumerable ]] | 表示能否通過for-in循環返回屬性, 默認值true |
[[ Writable ]] | 表示能否修改屬性的值, 默認值true |
[[ Value ]] | 包含這個屬性的數據值. 讀取屬性值的時候, 從這個位置讀; 寫入屬性值的時候, 把新值保存在這個位置, 默認值undefined |
var person = { name : Jon, //value值變成Jon };
接收3個參數, 屬性所在的對象, 屬性的名字, 一個描述符對象
描述符對象的屬性必須是 : configurable, enumerable, writable, value
//設置為不可寫 : var person = {}; person.name = "fire"; Object.defineProperty(person, "name", { writable : false, //設置為只讀, 不可寫 value : "jon" }); person.name = "mark"; alert(person.name); //輸出還是jon
//設置為不可配置 : var person = {}; Object.defineProperty(person, "name" , { configurable : false, //設置為不可配置 value : "jon", }); person.name = "mark"; //無效! delete person.name; //刪除無效! alert(person.name); //依然能輸出jon //NOTE : 配置configurable為false時, 其他3各特性也有相應的限制訪問器屬性
訪問器屬性不包括數據值;
包含一對getter 與 setter 函數 (非必須)
讀取訪問器屬性時, 會調用getter函數,該函數負責返回有效的值;
寫入訪問器屬性時, 會調用setter函數,該函數負責決定如何處理數據;
名稱 | 描述 |
---|---|
[[ Configurable ]] | 表示能否通過delete刪除屬性而重新定義屬性, 能否修改屬性的特性, 或者能否把屬性修改為訪問器屬性, 默認值true |
[[ Enumerable ]] | 表示能否通過for-in循環返回屬性, 默認值true |
[[ Get ]] | 在讀取屬性時調用的函數, 默認值undefined |
[[ Set ]] | 在寫入屬性時調用的函數, 默認值undefined |
定義訪問器屬性 : 使用Object.defineProperty()方法.
//創建book對象, var book = { //定義兩個默認的屬性, _year和edition, 下劃線定義的屬性表示只能通過對象方法訪問 _year : 2004, edition : 1 }; Object.defineProperty(book, "year", { get : function(){ return this._year; }, set : function(){ if(newValue > 2004){ this._year = newValue; this.edition += newValue - 2004; } }, }); book.year = 2005; alert(book.edition); //2
定義多個屬性 : defineProperties()
可以通過描述符一次過定義多個屬性
接收兩個參數 :
要添加和修改其屬性的對象
第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應.
var book = {}; Object.defineProperties(book, { _year : { value : 2004, }, edition : { value : 1, }, year : { get : funciton(){ return this._year, }, set : function(){ if(newValue > 2004){ this._year = newValue, this.edition += newValue - 2004, } } } });
讀取屬性的特性 : 使用Object.getOwnPropertyDescriptor();
可以取得給定屬性的描述符
接收兩個參數 :
屬性所在的對象, 要讀取其描述符的屬性名稱
返回值 : 一個對象, 如果返回的對象是訪問器屬性, 則這個對象的屬性有configurable, enumerable, get, set; 如果返回的對象是數據屬性, 則這個對象的屬性有configurable, enumerable, writable, value
var book = {}; Object.defineProperties(book, { _year : { value : 2004, }, edition : { value : 1, }, year : { get : funciton(){ return this._year, }, set : function(){ if(newValue > 2004){ this._year = newValue, this.edition += newValue - 2004, } } } }); var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); //數據屬性 alert(descriptor.value); //2004(最初的值) alert(descriptor.configurable); //false(最初的值) alert(typeof descriptor.get); // undefined var descriptor = Object.getOwnPropertyDescriptor(book, year); //訪問器屬性 alert(descriptor.value); // undefined(訪問器沒有value屬性) alert(descriptor.enumerable); // false alert(typeof descriptor.get); // function(一個指向getter的指針)創建對象 工廠模式
工廠模式抽象了創建具體對象的過程;
該模式沒有解決對象的識別問題(即怎樣知道一個對象的類型)
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayJob = function(){ console.log(this.job); }; return o; } var p1 = createPerson("Jon",25,"FrontEnd Developer"); var p2 = createPerson("Mark",24,"DBA"); p1.sayJob(); //FrontEnd Developer p2.sayJob(); //DBA構造函數模式
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayJob = function(){ console.log(this.job); }; } //使用new操作符創建Person的新實例 /* 調用構造函數會經歷以下步驟 : 1. 創建一個新對象; 2.將構造函數的作用域賦給新對象(因此this就指向了這個新對象) 3.執行構造函數中的代碼(為這個新對象添加屬性) 4.返回新對象 */ var p1 = new Person("Jon", 25, "FrontEnd Developer"); var p2 = new Person("Mark", 24, "DBA"); p1.sayJob(); //FrontEnd Developer p2.sayJob(); //DBA //新對象具有一個constructor(構造函數)屬性, 指向原創建的構造函數(即Person) console.log(p1.constructor == Person); //true console.log(p2.constructor == Person); //true //使用instanceof操作符檢測對象類型會更可靠 console.log(p1 instanceof Object); //Object是終極父類, 所以返回true console.log(p1 instanceof Person); //p1是Person構造函數的實例 console.log(p2 instanceof Object); //Object是終極父類, 所以返回true console.log(p2 instanceof Person); //p2是Person構造函數的實例 //構造函數本身也是函數, 所以可以當做普通函數來調用(不使用new操作符調用) Person("Martin", 27, "PHPer"); //添加到window對象(全局作用域中) window.sayJob(); //PHPer //在另一個對象的作用域調用(使用call()或者apply()) var o1 = new Object(); Person.call(o1, "Kiki", 23, "Singer"); o1.sayJob(); //Singer原型模式
每個函數都有一個prototype(原型)屬性, 是一個指針, 指向一個對象
對象的用途是包含可以由特定類型的所有實例共享的屬性和方法;
prototype就是通過調用構造函數而創建的那個對象實例的對象
使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法;
即 :?
不必在構造函數中定義對象實例的信息, 而是可以將這些信息直接添加到原型對象中;
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "f2e"; Person.prototype.sayName = function(){ alert(this.name); }; var p1 = new Person(); p1.sayName(); //jon var p2 = new Person(); p2.sayName(); //jon alert(p1.sayName == p2.sayName); //true
無論何時, 只要創建了一個新函數, 就會根據一組特定的規則為該函數創建一個prototype屬性, 該屬性指向函數的 原型對象
即 : (新函數會創建一個prototype屬性指向原型對象)
默認情況下, 所有原型對象會自動獲得一個constructor構造函數屬性, 該屬性包含指向prototype屬性所在函數的指針
即 : (所有原型對象獲得一個constructor(構造函數)屬性,包含指向prototype屬性所在函數的指針)
function Person(){}; //這是(空)構造函數,會有一個prototype屬性,指向(下面的)原型對象 //Person.prototype : 這是(構造函數的)原型對象, 會自動獲得一個constructor(構造函數)屬性, 包含一個指向prototype屬性所在函數的指針,在這里即上面的Person()函數; 即Person.prototype.constructor指向(上面的)Person()函數 //下面這些是(構造函數的)原型對象的自定義屬性s Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "f2e"; Person.prototype.sayName = function(){ alert(this.name); }; //這是實例,內部包含一個指針(內部屬性) [[Prototype]], 指向構造函數的原型對象(即上面的Person.prototype) var p1 = new Person(); p1.sayName(); //jon var p2 = new Person(); p2.sayName(); //jon alert(p1.sayName == p2.sayName); //true
isPrototypeOf() : 確定是否為給定實例的原型
getPrototypeOf() [ES5] : 跟上面的功能一樣, 并且這方法可以返回原型對象給定屬性的值
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.sayJob(); //FrontEnd Developer //測試Person是否為p1的原型 console.log(Person.prototype.isPrototypeOf(p1)); //true //如果支持ES5的getPrototypeOf() if(Object.getPrototypeOf){ //測試Person是否為p1的原型 console.log(Object.getPrototypeOf(p1) == Person.prototype); //true //輸出p1的name屬性的值 console.log(Object.getPrototypeOf(p1).name); //Jon }
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.job = "DBA"; p1.sayJob(); //DBA
delete操作符可以刪除實例的屬性
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); p1.job = "DBA"; p1.sayJob(); //返回自身添加的屬性, DBA delete p1.job; //刪除p1的job屬性 p1.sayJob(); //返回原型的屬性, FrontEnd Developer
hasOwnProperty()可以檢查一個屬性是位于實例還是原型中, 屬于實例會返回true
in操作符會在對象能訪問給定屬性時返回true,無論是實例還是原型 : (就是有這個屬性就會返回true)
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(); console.log(p1.hasOwnProperty("name")); //實例中沒有自己定義的name屬性, 返回false console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的) p1.name = "Mark"; //自己定義一個實例中的name屬性, 覆蓋原型繼承而來的name console.log(p1.hasOwnProperty("name")); //實例中有自己定義的name屬性(Mark), 返回true console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的) delete p1.name; //刪除p1實例的name屬性 console.log(p1.hasOwnProperty("name")); //p1的name屬性已經被delete操作符刪除, 所以現在又沒了自身實例的name屬性, 所以返回false console.log("name" in p1); //true, p1中有name屬性(從Person中的name繼承而來的)
可以同時使用hasOwnProperty()和in操作符, 以確定給定的屬性是位于實例還是原型中 :
in操作符只要能訪問給定屬性就返回true, hasOwnProperty()只在屬性屬于實例才返回true,
因此只要in操作符返回true而hasOwnProperty()返回false, 就能確定給定的屬性是原型的屬性
//obj表示要傳入的實例名稱, name表示要測試的實例屬性 function hasPrototypeProperty(obj, name){ //如果傳入的實例屬性name不屬于該實例obj(取反), 并且(&&)實例obj中有該傳入的屬性name, 則返回 return !obj.hasOwnProperty(name) && (name in obj); } function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; var p1 = new Person(hasPrototypeProperty(p1, "name")); console.log(hasPrototypeProperty(p1, "job")); //p1中還沒有定義實例的job屬性, 只使用了原型繼承而來的job屬性, 所以返回true (hasOwnProperty()返回!false(取反false, 即true), in操作符返回true) p1.job = "DBA"; //p1定義自身的實例屬性job console.log(hasPrototypeProperty(p1, "job")); //false (!hasOwnProperty(job)為 !true,即false, in返回true)
使用for-in返回能通過對象訪問的, 可枚舉的屬性(包括原型內和實例內的) :
var o = { name : "Jon", age : 25, saySth : function(){} } for(var prop in o){ if(prop){ console.log(prop); } } //name, age, saySth
Object.keys() [ES5]可獲得所有可枚舉的屬性 :
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //獲得原型中所有可枚舉的屬性 var protoKeys = Object.keys(Person.prototype); console.log(protoKeys); //"name", "age", "job", "sayJob" var p1 = new Person; p1.name = "Mark"; p1.nickname = "MM"; p1.age = 24; p1.fakeAge = 21; p1.job = "DBA"; p1.sayJob(); //如果通過實例調用, 則會得到該實例中所有可枚舉的屬性 var keys = Object.keys(p1); console.log(keys); //"name", "nickname", "age", "fakeAge", "job"
Object.getOwnPropertyNames()可以得到所有無論是否可枚舉的屬性
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //獲得原型中所有屬性(無論是否可枚舉) var protoKeys = Object.getOwnPropertyNames(Person.prototype); console.log(protoKeys); //"constructor", "name", "age", "job", "sayJob"
Object.keys() 和 Object.getOwnPropertyNames()都可以替代for-in循環 (IE9+, ...)
使用對象字面量來創建新對象
function Person(){} //這種方式其實已經重寫了默認的prototype對象, 此時constructor屬性已經不再指向Person了, 而是指向了Object Person.prototype = { name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //所以此時雖然instanceof操作符還能返回正確的結果, 但constructor已經無法確定對象的類型了 var f1 = new Person(); console.log(f1 instanceof Person); //true console.log(f1 instanceof Object); //true console.log(f1.constructor == Person); //false console.log(f1.constructor == Object); //true //如果constructor的值很重要, 可以像這樣把它設置回適當的值 //(修改上面的Person.prototype) Person.prototype = { constructor : Person, //顯式的把constructor設置為Person name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //如果像上面一樣把constructor的值顯式的設置, 那么它會變成可枚舉, 即[[Enumerable]]的值會變為true, 如果要把它設置回不可枚舉, 可以使用下面的ES5提供的新方法 : //重寫整個示例 function Person(){} Person.prototype = { name : "Jon", age : 25, job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //重設構造函數[ES5 only] Object.defineProperty(Person.prototype, "constructor", { enumerable : false, value : Person });
原型的動態性
原型中查找值的方法是一次搜索, 所謂動態性就是在原型對象上所有的修改都能立即從實例上反應出來, 即使是 先創建實例, 后修改原型 也是如此
function Person(){}; Person.prototype.name = "Jon"; Person.prototype.age = 25; Person.prototype.job = "FrontEnd Developer"; Person.prototype.sayJob = function(){ console.log(this.job); }; //創建原型實例 var p1 = new Person(); //創建實例后再創建原型方法 Person.prototype.sayAge = function(){ console.log(this.age); } //調用后創建的原型方法 p1.sayAge(); //照樣能工作! 輸出25
//但不能在創建原型實例后, 重寫整個原型對象 function Person(){} //創建原型實例 var p1 = new Person(); //此時再定義Person的原型對象 Person.prototype = { constructor : Person, name : "Jon", job : "FrontEnd Developer", sayJob : function(){ console.log(this.job); } }; //記住, 實例的指針[[ prototype ]]僅指向原型, 而不指向構造函數 p1.sayJob(); //出錯! Uncaught TypeError: p1.sayJob is not a function
原生對象的原型
所有原生的引用類型(Object, Array, String, etc...), 都是使用這種原型模式創建的, 都在其構造函數上定義了方法
通過原生對象的原型, 不僅可以取得所有默認方法的引用, 而且也可以定義新方法. 可以像修改自定義對象的原型一樣修改原生對象的原型: 即可以隨時添加方法(但不推薦) :
console.log(typeof Array.prototype.sort); //function console.log(typeof String.prototype.substr); //function //為原生引用類型String添加方法(不推薦) : String.prototype.startsWith = function(text){ return this.indexOf(text) == 0; } var s1 = "Hi Jon"; console.log(s1.startsWith("Hi")); //true
原型模式的問題 :
省略了為構造函數初始化參數的環節, 導致所有新建的實例都會取得相同的默認值
最大的問題是其共享的本性所導致的, 對于引用類型值的屬性來說問題非常突出 :
function Person(){} Person.prototype = { constructor : Person, name : "Jon", job : "FrontEnd Developer", friends : ["Lucy","Jeniffer"], sayJob : function(){ console.log(this.job); } }; var p1 = new Person(); var p2 = new Person(); p1.friends.push("Quinene"); console.log(p1.friends); //"Lucy", "Jeniffer", "Quinene" console.log(p2.friends); //"Lucy", "Jeniffer", "Quinene" console.log(p1.friends === p2.friends); //true組合使用構造函數模式和原型模式(最常用)
構造函數模式用于定義實例屬性, 原型模式用于定義方法和共享的屬性 :
結果每個實例都有自己的一份實例屬性的副本, 但同時又共享著對方法的引用,最大限度的節省了內存:
這種模式還支持向構造函數傳參 :
function Person(name, age, job){ //定義實例屬性(將來創建實例時不會相同的屬性s) this.name = name; this.age = age; this. job = job; this. friends = ["Mark", "Martin"]; } Person.prototype = { //構造函數屬性指回Person cosntructor : Person, //定義方法 sayJob : function(){ console.log(this.job); }, //定義共享屬性 country : "China" }; //創建實例 var p1 = new Person("Jon", 25, "FrontEnd Developer"); var p2 = new Person("Percy", 26, "DBA"); //為實例p1的friends屬性添加值 p1.friends.push("Jeniffer"); console.log(p1.friends); //"Mark", "Martin", "Jeniffer" console.log(p2.friends); //"Mark", "Martin" console.log(p1.friends == p2.friends); //false console.log(p1.sayJob == p2.sayJob); //true console.log(p1.country == p2.country); //true動態原型模式
動態原型模式把所有信息都封裝在構造函數中, 而通過在構造函數中初始化原型(僅在必要的情況下), 又保持了同時使用構造函數和原型的優點 :?
即 可以通過檢查某個應該存在的方法是否有效, 來決定是否需要初始化原型
function Person(name, age, job){ //屬性 this.name = name; this.age = age; this.job = job; //方法 if(typeof this.sayJob != "function"){ Person.prototype.sayJob = function(){ console.log(this.job); } } } var p1 = new Person("Jon", 25, "F2E"); p1.sayJob(); //F2E
這里只在sayJob()方法不存在的情況下, 才會將它添加到原型中.
這段代碼只會在初次調用構造函數時才會執行.
這里對原型所做的修改, 也會立即在所有實例中得到反映.
if語句檢查的可以是初始化之后應該存在的任何屬性和方法—— 不必用一大堆if語句判斷每個屬性的方法,只要其中檢查一個即可;
這種模式創建的對象可以用instanceof操作符確定它的類型
寄生構造函數模式基本思路是創建一個函數, 這個函數作用僅僅是封裝創建對象的代碼, 然后再返回新創建的對象.
function Person(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayJob = function(){ console.log(this.name); }; return o; } var p1 = new Person("Jon", 25, "F2E"); p1.sayJob(); //F2E
Person函數創建了一個新對象o, 并以相應的屬性和方法初始化該對象, 然后把它返回.
除了使用new操作符并把使用的包裝函數叫做構造函數外, 這個模式跟工廠模式其實是一樣的.
構造函數在不返回值的情況下, 默認會返回新對象的實例, 而通過在構造函數的末尾添加一個return語句, 可以重寫調用構造函數時返回的值.
這種模式在特殊的情況下用來為對象創建構造函數.假設我們想創建一個具有額外方法的特殊數組,
因為不能直接修改Array構造函數, 因此可以使用這種模式 :
function SpecialArray(){ //創建一個數組用于接收傳入的值 var values = new Array(); //然后使用push方法(用構造函數接收到的所有參數)初始化了數組的值; values.push.apply(values, arguments); //給數組實例添加了一個toPipedString()方法, 該方法返回以短橫線分割的數組值; values.toPipedString = function(){ return this.join("-"); }; //將數組以函數值的形式返回. return values; } var colorsArr = new SpecialArray("red", "blue", "purple"); console.log(colorsArr.toPipedString()); //red-blue-purple //關于該模式 : 首先, 返回的對象與構造函數或者構造函數的原型屬性之間沒有關系;也就是說, 構造函數返回的對象與在構造函數外部創建的對象沒有什么不同.為此不能依賴instance操作符來確定對象類型. console.log(colorsArr instanceof SpecialArray); //false穩妥構造函數模式
穩妥對象 : 沒有公共屬性, 其方法也不引用this的對象.
適合在安全的環境中(禁止使用this和new), 或者在防止數據被其他應用程序改動時使用
穩妥構造函數遵循與寄生構造函數類似的模式, 但有兩點不同 :?
一是新創建對象的實力方法不引用this,
二是不適用new操作符調用構造函數 :
function Person(name, age, job){ //創建要返回的對象 var o = new Object(); //可以在這里定義私有變量和方法 //添加方法 o.sayJob = function(){ console.log(job); } //返回對象 return o; } /*這種方式創建的對象中, 除了使用sayJob()方法外, 沒有其他辦法訪問job的值*/ //使用穩妥的Person構造函數 var p1 = new Person("Jon", 25, "FrontEnd Developer"); p1.sayJob(); //FrontEnd Developer console.log(p1.job); //嘗試直接訪問job屬性會返回undefined繼承
許多OO語言都支持兩種繼承方式 :?
接口繼承 : 只繼承方法簽名
實現繼承 : 繼承實際的方法
由于函數沒有簽名, 在ES中無法實現接口繼承.ES只支持實現繼承, 而且其 實現繼承 主要是依靠原型鏈實現的.
方法簽名由方法名稱和一個參數列表(方法的參數的順序和類型)組成。
方法簽名應該如下所示,相應的可變參數分別使用String和Exception聲明:
Log.log(String message, Exception e, Object... objects) {...}
原型鏈利用原型讓一個引用類型繼承另一個引用類型的屬性和方法.
簡單回顧下構造函數, 原型, 實例的關系 :?
每個構造函數都有一個原型對象( prototype ), 原型對象都包含一個指向構造函數的指針( constructor ), 而每個實例都包含一個指向原型對象的內部指針( [[ prototype ]], __proto__ )
那么,假如我們讓原型對象(prototype)等于另一個類型的實例, 那么此時的原型對象將包含一個指向另一個原型的指針.
相應地, 另一個原型中也包含著一個指向另一個構造函數的指針.
假如另一個原型又是另一個原型的實例, 那么上述關系依然成立, 如此層層遞進, 就構成了實力與原型的鏈條, 這就是所謂的原型鏈的概念.
實現原型鏈的基本模式 :
/*定義兩個類型, SuperType和SubType*/ function SuperType(){ //SuperType自己的屬性 this.property = true; } SuperType.prototype.getSuperValue = function(){ //SuperType自己的方法 return this.property; } function SubType(){ //SubType自己的屬性 this.subproperty = false; } /*SupType通過創建SuperType()的實例繼承了SuperType, 并賦值給SubType.prototype, 即SubType的原型對象實現的本質是重寫原型對象, 代之以一個新類型的實例. 換句話說, 原來存在于SuperType的實例中的所有屬性和方法, 現在也存在于SubType.prototype中了 */ SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ //添加SubType自己的方法, 這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法 return this.subproperty; } //創建一個新實例 var instance = new SubType(); console.log(instance.getSuperValue()); //true //測試是否為Object, SuperType, SubType的實例 console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
關系如圖所示 :?
最終結果 :
instance實例指向SubType的原型, SubType的原型又指向SuperType的原型.
getSuperValue()方法仍然還在SuperType.prototype中, 但property則位于SubType.prototype中.
這是因為, property是一個實例屬性,而getSuperType()則是一個原型方法
既然SubType.prototype現在是SuperType的實例, 那么prototype當然就位于該實例中了.
要注意,實例的 instance.constructor現在指向的是SuperType, 這是因為SubType的原型現在指向了另一個對象—— SuperType的原型.
而這個原型對象的constructor屬性指向的是SuperType.
所有引用類型默認都繼承了Object, 而這個繼承也是通過原型鏈實現的.
要記住, 所有函數的默認原型都是Object的實例, 因此默認原型都會包含一個內部指針指向Object.prototype.
這也正是所有自定義類型都會繼承toString(), valueOf()的根本原因.
所以, 上面例子展示的原型鏈應該還包含另一個繼承層次 : (完整的原型鏈如下)
使用instanceof 操作符, 測試實例和原型鏈中出現過的構造函數, 如果存在就會返回true
使用isPrototypeOf() 方法, 只要是原型鏈中出現過的原型, 都可以說是該原型鏈所派生的實例的原型,因此該方法會返回true
//上面第一段代碼的最后片段 : console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
給原型添加方法的代碼一定要放在替換原型的語句之后 :
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //從SuperType繼承 SubType.prototype = new SuperType(); //SubType自己的新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; } //SubType繼承的父類方法getSuperValue()被重寫, 但只會重寫SubType自身的getSuperValue(), 不會影響上一級父類原來的方法, 即如果調用的是SuperType的getSuperValue()方法的話還是會返回原來的true. SubType.prototype.getSuperValue = function(){ return false; } //創建實例 var ins1 = new SubType(); var ins2 = new SuperType(); console.log(ins1.getSuperValue()); //false, 重寫的方法 console.log(ins2.getSuperValue()); //true, SubType重寫getSuperValue()方法并不會影響父類原有的方法
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //從SuperType繼承 SubType.prototype = new SuperType(); /*剛剛把SuperType的實例賦值給SubType的原型??, 又使用對象字面量??把原型替換 SubType.prototype = {...}, 所以現在SubType的原型包含的是 一個屬于Object的實例而不是SuperType的, 原先的原型鏈已經被切斷, SubType與SuperType已經沒有任何關系了*/ //使用對象字面量把 原型替換 SubType.prototype = { getSubValue : function(){ return this.subproperty; }, someOtherMethod : function(){ return false; } } var ins1 = new SubType(); console.log(ins1.getSuperValue()); //Uncaught TypeError: ins1.getSuperValue is not a function
最主要的問題來自包含引用類型值的原型.
之前說過, 包含引用類型值的原型屬性會被所有實例共享.
而這也是為什么要在構造函數中, 而不是原型對象中定義屬性的原因.
第二個問題是, 在創建子類型的實例時, 不能向超類型的構造函數傳遞參數
在通過原型來實現繼承時, 原型實際上會變成另一個類型是實例( SubType.prototype = new SuperType(); ), 于是, 原先的實例屬性也就順理成章的變成了現在的原型屬性了.
function SuperType(){ this.colors = ["red", "green", "blue"]; } function SubType(){} SubType.prototype = new SuperType(); var ins1 = new SubType(); console.log(ins1.colors); // "red", "green", "blue" //在ins1添加colors屬性的屬性值 ins1.colors.push("purple"); console.log(ins1.colors); //"red", "green", "blue", "purple" var ins2 = new SubType(); //ins1中添加到colors中的屬性值直接被添加到了SubType()的原型屬性里面, 導致后來新增的實例也繼承了這些屬性 console.log(ins2.colors); //"red", "green", "blue", "purple"]
思路 : 在子類型構造函數的內部調用超類型的構造函數.
函數只不過是在特定環境中執行代碼的對象, 因此可以通過apply()和call()方法也可以在(將來)新創建的對象上執行構造函數
function SuperType(){ this.colors = ["green", "blue", "purple"]; } function SubType(){ //繼承自SuperType //當SubType(){...}被實例化后, SuperType()函數中定義的所有對象初始化代碼就會被執行 SuperType.call(this); } var ins1 = new SubType(); ins1.colors.push("red"); console.log(ins1.colors); //"green", "blue", "purple", "red" var ins2 = new SubType(); console.log(ins2.colors); //"green", "blue", "purple"
//相比原型鏈, 借用構造函數還有一個很大的優勢, 就是子類型的構造函數可以向超類型的構造函數傳遞參數 function SuperType(name){ //父類構造函數接受一個name函數, 并賦值給一個屬性 this.name = name; } function SubType(){ /*在SubType()構造函數中調用SuperType()構造函數時, 實際上是為SubType的實例設置了name屬性*/ SuperType.call(this, "Jon"); /*為了確保SuperType構造函數不會重寫子類型的屬性, 可以在調用父類構造函數后,再添加應該在子類型中定義的屬性*/ this.age = 25; } var ins1 = new SubType(); console.log(ins1.name); //Jon console.log(ins1.age); //25
如果僅僅是借用構造函數, 那么也無法避免構造函數模式存在的問題—— 方法都在構造函數內部定義, 那么函數復用就無從談起了.
而且在超類型的原型中定義的方法, 對子類型而言也是不可見的, 結果所有類型都只能使用構造函數模式.
所以這種方式也是很少多帶帶使用的
組合繼承( 偽經典繼承 )指的是將 原型鏈 與 借用構造函數 的技術組合到一塊, 從而發揮二者之長的一種繼承模式.
思路是, 使用 原型鏈 實現 對原型屬性和方法的繼承 , 而通過 借用構造函數 來實現對 實例屬性的繼承
這樣, 既通過在原型上定義方法實現了函數復用, 又能夠保證每個實例都有自己的屬性. 所以這成為JavaScript中常用的繼承方式
function SuperType(name){ //父類定義兩個屬性name和colors this.name = name; this.colors = ["blue", "red", "yellow"]; } //父類定義原型方法sayName SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ //SubType構造函數在調用SuperType構造函數時傳入了name參數 SuperType.call(this, name); //然后定義自己的屬性age this.age = age; } //將SuperType的實例賦值給SubType的原型 SubType.prototype = new SuperType(); //name, colors[], sayName() SubType.prototype.constructor = SubType; //構造函數指回自己 //在該新原型上定義了方法sayAge() SubType.prototype.sayAge = function(){ console.log(this.age); } //兩個不同的SubType實例既分別擁有自己的屬性————包括colors屬性, 又可以使用相同的方法了 var ins1 = new SubType("Jon", 25); ins1.colors.push("purple"); console.log(ins1.colors); //"blue", "red", "yellow", "purple" ins1.sayName(); //Jon ins1.sayAge(); //25 var ins2 = new SubType("Mark", 24); console.log(ins2.colors); //"blue", "red", "yellow" ins2.sayName(); //Mark ins2.sayAge(); //24原型式繼承
借助原型可以基于已有的對象創建新對象, 同時還不必因此創建自定義類型.
/* 在object()函數內部, 先創建了一個臨時性的構造函數F(){}, 然后將傳入的對象o作為這個構造函數F(){}的原型, 最后返回了這個臨時類型的新實例. 從本質上講, object()對傳入其中的對象o執行了一次淺復制 */ /* 這種繼承方式要求你必須有一個對象可以作為另一個對象的基礎, 把它傳給object()函數,然后再根據具體需求對得到的對象加以修改即可. */ function object(o){ function F(){} F.prototype = o; return new F; } /* 這個例子中, 可以作為另一個對象的基礎是person對象 */ var person = { name : "Jon", colorsLike : ["black", "white"] } /* 把它(person對象)傳入到object()函數中, 然后該函數就會返回一個新對象( anotherPerson1 和 anotherPerson2 ), 這兩個新對象把person作為原型, 所有它們的原型中就包含一個基本類型值屬性和 一個引用類型值屬性,這意味著person.colorsLike不僅于person所有,同時也會被 anotherPerson1, anotherPerson2共享, 實際上, 就相當于又創建了person對象的兩個副本 */ var anotherPerson1 = object(person); //anotherPerson1現在有了person的所有屬性(這里是name和colorsLike[]) console.log(anotherPerson1.name); //person原有的name屬性值, 輸出Jon console.log(anotherPerson1.colorsLike); //person原有的colorsLike[]數組, 輸出["black", "white"] anotherPerson1.name = "Percy"; //修改anotherPerson1的name屬性為自己的值 anotherPerson1.colorsLike.push("purple"); //添加anotherPerson1自己喜歡的顏色 console.log(anotherPerson1.name); //Percy console.log(anotherPerson1.colorsLike); //["black", "white", "purple"] console.log(person.name); //Jon console.log(person.colorsLike); //person的colorsLike數組值已經被anotherPerson1添加的屬性影響, 此時也輸出了["black", "white", "purple"] var anotherPerson2 = object(person); console.log(anotherPerson2.name); //Jon console.log(anotherPerson2.colorsLike); //["black", "white", "purple"] anotherPerson2.colorsLike.push("red"); //再push一個 console.log(anotherPerson2.colorsLike); //["black", "white", "purple", "red"] console.log(person.colorsLike); //再度被anotherPerson2新增的值影響, 輸出["black", "white", "purple", "red"]
ES5新增了一個方法Object.create()規范了原型式繼承, 該方法接收兩個參數, 一個用作新對象的原型的對象和一個(可選)一個為新對象定義額外屬性的對象
瀏覽器支持, IE9+和各現代瀏覽器
還是直接看例子比較直觀 :
傳入一個參數的時候, 這個方法跟上面object()方法的行為相同 :
var person = { name : "Jon", colorsLike : ["black", "white"] }; console.log(person.colorsLike); // ["black", "white"] //傳入一個參數的時候, 這個方法跟上面object()方法的行為相同 var anotherPerson = Object.create(person); anotherPerson.name = "Percy"; anotherPerson.colorsLike.push("purple"); console.log(anotherPerson.name); //Percy console.log(anotherPerson.colorsLike); //["black", "white", "purple"] console.log(person.name); //Jon console.log(person.colorsLike); //["black", "white", "purple"]
傳入兩個參數的時候, 第二個參數與Object.defineProperties()方法的第二個參數格式相同 : 每個屬性都是通過自己的描述符定義的, 以這種方式指定任何屬性都會覆蓋原型對象上的同名屬性
var person = { name : "Jon", colorsLike : ["black", "white"] }; console.log(person.colorsLike); // ["black", "white"] //傳入兩個參數 var anotherPerson = Object.create(person, { name : { value : "Martin" } }); console.log(anotherPerson.name); //Martin寄生式繼承
與原型式繼承緊密相關的思路, 與寄生構造函數和工廠模式類似, 即創建一個僅用于封裝繼承過程的函數, 該函數在內部以某種形式來增強對象, 最后再像真的是它做了所有工作一樣返回對象
function object(o){ function F(){} F.prototype = o; return new F; } function createAnother(original){ var clone = object(original); //通過調用函數創建一個新對象 clone.sayHi = function(){ //以某種方式增強這個對象(添加自身方法或者屬性等) console.log("Good Day!"); }; return clone; //返回該對象 } var person = { name : "Jon", friends : ["Martin", "Jeniffer"] }; /* 這個實例中的代碼 基于person 返回了一個新對象————anotherPerson 該對象不僅具有person所有屬性和方法, 而且還有自己的sayHi()方法 */ /* 在主要考慮對象而不是自定義類型和構造函數的情況下, 寄生式繼承也是一種有用的方式, 前面示范繼承模式使用的object()函數并不是必須的, 任何能夠返回新對象的函數都適用于此模式 */ var anotherPerson = createAnother(person); anotherPerson.sayHi(); //Good Day!寄生組合式繼承
前面說過, 組合繼承是JS中最常用的繼承模式, 不過它也有自己的不足
組合繼承 最大的問題是, 無論在什么情況下, 都會調用兩次父類型構造函數, 一次是在創建子類型原型的時候, 一次是在子類型構造函數內部 :
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"] } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); //第二次調用SuperType() this.age = age; } /* 第一次 調用SuperType構造函數時, SubType.prototype會得到兩個屬性, name和colors[], 它們都是SuperType的實例屬性, 只不過現在位于SubType的原型中; 當調用SubType構造函數時, 又會再一次調用一次SuperType構造函數, 這一次又在新對象上創建了實例屬性name和colors[], 于是, 這兩個屬性就遮蔽了原型中的兩個同名屬性 */ SubType.prototype = new SuperType(); //第一次調用SuperType() SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }
如下圖 :
寄生組合式繼承, 即通過借用構造函數來繼承屬性, 通過原型鏈的混成模式來繼承方法.
思路是, 不必為了指定子類型的原型而調用構造超類型的構造函數, 我們所需要的無非就是超類型的一個副本而已 ??
本質上, 就是使用寄生式繼承來繼承超類型的原型, 然后再將結果指定給子類型的原型.
基本模式如下所示. ??
function object(o){ function F(){} F.prototype = o; return new F(); } /* 寄生組合式繼承的最簡單形式, 這個函數接收兩個參數, 子類型構造函數和超類型構造函數; 在函數內部,第一步是創建超類型原型的一個副本, 第二步是為創建的的副本添加constructor屬性, 從而彌補因重寫而失去默認的constructor屬性; 最后一步, 將新創建的對象(即副本)賦值給子類型的原型,這樣我們就可以調用inheritPrototype()函數的語句,去替換前面例子中未知類型原型賦值的語句了(41行) */ function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //創建對象 prototype.constructor = subType; //增強對象 subType.prototype = prototype; //指定對象 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); //調用inheritPrototype()函數 SubType.prototype.sayAge = function(){ console.log(this.age); }; var instance1 = new SubType("Jon", 25); instance1.colors.push("black"); console.log(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Jon" instance1.sayAge(); //25 var instance2 = new SubType("Mark", 24); console.log(instance2.colors); //"red,blue,green" instance2.sayName(); //"Mark" instance2.sayAge(); //24函數表達式
第一種是函數聲明 :
function Person(name){ this.name = name; console.log("name is " + this.name); } Person("Jon"); //name is Jon //函數聲明支持函數聲明提升, 即執行代碼前會先讀取函數聲明, 那么函數聲明可以放在調用它的代碼之后而不出錯 : Person("Jon"); //works ! 輸出name is Jon function Person(name){ this.name = name; console.log("name is " + this.name); }
第二種是函數表達式 :
var Person = function(name){ this.name = name; console.log(this.name); } Person("Jon"); //Jon //函數表達式不支持函數聲明提升 Person("Jon"); //Uncaught TypeError: Person is not a function var Person = function(name){ this.name = name; console.log(this.name); }
要在使用條件語句后面執行函數的話, 條件語句內的函數必須使用函數表達式的方式定義, 如果使用函數聲明方式定義, 會在不同的瀏覽器導致不同問題的發生 :
//條件語句內的函數定義必須使用函數表達式 var b = true; if(b){ sayColors = function(){ console.log(this.color); }; }else{ console.log("error!"); }
function createComparisonFunction(propertyName){ //這里返回的就是匿名函數, 它能賦值給一個變量, 或者以其他的方式調用 return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; }else if(value1 > value2){ return 1; }else{ return 0; } }; }遞歸
遞歸函數是一個函數通過名字調用自身的情況下構成的
//遞歸階乘函數 function factorial(num){ if(num <= 1){ return 1; }else{ return num * factorial(num - 1); } } //注意如下調用會產生錯誤 var anotherFactorial = factorial; //把factorial()函數保存在一個變量中 factorial = null; //把factorial函數設置為null console.log(anotherFactorial(3)); //Uncaught TypeError: factorial is not a function
//使用arguments.callee解決上面的問題 //arguments.callee是一個指向當前正在執行的函數的指針, 因此可以用它來實現對函數的遞歸調用 //嚴格模式下不允許使用arguments.callee function factorial(num){ if (num <= 1) { return -1; }else{ //arguments.callee代替了函數名factorial return num * arguments.callee(num - 1); } }
//解決嚴格模式下不允許使用arguments.callee的問題 //使用命名函數表達式來達成相同的結果 var factorial = (function f(num){ //創建一個名為f()的命名函數表達式, 賦值給factorial if(num <= 1){ return 1; }else{ return num * f(num - 1); } });閉包
注意匿名函數與閉包不要混淆.
閉包指的是有權訪問 另一個函數作用域中的變量 的函數
創建閉包常用的方式, 就是在一個函數內部創建另一個函數 :
function createComparisonFunction(propertyName){ return function(object1, object2){ //value1和value2訪問了外部函數的變量propertyName, 即使該內部函數被返回或被其他地方調用, 也不影響它訪問外部函數的propertyName變量(因為該外部變量在本內部函數的作用域內) var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2) { return -1; }else if(value1 > value2){ return 1; }else{ return 0; } }; }
理解 :
//定義compare函數 function compare(value1, value2){ if(value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else{ return 0; } } //在全局作用域中調用函數, 從作用域鏈的優先級來分的話, 外部函數的活動對象始終處于第二位, 外部函數的外部函數的活動對象處于第三位 ...(以此類推), 直到作為作用域鏈終點的全局執行環境 var result = compare(5, 8); //在調用compare()函數時, 會創建一個包含arguments, value1, value2的活動對象(在作用域鏈的優先級處于第一位), 全局執行環境的變量對象(包含result和compare)在compare()執行環境的作用域鏈優先級處于第二位
作用域鏈優先級圖示 :
后臺的每一個執行環境都有一個表示變量的對象 — — 變量對象
全局環境的變量對象始終存在, 而像compare()函數這樣的局部環境的變量對象, 則只在函數執行的過程中存在.
創建compare()函數時, 會創建一個預先包含全局變量對象的作用域鏈, 該作用域鏈會被保存在內部的[[ Scope ]]屬性中
調用compare()函數時, 會為函數創建一個執行環境
然后通過復制函數的[[ Scope ]]屬性中的對象構建起執行環境的作用域鏈
?
未完待續...
模仿塊級作用域TODO
私有變量TODO
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86308.html
摘要:第一部分請點擊快速掌握面試基礎知識一關鍵字如果使用關鍵字來調用函數式很特別的形式。該對象默認包含了指向原構造函數的屬性。接下來通過例子來幫助理解屬性包含了構造函數以及構造函數中在上定義的屬性。也就是說,的回調函數后執行。 譯者按: 總結了大量JavaScript基本知識點,很有用! 原文: The Definitive JavaScript Handbook for your next...
摘要:特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 特意對前端學習資源做一個匯總,方便自己學習查閱參考,和好友們共同進步。 本以為自己收藏的站點多,可以很快搞定,沒想到一入匯總深似海。還有很多不足&遺漏的地方,歡迎補充。有錯誤的地方,還請斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應和斧正,會及時更新,平時業務工作時也會不定期更...
摘要:現在回過頭總結,才又進一步的揭開了閉包的一層后臺管理系統解決方案前端掘金基于系列的后臺管理系統解決方案。什么是繼承大多數人使用繼承不外乎是為了獲得基于的單頁應用項目模板前端掘金小貼士本項目已升級至。 關于js、jq零碎知識點 - 掘金寫在前面: 本文都是我目前學到的一些比較零碎的知識點,也是相對偏一點的知識,這是第二篇。前后可能沒有太大的相關性,需要的朋友可以過來參考下,喜歡的可以點個...
閱讀 663·2023-04-26 02:03
閱讀 1037·2021-11-23 09:51
閱讀 1111·2021-10-14 09:42
閱讀 1738·2021-09-13 10:23
閱讀 927·2021-08-27 13:12
閱讀 839·2019-08-30 11:21
閱讀 1001·2019-08-30 11:14
閱讀 1041·2019-08-30 11:09