摘要:像和這樣的原生構造函數,在運行時會自動出現在執行環境中。理解原型對象在默認情況下,所有原型對象都會自動獲得一個構造函數屬性,這個屬性包含一個指向屬性所在函數的指針。而通過這個構造函數,我們還可繼續為原型對象添加其他屬性和方法。
原型鏈是一種機制,指的是 JavaScript 每個對象都有一個內置的 __proto__ 屬性指向創建它的構造函數的 prototype(原型)屬性。原型鏈的作用是為了實現對象的繼承,要理解原型鏈,需要先從函數對象、constructor、new、prototype、__proto__ 這五個概念入手。
函數對象前面講過,在 JavaScript 里,函數即對象,程序可以隨意操控它們。比如,可以把函數賦值給變量,或者作為參數傳遞給其他函數,也可以給它們設置屬性,甚至調用它們的方法。下面示例代碼對「普通對象」和「函數對象」進行了區分。
普通對象:
var o1 = {}; var o2 = new Object();
函數對象:
function f1(){}; var f2 = function(){}; var f3 = new Function("str","console.log(str)");
簡單的說,凡是使用 function 關鍵字或 Function 構造函數創建的對象都是函數對象。而且,只有函數對象才擁有 prototype (原型)屬性。
constructor 構造函數函數還有一種用法,就是把它作為構造函數使用。像 Object 和 Array 這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而自定義對象類型的屬性和方法。如下代碼所示:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log(this.name); }; } var person1 = new Person("Stone", 28, "Software Engineer"); var person2 = new Person("Sophie", 29, "English Teacher");
在這個例子中,我們創建了一個自定義構造函數 Person(),并通過該構造函數創建了兩個普通對象 person1 和 person2,這兩個普通對象均包含3個屬性和1個方法。
你應該注意到函數名 Person 使用的是大寫字母 P。按照慣例,構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個做法借鑒自其他面向對象語言,主要是為了區別于 JavaScript 中的其他函數;因為構造函數本身也是函數,只不過可以用來創建對象而已。
new 操作符要創建 Person 的新實例,必須使用 new 操作符。以這種方式調用構造函數實際上會經歷以下4個步驟:
創建一個新對象;
將構造函數的作用域賦給新對象(因此 this 就指向了這個新對象);
執行構造函數中的代碼(為這個新對象添加屬性);
返回新對象。
將構造函數當作函數構造函數與其他函數的唯一區別,就在于調用它們的方式不同。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要通過 new 操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過 new 操作符來調用,那它跟普通函數也不會有什么兩樣。例如,前面例子中定義的 Person() 函數可以通過下列任何一種方式來調用。
// 當作構造函數使用 var person = new Person("Stone", 28, "Software Engineer"); person.sayName(); // "Stone" // 作為普通函數調用 Person("Sophie", 29, "English Teacher"); // 添加到 window window.sayName(); // "Sophie" // 在另一個對象的作用域中調用 var o = new Object(); Person.call(o, "Tommy", 3, "Baby"); o.sayName(); // "Tommy"
這個例子中的前兩行代碼展示了構造函數的典型用法,即使用 new 操作符來創建一個新對象。接下來的兩行代碼展示了不使用 new 操作符調用 Person() 會出現什么結果,屬性和方法都被添加給 window 對象了。當在全局作用域中調用一個函數時,this 對象總是指向 Global 對象(在瀏覽器中就是 window 對象)。因此,在調用完函數之后,可以通過 window 對象來調用 sayName() 方法,并且還返回了 "Sophie" 。最后,也可以使用 call()(或者 apply())在某個特殊對象的作用域中調用 Person() 函數。這里是在對象 o 的作用域中調用的,因此調用后 o 就擁有了所有屬性和 sayName() 方法。
構造函數的問題構造函數模式雖然好用,但也并非沒有缺點。使用構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName() 的方法,但那兩個方法不是同一個 Function 的實例。因為 JavaScript 中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也可以這樣定義。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("console.log(this.name)"); // 與聲明函數在邏輯上是等價的 }
從這個角度上來看構造函數,更容易明白每個 Person 實例都包含一個不同的 Function 實例(sayName() 方法)。說得明白些,以這種方式創建函數,雖然創建 Function 新實例的機制仍然是相同的,但是不同實例上的同名函數是不相等的,以下代碼可以證明這一點。
console.log(person1.sayName == person2.sayName); // false
然而,創建兩個完成同樣任務的 Function 實例的確沒有必要;況且有 this 對象在,根本不用在執行代碼前就把函數綁定到特定對象上面。因此,大可像下面這樣,通過把函數定義轉移到構造函數外部來解決這個問題。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ console.log(this.name); } var person1 = new Person("Stone", 28, "Software Engineer"); var person2 = new Person("Sophie", 29, "English Teacher");
在這個例子中,我們把 sayName() 函數的定義轉移到了構造函數外部。而在構造函數內部,我們將 sayName 屬性設置成等于全局的 sayName 函數。這樣一來,由于 sayName 包含的是一個指向函數的指針,因此 person1 和 person2 對象就共享了在全局作用域中定義的同一個 sayName() 函數。這樣做確實解決了兩個函數做同一件事的問題,可是新問題又來了,在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是,如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型來解決。
prototype 原型我們創建的每個函數都有一個 prototype(原型)屬性。使用原型的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象實例的信息,而是可以將這些信息直接添加到原型中,如下面的例子所示。
function Person(){} Person.prototype.name = "Stone"; Person.prototype.age = 28; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); person1.sayName(); // "Stone" var person2 = new Person(); person2.sayName(); // "Stone" console.log(person1.sayName == person2.sayName); // true
在此,我們將 sayName() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中,構造函數變成了空函數。即使如此,也仍然可以通過調用構造函數來創建新對象,而且新對象還會具有相同的屬性和方法。但與前面的例子不同的是,新對象的這些屬性和方法是由所有實例共享的。換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 函數。
理解原型對象在默認情況下,所有原型對象都會自動獲得一個 constructor(構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。就拿前面的例子來說,Person.prototype.constructor 指向 Person。而通過這個構造函數,我們還可繼續為原型對象添加其他屬性和方法。
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。如果我們在實例中添加了一個屬性,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創建該屬性,該屬性將會屏蔽原型中的那個屬性。來看下面的例子。
function Person(){} Person.prototype.name = "Stone"; Person.prototype.age = 28; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = "Sophie"; console.log(person1.name); // "Sophie",來自實例 console.log(person2.name); // "Stone",來自原型
在這個例子中,person1 的 name 被一個新值給屏蔽了。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常地返回值,即分別是 "Sophie"(來自對象實例)和 "Stone"(來自原型)。當訪問 person1.name 時,需要讀取它的值,因此就會在這個實例上搜索一個名為 name 的屬性。這個屬性確實存在,于是就返回它的值而不必再搜索原型了。當訪問 person2. name 時,并沒有在實例上發現該屬性,因此就會繼續搜索原型,結果在那里找到了 name 屬性。
當為對象實例添加一個屬性時,這個屬性就會屏蔽原型中保存的同名屬性;換句話說,添加這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設置為 null ,也只會在實例中設置這個屬性,而不會恢復其指向原型的連接。不過,使用 delete 操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中的屬性,如下所示。
function Person(){} Person.prototype.name = "Stone"; Person.prototype.age = 28; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ console.log(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = "Sophie"; console.log(person1.name); // "Sophie",來自實例 console.log(person2.name); // "Stone",來自原型 delete person1.name; console.log(person1.name); // "Stone",來自原型
在這個修改后的例子中,我們使用 delete 操作符刪除了 person1.name,之前它保存的 "Sophie" 值屏蔽了同名的原型屬性。把它刪除以后,就恢復了對原型中 name 屬性的連接。因此,接下來再調用 person1.name 時,返回的就是原型中 name 屬性的值了。
更簡單的原型語法前面例子中每添加一個屬性和方法就要敲一遍 Person.prototype。為減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下面的例子所示。
function Person(){} Person.prototype = { name : "Stone", age : 28, job: "Software Engineer", sayName : function () { console.log(this.name); } };
在上面的代碼中,我們將 Person.prototype 設置為等于一個以對象字面量形式創建的新對象。最終結果相同,但有一個例外:constructor 屬性不再指向 Person 了。前面曾經介紹過,每創建一個函數,就會同時創建它的 prototype 對象,這個對象也會自動獲得 constructor 屬性。而我們在這里使用的語法,本質上完全重寫了默認的 prototype 對象,因此 constructor 屬性也就變成了新對象的 constructor 屬性(指向 Object 構造函數),不再指向 Person 函數。此時,盡管 instanceof 操作符還能返回正確的結果,但通過 constructor 已經無法確定對象的類型了,如下所示。
var friend = new Person(); console.log(friend instanceof Object); // true console.log(friend instanceof Person); // true console.log(friend.constructor === Person); // false console.log(friend.constructor === Object); // true
在此,用 instanceof 操作符測試 Object 和 Person 仍然返回 true,但 constructor 屬性則等于 Object 而不等于 Person 了。如果 constructor 的值真的很重要,可以像下面這樣特意將它設置回適當的值。
function Person(){} Person.prototype = { constructor : Person, name : "Stone", age : 28, job: "Software Engineer", sayName : function () { console.log(this.name); } };
以上代碼特意包含了一個 constructor 屬性,并將它的值設置為 Person ,從而確保了通過該屬性能夠訪問到適當的值。
注意,以這種方式重設 constructor 屬性會導致它的 [[Enumerable]] 特性被設置為 true。默認情況下,原生的 constructor 屬性是不可枚舉的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,可以試一試 Object.defineProperty()。
function Person(){} Person.prototype = { name : "Stone", age : 28, job : "Software Engineer", sayName : function () { console.log(this.name); } }; // 重設構造函數,只適用于 ECMAScript 5 兼容的瀏覽器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });原型的動態性
由于在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來,即使是先創建了實例后修改原型也照樣如此。請看下面的例子。
var friend = new Person(); Person.prototype.sayHi = function(){ console.log("hi"); }; friend.sayHi(); // "hi"(沒有問題!)
以上代碼先創建了 Person 的一個實例,并將其保存在 friend 中。然后,下一條語句在 Person.prototype 中添加了一個方法 sayHi()。即使 person 實例是在添加新方法之前創建的,但它仍然可以訪問這個新方法。其原因可以歸結為實例與原型之間的松散連接關系。當我們調用 friend.sayHi() 時,首先會在實例中搜索名為 sayHi 的屬性,在沒找到的情況下,會繼續搜索原型。因為實例與原型之間的連接只不過是一個指針,而非一個副本,因此就可以在原型中找到新的 sayHi 屬性并返回保存在那里的函數。
盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來,但如果是重寫整個原型對象,那么情況就不一樣了。我們知道,調用構造函數時會為實例添加一個指向最初原型的 [[Prototype]] 指針,而把原型修改為另外一個對象就等于切斷了構造函數與最初原型之間的聯系。請記住:實例中的指針僅指向原型,而不指向構造函數。看下面的例子。
function Person(){} var friend = new Person(); Person.prototype = { constructor: Person, name : "Stone", age : 28, job : "Software Engineer", sayName : function () { console.log(this.name); } }; friend.sayName(); // Uncaught TypeError: friend.sayName is not a function
在這個例子中,我們先創建了 Person 的一個實例,然后又重寫了其原型對象。然后在調用 friend.sayName() 時發生了錯誤,因為 friend 指向的是重寫前的原型對象,其中并不包含以該名字命名的屬性。
原生對象的原型原型的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創建的。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法,如下所示。
console.log(typeof Array.prototype.sort); // "function" console.log(typeof String.prototype.substring); // "function"
通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。下面的代碼就給基本包裝類型 String 添加了一個名為 startsWith() 的方法。
String.prototype.startsWith = function (text) { return this.indexOf(text) === 0; }; var msg = "Hello world!"; console.log(msg.startsWith("Hello")); // true
這里新定義的 startsWith() 方法會在傳入的文本位于一個字符串開始時返回 true。既然方法被添加給了 String.prototype ,那么當前環境中的所有字符串就都可以調用它。由于 msg 是字符串,而且后臺會調用 String 基本包裝函數創建這個字符串,因此通過 msg 就可以調用 startsWith() 方法。
盡管可以這樣做,但我們不推薦在產品化的程序中修改原生對象的原型。如果因某個實現中缺少某個方法,就在原生對象的原型中添加這個方法,那么當在另一個支持該方法的實現中運行代碼時,就可能會導致命名沖突。而且,這樣做也可能會意外地重寫原生方法。
原型對象的問題原型模式也不是沒有缺點。首先,它省略了為構造函數傳遞初始化參數這一環節,結果所有實例在默認情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。原型模式的最大問題是由其共享的本性所導致的。
原型中所有屬性是被很多實例共享的,這種共享對于函數非常合適。對于那些包含基本值的屬性倒也說得過去,畢竟(如前面的例子所示),通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性。然而,對于包含引用類型值的屬性來說,問題就比較突出了。來看下面的例子。
function Person(){} Person.prototype = { constructor: Person, name : "Stone", age : 28, job : "Software Engineer", friends : ["ZhangSan", "LiSi"], sayName : function () { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("WangWu"); console.log(person1.friends); // "ZhangSan,LiSi,WangWu" console.log(person2.friends); // "ZhangSan,LiSi,WangWu" console.log(person1.friends === person2.friends); // true
在此,Person.prototype 對象有一個名為 friends 的屬性,該屬性包含一個字符串數組。然后,創建了 Person 的兩個實例。接著,修改了 person1.friends 引用的數組,向數組中添加了一個字符串。由于 friends 數組存在于 Person.prototype 而非 person1 中,所以剛剛提到的修改也會通過 person2.friends(與 person1.friends 指向同一個數組)反映出來。假如我們的初衷就是像這樣在所有實例中共享一個數組,那么對這個結果我沒有話可說。可是,實例一般都是要有屬于自己的全部屬性的。
構造函數和原型結合所以,構造函數用于定義實例屬性,而原型用于定義方法和共享的屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享著對方法的引用,最大限度地節省了內存。下面的代碼重寫了前面的例子。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["ZhangSan", "LiSi"]; } Person.prototype = { constructor : Person, sayName : function(){ console.log(this.name); } } var person1 = new Person("Stone", 28, "Software Engineer"); var person2 = new Person("Sophie", 29, "English Teacher"); person1.friends.push("WangWu"); console.log(person1.friends); // "ZhangSan,LiSi,WangWu" console.log(person2.friends); // "ZhangSan,LiSi" console.log(person1.friends === person2.friends); // false console.log(person1.sayName === person2.sayName); // true
在這個例子中,實例屬性都是在構造函數中定義的,而由所有實例共享的屬性 constructor 和方法 sayName() 則是在原型中定義的。而修改了 person1.friends(向其中添加一個新字符串),并不會影響到 person2.friends,因為它們分別引用了不同的數組。
這種構造函數與原型混成的模式,是目前在 JavaScript 中使用最廣泛、認同度最高的一種創建自定義類型的方法。可以說,這是用來定義引用類型的一種默認模式。
__proto__為什么在構造函數的 prototype 中定義了屬性和方法,它的實例中就能訪問呢?
那是因為當調用構造函數創建一個新實例后,該實例的內部將包含一個指針 __proto__,指向構造函數的原型。Firefox、Safari 和 Chrome 的每個對象上都有這個屬性 ,而在其他瀏覽器中是完全不可見的(為了確保瀏覽器兼容性問題,不要直接使用 __proto__ 屬性,此處只為解釋原型鏈而演示)。讓我們來看下面代碼和圖片:
圖中展示了 Person 構造函數、Person 的原型屬性以及 Person 現有的兩個實例之間的關系。在此,Person.prototype.constructor 指回了 Person。Person.prototype 中除了包含 constructor 屬性之外,還包括后來添加的其他屬性。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用 person1.sayName()。這是因為內部指針 __proto__ 指向 Person.prototype,而在 Person.prototype 中能找到 sayName() 方法。
我們來證實一下,__proto__ 是不是真的指向 Person.prototype 的?如下代碼所示:
function Person(){} var person = new Person(); console.log(person.__proto__ === Person.prototype); // true
既然,__proto__ 確實是指向 Person.prototype,那么使用 new 操作符創建對象的過程可以演變為,為實例對象的 __proto__ 賦值的過程。如下代碼所示:
function Person(){} // var person = new Person(); // 上一行代碼等同于以下過程 ==> var person = {}; person.__proto__ = Person.prototype; Person.call(person);
這個例子中,我先創建了一個空對象 person,然后把 person.__proto__ 指向了 Person 的原型對象,便繼承了 Person 原型對象中的所有屬性和方法,最后又以 person 為作用域執行了 Person 函數,person 便就擁有了 Person 的所有屬性和方法。這個過程和 var person = new Person(); 完全一樣。
簡單來說,當我們訪問一個對象的屬性時,如果這個屬性不存在,那么就會去 __proto__ 里找,這個 __proto__ 又會有自己的 __proto__,于是就這樣一直找下去,直到找到為止。在找不到的情況下,搜索過程總是要一環一環地前行到原型鏈末端才會停下來。
原型鏈JavaScript 中描述了原型鏈的概念,并將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。如下圖所示:(圖源:segmentfault.com,作者:manxisuo)
那么,假如我們讓原型對象等于另一個類型的實例,結果會怎么樣呢?顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含著一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。
上面這段話比較繞口,代碼更容易理解,讓我們來看看實現原型鏈的基本模式。如下代碼所示:
function Father(){ this.value = true; } Father.prototype.getValue = function(){ return this.value; }; function Son(){ this.value2 = false; } // 繼承了 Father Son.prototype = new Father(); Son.prototype.getValue2 = function (){ return this.value2; }; var son = new Son(); console.log(son.getValue()); // true
以上代碼定義了兩個類型:Father 和 Son。每個類型分別有一個屬性和一個方法。它們的主要區別是 Son 繼承了 Father,而繼承是通過創建 Father 的實例,并將該實例賦給 Son.prototype 實現的。實現的本質是重寫原型對象,代之以一個新類型的實例。換句話說,原來存在于 Father 的實例中的所有屬性和方法,現在也存在于 Son.prototype 中了。在確立了繼承關系之后,我們給 Son.prototype 添加了一個方法,這樣就在繼承了 Father 的屬性和方法的基礎上又添加了一個新方法。
我們再用 __proto__ 重寫上面代碼,更便于大家的理解:
function Father(){ this.value = true; } Father.prototype.getValue = function(){ return this.value; }; function Son(){ this.value2 = false; } // 繼承了 Father // Son.prototype = new Father(); ==> Son.prototype = {}; Son.prototype.__proto__ = Father.prototype; Father.call(Son.prototype); Son.prototype.getValue2 = function (){ return this.value2; }; // var son = new Son(); ==> var son = {}; son.__proto__ = Son.prototype; Son.call(son); console.log(son.getValue()); // true console.log(son.getValue === son.__proto__.__proto__.getValue); // true
從以上代碼可以看出,實例 son 調用 getValue() 方法,實際是經過了 son.__proto__.__proto__.getValue 的過程的,其中 son.__proto__ 等于 Son.prototype,而 Son.prototype.__proto__ 又等于 Father.prototype,所以 son.__proto__.__proto__.getValue 其實就是 Father.prototype.getValue。
事實上,前面例子中展示的原型鏈還少一環。我們知道,所有引用類型默然都繼承了 Obeject,而這個繼承也是通過原型鏈實現的。大家要記住,所有函數的默認原型都是 Object 的實例,因此默認原型都會包含一個內部指針 __proto__,指向 Object.prototype。這也正是所有自定義類型都會繼承 toString()、valueOf() 等默認方法的根本原因。
下圖展示了原型鏈實現繼承的全部過程。(圖源:segmentfault.com,作者:manxisuo)
上圖中,p 指 prototype 屬性,[p] 即 __proto__ 指對象的原型,[p] 形成的鏈(虛線部分)就是原型鏈。從圖中可以得出以下信息:
Object.prototype 是頂級對象,所有對象都繼承自它。
Object.prototype.__proto__ === null ,說明原型鏈到 Object.prototype 終止。
Function.__proto__ 指向 Function.prototype。
關卡根據描述寫出對應的代碼。
// 挑戰一 // 1.定義一個構造函數 Animal,它有一個 name 屬性,以及一個 eat() 原型方法。 // 2.eat() 的方法體為:console.log(this.name + " is eating something.")。 // 3.new 一個 Animal 的實例 tiger,然后調用 eat() 方法。 // 4.用 __proto__ 模擬 new Animal() 的過程,然后調用 eat() 方法。 var Animal = function(name){ // 待補充的代碼 }; var tiger = new Animal("tiger"); // 待補充的代碼 var tiger2 = {}; // 待補充的代碼
// 挑戰二 // 1.定義一個構造函數 Bird,它繼承自 Animal,它有一個 name 屬性,以及一個 fly() 原型方法。 // 2.fly() 的方法體為:console.log(this.name + " want to fly higher.");。 // 3.new 一個 Bird 的實例 pigeon,然后調用 eat() 和 fly() 方法。 // 4.用 __proto__ 模擬 new Bird() 的過程,然后用代碼解釋 pigeon2 為何能調用 eat() 方法。 var Bird = function(name){ // 待補充的代碼 } var pigeon = new Bird("pigeon"); // 待補充的代碼 var pigeon2 = {}; // 待補充的代碼
// 挑戰三 // 1.定義一個構造函數 Swallow,它繼承自 Bird,它有一個 name 屬性,以及一個 nesting() 原型方法。 // 2.nesting() 的方法體為:console.log(this.name + " is nesting now.");。 // 3.new 一個 Swallow 的實例 yanzi,然后調用 eat()、fly() 和 nesting() 方法。 // 4.用 __proto__ 模擬 new Swallow() 的過程,然后用代碼解釋 yanzi2 為何能調用 eat() 方法。 var Swallow = function(name){ // 待補充的代碼 } var yanzi = new Swallow("yanzi"); // 待補充的代碼 var yanzi2 = {}; // 待補充的代碼更多
關注微信公眾號「劼哥舍」回復「答案」,獲取關卡詳解。
關注 https://github.com/stone0090/javascript-lessons,獲取最新動態。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86566.html
摘要:對象數組初始化表達式,闖關記之上文檔對象模型是針對和文檔的一個。闖關記之數組數組是值的有序集合。數組是動態的,根闖關記之語法的語法大量借鑒了及其他類語言如和的語法。 《JavaScript 闖關記》之 DOM(下) Element 類型 除了 Document 類型之外,Element 類型就要算是 Web 編程中最常用的類型了。Element 類型用于表現 XML 或 HTML 元素...
摘要:本課程之所以叫做闖關記,是因為部分章節精心設計了挑戰關卡,通過提供更多的實戰機會,讓大家可以循序漸進地有目的地有挑戰地開展學習。課程結構及目錄以下目錄只是初步構想,課程結構及內容會根據實際情況隨時進行調整。 為何寫作此課程 stone 主要負責基于 Web 的企業內部管理系統的開發,雖然能夠熟練地使用 JavaScript,但隨著對 JavaScript 的理解越來越深,才發現自己尚...
摘要:屬性名可以是包含空字符串在內的任意字符串,但對象中不能存在兩個同名的屬性。客戶端中表示網頁結構的對象均是宿主對象。這里的函數稱做構造函數,構造函數用以初始化一個新創建的對象。通過關鍵字和構造函數調用創建的對象的原型就是構造函數的屬性的值。 對象是 JavaScript 的數據類型。它將很多值(原始值或者其他對象)聚合在一起,可通過名字訪問這些值,因此我們可以把它看成是從字符串到值的映射...
摘要:原文鏈接關于的原型和原型鏈,看我就夠了一參考鏈接闖關記之原型及原型鏈之原型與原型鏈一篇文章帶你理解原型和原型鏈徹底理解原型鏈一的默認指向圖解和的三角關系原型和原型鏈三張圖搞懂的原型對象與原型鏈 溫故 創建對象的三種方式 通過對象直接量 通過new創建對象 通過Object.create() js中對象分為兩種 函數對象 普通對象 仔細觀察如下代碼 function Foo(na...
閱讀 3054·2023-04-26 00:40
閱讀 2391·2021-09-27 13:47
閱讀 4197·2021-09-07 10:22
閱讀 2966·2021-09-06 15:02
閱讀 3307·2021-09-04 16:45
閱讀 2484·2021-08-11 10:23
閱讀 3599·2021-07-26 23:38
閱讀 2900·2019-08-30 15:54