轉載請註明預見才能遇見的博客:http://my.csdn.net/
原文地址:https://blog.csdn.net/pcaxb/article/details/100668246
JavaScript原生系列-創建對象、原型、原型鏈、繼承(原型鏈繼承、構造函數繼承、組合繼承)、插件模擬
目錄
JavaScript原生系列-原型、原型鏈、原型鏈繼承、組合繼承
1.Object.create();//參數是對象,或者是null,參數是原型
2.自定義原型:(不是所有的對象都繼承Object.prototype)
2.借用構造函數 (解決在創建子類型實例時,不能向超類型的構造函數中傳遞參數)
文章中涉及到的打印
//打印封裝
function L() {
console.log.apply(this,arguments);
}
1.創建對象方式
1.最初創建對象的方式
let obj = new Object();
obj.name = "aa";
obj.getName = function() {
return this.name;
}
L(obj.getName());
2.字面量創建對象
let ps = {
name:"cc",
age:12,
getAge:function(){
return this.age;
},
}
L(ps.getAge());
Object構造函數或對象字面量創建的對象是一樣的效果,推薦使用字面量,可以設置默認屬性,簡單方便。 雖然Object構造函數或對象字面量都可以用來創建單個對象,但是這種創建對象的做法直接導致代碼無法複用,所以爲了解決這個問題就產生了新的創建對象的方法--工廠模式。
3.工廠模式創建對象
function createPs(name,type){
let obj = new Object();
obj.name = name;
obj.type = type;
obj.getType = function(){
return this.type;
}
return obj;
}
let cp = createPs("dd","5");
這種方式雖然解決了對象實例化代碼重複的問題,但這種方式創建的對象無法知道它的類型。公共屬性和方法都是屬於每個對象的,浪費內存。再看構造函數創建對象。
4.構造函數創建對象
看後面
5.原型模式創建對象
看後面
6.Object.create()創建對象
看後面
2.原型類型
隱式原型:所有引用類型(函數,數組,對象)都擁有__proto__屬性(null、undefined、Object.create(null)除外)
顯式原型:所有函數擁有prototype屬性(Function除外),原型對象在定義函數時就被創建
我們創建的每個函數都有一個prototype原型屬性,這個屬性是一個指針,指向一個對象, 而這個對象的用途是包含所有實例共享的屬性和方法,這個對象就是原型對象。原型對象會在創建一個新函數的時候根據一組特定的規則來生成。
普通對象:只擁有__proto__
函數對象:擁有__proto__和prototype,特殊情況Function.prototype
function person(){};
console.log(typeof person.prototype) //Object
console.log(typeof Object.prototype) // Object
console.log(typeof Function.prototype) // 特殊 Function
console.log(typeof Function.prototype.prototype) //undefined 函數對象卻沒有prototype屬性
function Function(){};
var temp = new Function();
Function.prototype = temp; //由new Function()產生的對象都是函數對象
3.構造函數創建對象
對象會繼承構造函數的方法,隱式原型指向構造函數的顯示原型。
function Person(name,height,age){
this.name = name;
this.age = age;
this.height = height;
this.getHeight = function(){
return this.height;
}
}
Person.prototype.getAge = function(){
return this.age;
}
let p = new Person("hh",146,20);
p.getName = function() {
console.log(this.name)
};
L(p.getName(),p.getAge());
//getAge 雖然不是在對象p上但是對象會繼承構造函數的方法
//對象的隱式原型指向構造函數的顯示原型
console.log(p.__proto__ === Person.prototype);//true
當調用某種方法或查找某種屬性時,首先會在自身調用和查找,如果自身並沒有該屬性或方法,則會去它的__proto__屬性中調用查找,也就是它構造函數的prototype中調用查找。
每個對象都有一個constructor屬性,constructor屬性指向了Person這個構造函數。使用構造函數的方式創建的對象下多了一層__proto__,__proto__下有constructor屬性。可以通過constructor屬性來標識對象的類型,可以使用instanceof判斷。
在執行new操作的時候會經歷以下4個步驟:
(1)創建一個對象
(2)將構造函數的作用域賦給新對象(因此this指針就指向了新的對象) 【代碼:p.__proto__ = Person.prototype】
說明:Person中的prototype是在創建構造函數的時候創建的。其實原型對象就是構造函數的一個實例對象。Person.prototype就是Person的一個實例對象。相當於在Person創建的時候,自動創建了一個它的實例,並且把這個實例賦值給了prototype。
function Person(){};
var temp = new Person();
Person.prototype = temp;
(3)執行構造函數中的代碼
(4)返回新對象
使用構造函數的主要問題是每個對象都會把每個函數創建一遍,比如getHeight,每個對象都會創建一遍,非常損耗內存,所以引入了原型模式。
判斷屬性是實例對象屬性還是原型對象屬性:isOwnProperty()
獲取所有可枚舉的屬性:Object.keys()
獲取所有實例屬性:Object.getOwnPropertyNames()
4.原型模式創建對象
一般不要把屬性放在prototype中,否則實例會共享一個屬性。
function Animal(){}
Animal.prototype.name = 'PIG';
Animal.prototype.type = '111';
Animal.prototype.say = function(){
return this.type;
};
let pig = new Animal();
除了原型使用上面的賦值操作,我們目前更喜歡使用對象字面量或者是new關鍵詞來操作原型。但是使用對象字面量或者new關鍵詞有一個很重要的知識點:無論是使用對象字面量還是new關鍵詞,都是創建一個新的對象來替換掉原來的原型對象。
之前說過原型模式也是有缺點,其最大的缺點便是其共享的特性,隨便修改原型對象中的任何一個屬性,都會影響到它所實例化的所有對象,這樣造成了不能出現“求同存異”的現象。因此我們會更多地使用下面的一種方法。
5.組合使用構造函數模式和原型模式
組合使用當然是將所有共享的屬性放在原型對象中,所有獨特的屬性放在構造函數中
function Person(name,age){//獨特的屬性
this.name = name;
this.age = age;
}
Person.prototype = {//公共的屬性
constructor:Person,
color:"white"
}
let person = new Person("cc",20);
L(person);
L(Person.prototype.constructor,Person,Person.prototype.constructor === Person);
這裏如果不把constructor:Person那麼Person.prototype.constructor === Object 不是Person
動態原型模式
function Person(name,age){
this.name = name;
this.age = age;
//動態原型模式
if(typeof this.getColor != 'function'){
Person.prototype.color = "white";
Person.prototype.getColor = function(){
return this.color;
}
//這裏不能使用對象字面量的形式
// Person.prototype = {
// constructor:Person,
// color:"white",
// getColor:function(){
// return this.color;
// }
// }
}
}
let person = new Person("cc",20);
動態原型模式不能使用字面量的新式,因爲在new的時候,是先給__proto__ 賦值,然後再執行構造函數的,所以,通過字面量的方式__proto__不會被賦值到新的prototype。
6.原型和原型鏈
我們創建的每個函數都有一個prototype原型屬性,prototype是function對象的一個屬性 ,這個屬性是一個指針,指向一個對象, 而這個對象的用途是包含所有實例共享的屬性和方法,這個對象就是原型對象。原型對象會在創建一個新函數的時候根據一組特定的規則來生成。
創建構造函數 -- 創建prototype屬性 -- 指向原型對象 -- 原型對象有一個constructor屬性 -- 指向構造函數
Person.prototype.constructor = Person;
根據ECMA-262第五版中的介紹,實例內部的指針明確稱爲[[Prototype]],雖然沒有標準的方式訪問該指針,但常用瀏覽器每個對象上都支持一個屬性__proto__,__proto__這個
是瀏覽器自己實現的一個訪問的接口,不是標準設定的,但是等價於標準定義的[[Prototype]]。
但prototype
屬性卻是構造函數特有的,它永遠指向構造函數的原型對象。
實例雖然無法訪問到[[Prototype]],但是可以使用isPrototypeof()方法來確定對象之間是否存在關係,也可以使用getPrototypeOf()來獲取[[Prototype]]的值。for-in循環將會遍歷所有能夠通過對象訪問、可枚舉的屬性,無論該屬性位於實例中還是原型中。
* 構造函數--prototype
* 實例對象--[[prototype]]--瀏覽器自己實現的__proto__,[[prototype]] === __proto__
L(Object.getPrototypeOf(person) === Person.prototype);//true
L(Object.getPrototypeOf(person) === person.__proto__);//true
__proto__是瀏覽器自己實現的一個接口,constructor需要自己實現
console.log(Person.prototype);
Person.prototype = {
constructor:Person,
//這裏是其他的屬性
}
console.log(Person.prototype);
這裏的兩個打印是一樣的
console.log(Function)//ƒ Function() { [native code] }
console.log(Object)//ƒ Object() { [native code] }
console.log(new Function())//ƒ anonymous() {}
console.log(new Object())//對象{}
隱式原型指向構造函數的顯示原型(圖一的綠色和藍色可以看出prototype和__proto__相等的),所以
console.log(Person.prototype === p.__proto__);//true
對象p擁有一個屬性值__proto__,並且__proto__是一個對象,包含兩個屬性值constructor和__proto__
console.log(p.__proto__);//對象{}
console.log(p.__proto__.constructor);//function Person(){}
console.log(p.__proto__.__proto__);//對象{},擁有很多屬性值
console.log(p.__proto__.__proto__.constructor);//function Object(){}
console.log(Person.prototype.__proto__.constructor === Object);//true
console.log(Person.prototype.constructor === Person);//true
console.log(p.__proto__.__proto__ === Object.prototype);//true
console.log(Object.prototype.__proto__);//null
console.log(p.__proto__.__proto__.__proto__);//null
let Person = function(name,age) {
this.name = name;
this.age = age;
}
//prototype只有函數有
Person.prototype.getAge = function(){
console.log(this.age);
}
let p = new Person("cc",20);
p.getName = function() {
console.log(this.name)
};
p.getAge();//20
p.getName();//"cc"
console.log(p);
查找屬性,如果本身沒有,則會去__proto__中查找,也就是構造函數的顯式原型中查找,如果構造函數中也沒有該屬性,因爲構造函數也是對象,也有__proto__,那麼會去它的顯式原型中查找,一直找到null(Object層沒有__proto__),如果沒有則返回undefined。
每個對象無論是用什麼方法創建的,都有一個__proto__
屬性,這個__proto__
屬性便是連接原型鏈的關鍵地方。通過__proto__形成原型鏈而非protrotype。
let c = {}
console.log(c.__proto__.constructor);
console.log(c.__proto__);
console.log(c.__proto__.constructor.prototype);
console.log(c.__proto__ === c.__proto__.constructor.prototype);//true
7.原型鏈(原型鏈繼承)
原型鏈:原型鏈的基本思想是利用原型讓一個類型繼承另一個類型的屬性和方法,把子類的原型對象設置爲父類的實例,就形成了原型鏈。__proto__就是一個容器,裝prototype的,沿着__proto__往上找,形成原型鏈。
function Person(name){
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
function Gril(beautiful){
this.beautiful = beautiful;
}
//把子類的原型對象設置爲父類的實例
Gril.prototype = new Person("cc");//__proto__
Gril.prototype.constructor = Gril;
Gril.prototype.getBeautiful = function(){
return this.beautiful;
};
var gril = new Gril("yes");
L(gril);
原型鏈也存在兩個問題:
(1)原型中包含引用類型值的問題(同5中的原型引用問題)
(2)在創建子類型實例時,不能向超類型的構造函數中傳遞參數
8.Object.create()創建對象
1.Object.create();//參數是對象,或者是null,參數是原型
function Person(){
}
var p1 = Object.create(Person.prototype);
var p2 = new Person();
console.log(p1,p2)//p1和p2效果一樣
2.自定義原型:(不是所有的對象都繼承Object.prototype)
var obj = Object.create(null);//一個空對象
//undefined null 沒有原型,沒有包裝類
3.__proto__可以更改,但是不能自己創建
var obj1 = Object.create(null);
obj1.__proto__ = {//新建
num:1
}
console.log(obj1.num);//undefined
var obj2 = new Object();
obj2.__proto__ = {//修改
num:1
}
console.log(obj2.num);//1
9.繼承
1.原型鏈繼承
看上面
2.借用構造函數 (解決在創建子類型實例時,不能向超類型的構造函數中傳遞參數)
借用構造函數(constructor stealing)(有時候也叫作僞造對象或經典繼承),在子類型構造函數的內部調用超類型構造函數,函數只不過是在特定環境中執行代碼的對象, 因此通過使用 apply()和 call()方法也可以在(將來)新創建的對象上執行構造函數。
function Person(name,age){
this.name = name;
this.age = age;
this.classify = "DEFAULT";
//動態原型模式
if(typeof this.getColor != 'function'){
Person.prototype.color = "white";
Person.prototype.getColor = function(){
return this.color;
}
}
}
function Gril(name,age,clothes){
Person.call(this,name,age);
this.clothes = clothes;
}
let gril = new Gril("cc",20,"beautiful");
gril.classify = "A";
console.log(gril);
let gril1 = new Gril("cc",20,"beautiful");
gril1.classify = "B";
console.log(gril1);
因爲使用call函數的方法,讓實例化Person超類的時候this指針指向了實例化的子類,相當於classify成了子類Gril的屬性,因此每個實例操作的classify都是自己的私有屬性。
因爲使用call方法,我們還可以傳遞參數給超類,這樣方法無法複用,每個實例都有自己一個實例的方法,所以一般需要要傳遞參數。
3.組合繼承
組合繼承(combination inheritance)有時候也叫作僞經典繼承,結合了借用構造函數和原型鏈。使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數複用,又能夠保證每個實例都有它自己的屬性。
function Person(name,age){
this.name = name;
this.age = age;
this.classify = "DEFAULT";
//動態原型模式
if(typeof this.getColor != 'function'){
Person.prototype.color = "white";
Person.prototype.getColor = function(){
return this.color;
}
}
}
function Gril(name,age,clothes){
Person.call(this,name,age);
this.clothes = clothes;
}
Gril.prototype = new Person;
Gril.prototype.constructor = Gril;
Gril.prototype.getAge = function(){
return this.age;
}
let gril = new Gril("cc",20,"beautiful");
L(gril.getColor());//white
L(gril.getAge());//20
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲 JavaScript 中最常用的繼 承模式。而且, instanceof 和 isPrototypeOf()也能夠用於識別基於組合繼承創建的對象。
10.擴展
1.原始值是沒有屬性的
var num = 88;
console.log(num.__proto__)
console.log(num.toString());
2.原型方法的重寫 toString 例子
var num = 555;
L(num.__proto__);
L(num.toString());//555 Number的toString
delete num.__proto__.toString;
L(num.__proto__);
L(num.toString());//[object Number] Object的toString
delete num.__proto__.__proto__.toString;
L(num.__proto__.__proto__);
L(num.toString());//報錯 Object之上再無toString
3.表達式可以立即執行
;(function(){console.log(111)})()
;+function(){console.log(222)}()
;-function(){console.log(333)}()
;!function(){console.log(444)}()
;var fun = function(){console.log(555)}()
;(function(msg){console.log(msg)})(666)
4.插件模擬
;(function(){
function LogUtil(){
}
LogUtil.prototype = {
}
window.LogUtil = new LogUtil();
})()
JavaScript原生系列-創建對象、原型、原型鏈、繼承(原型鏈繼承、構造函數繼承、組合繼承)、插件模擬