面向物件的程式設計---建立物件/繼承/函式/閉包
一、建立物件的幾種方式
1.工廠模式
工廠模式:定義一個"工廠函式",每次呼叫這個函式就會得到一個具體的擁有特定屬性和方法的物件。抽象了建立具體物件的過程。
寫法:函式內通過new object()顯示的建立一個物件,為其新增屬性和方法;最後返回這個物件。
function creatPeople (name,age) { var o=new object () o.name=name; o.age=age; o.sayName = function(){ alert(this.name); };return o; } var person1 = createPerson("Nicholas", 29);
//person1相當於:
{
name:'Nicholas',
age:29
}
//用typeof檢測,person1是Object型別
優點:可以建立多個具有相同屬性和方法的相似的物件
不足:無法確定一個物件的型別(Object物件、Array型別的物件、Function型別的物件、Date型別的物件等)
2.建構函式模式
通過建構函式生成物件:先寫一個建構函式---通過new 這個建構函式,建立一個例項物件(它是建構函式的原型的孩子)
寫法:函式內直接將屬性和方法賦給this物件;沒有return語句;通過new一個建構函式(用來建立一個物件)來例項化一個物件。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ alert(this.name); };
//this指的是這個建構函式的prototype,即原型Person.prototype
//每個函式都有一個prototype屬性;每個物件都有一個__proto__屬性,都指向原型
//每個原型都有一個constructor屬性,指向建構函式 } var person1 = new Person("Nicholas", 29);
//例項物件的__proto__就是例項物件的建構函式的prototype 即person1.__proto__===Person.prototype
//例項物件的原型的建構函式是:person1.constructor===Person()
優點:可以確定一個物件的型別
建立的自定義的建構函式,可以將它的例項標識為一種特定的型別 (如上述示例中:例項物件person1是有型別而言的,它是一個Person型別,同時它也是Object型別的一個例項-萬物皆物件)
注意一個規則:凡是通過new來呼叫的函式,會被當做一個建構函式。否則就是一個普通的函式。
缺點:每例項化一個物件,就相當於呼叫一次建構函式,每次呼叫就會建立一次函式中完成同樣任務的的方法,造成資源的浪費 (這顯然是沒必要的,怎麼做讓例項化多個物件時只建立一次方法呢?)
每個例項都包含一個不同的Function例項(以區別name屬性,裡面的方法也是不等價的)-以下是證明
//1.首先, this.sayName=function(){}其實經歷了以下兩步: var temp=new Function() this.sayName=temp //所以,每次呼叫建構函式,都會建立一個函式例項(Function型別的物件例項) //2.其次,通過測試 var person1=new Person('mike',10) var person2=new Person('anmy',20) console.log( person1.sayName=== person2.sayName) //結果是false
這個問題可以通過把函式定義到外面解決。在建構函式外面只宣告一次函式,如下:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){ alert(this.name); } var person1 = new Person("Nicholas", 29); var person2 = new Person("Greg", 27); //此時sayName 包含的是一個指向函式的指標,person1 和 person2 物件就共享了在全域性作用域中定義的同一個 sayName()函式。
//可是新問題又來了:在全域性作用域中定義的函式實際上只能被某個物件呼叫,這讓全域性作用域有點名不副實。
//此外如果物件需要定義很多方法,那麼就要定義很多個全域性函式,於是我們這個自定義的引用型別就絲毫沒有封裝性可言了。
如何解決上述問題呢?
3.原型模式
因為每建立一個函式都有一個prototype屬性,這個屬性是一個指標,指向一個物件,也就是父親。
這個父親擁有的所有的屬性和方法都可以被它的孩子繼承。那麼給這個父親新增的所有的屬性和方法,其實也是在給孩子新增屬性和方法。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas"; var person2 = new Person(); person2.sayName(); //"Nicholas"; alert(person1.sayName == person2.sayName); //true
//person1、person2作為一個例項物件是通過Person()構造出來的。他們訪問的是同一組屬性和方法,即值都是一樣的,這些屬性是繼承自他們的父親Person.prototype的。
//如果我們給這些屬性值重新賦值,其實並不是改變原型中的這些屬性值,而是覆蓋掉這些值,原型中的屬性並不會改變。
//當為物件例項新增一個屬性時,這個屬性就會遮蔽原型物件中儲存的同名屬性;換句話說,新增這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。
person1.name=‘haha’
console.log(person1.name) //haha
var person3 = new Person();
console.log(person3.name) //'Nicholas'
//如果我們想重新訪問到原型中的的屬性,只需要把這個例項的屬性delete掉就可以。
delete person1.name
//使用 hasOwnProperty()方法可以檢測一個屬性是存在於例項中,還是存在於原型中
//只有重寫原型中的屬性,返回true,證明是例項中的屬性,否則返回false,證明是原型中的屬性
console.log(person1.hasOwnProperty('name')) //true
console.log(person3.hasOwnProperty('name')) //false
3.1 in操作符
in操作符在通過物件能夠訪問到給定的屬性時,返回true。其中既包括存在於例項中的屬性,也包括存在於原型中的屬性。
同時使用hasOwnProperty()和In操作符可以確定屬性是存在於物件中還是原型中:
function whereProperty(object,pro){ console.log(!object.hasOwnProperty(pro)&&(pro in object)) }
由於 in 操作符只要通過物件能夠訪問到屬性就返回 true, hasOwnProperty()只在屬性存在於例項中時才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以確定屬性是原型中的屬性。通過for in,遍歷物件所能訪問到的可列舉屬性(例項和原型中)
通過Object. keys()方法,接收一個物件作為引數,返回一個包含物件的所有可列舉的自有屬性的字串陣列。
var objKey={name:'jin',age:20} console.log(Object.keys(objKey)) //name age
通過 Object.getOwnPropertyNames()方法 可以列舉例項的所有屬性,無論是否可以列舉。
3.2原型模式的簡寫
前面的原型模式的例子可以簡寫成如下樣式:-用物件字面量重寫原型物件
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
這樣寫,相當於給Person的原型重新賦值了,原來的寫法只是給Person的原型新增屬性,這是兩種不用的概念。我們在
這裡使用的語法,本質上完全重寫了預設的 prototype 物件(相當於new Object()),因此 constructor 屬性也就變成了新物件的 constructor 屬性(指向 Object 建構函式),不再指向 Person 函式。
如果我們希望以後通過這個建構函式建立的例項物件,可以訪問constructor,並且依然指向Person,那我們在給原型賦值的時候,設定一下constructor。如下:
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, sayName : function () { alert(this.name); } };
上述方法直接寫明constructor屬性,會使其變成可列舉的屬性,而預設是不可列舉的,如果想要原來的不可列舉的效果,可用Object.defineProperty()。
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, sayName : function () { alert(this.name); } }; Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
3.2.1原型的動態性
保證了對原型物件所做的任何修改都會立刻反應到例項物件上,即使是先建立了例項物件後修改原型
對原型的操作(如新增屬性、方法)都是動態的,不管孩子(例項物件)是什麼時候建立的,只要父親(原型)變了,孩子就會跟著變。
var person4=new Person() Person.prototype.sayHi = function(){ alert("hi"); } person4.sayHi() //hi
若重寫整個原型物件,就無法在例項物件中得到反饋了function Person(){
} var friend = new Person(); Person.prototype = { constructor: Person, name : "Nicholas", age : 29, sayName : function () { alert(this.name); } }; friend.sayName(); //error
//呼叫例項時會為例項新增一個指向最初原型的__proto__指標,而把原型修改為另一個物件,就切斷了建構函式與原始原型的聯絡,切斷了現有原型與任何之前已經存在的物件例項之間的聯絡
因為此時的原型的constructor不在指向Person(),而是指向Object()
//例項的物件指標僅僅指向原型,而不是指向建構函式
//此時friend 指向的原型中不包含以該名字命名的屬性,這些例項引用的仍是最初的原型,所以會報錯
缺點:使用原型模式例項化物件時,所有的物件都會有統一的屬性和方法,這樣一來,就無法區分了,也就沒有定製性了。
3.2.2原型物件的問題
除上述問題,原型模式最大的問題是由其共享的本質決定的
對於普通的屬性,通過在例項上新增一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用型別值的屬性來說,問題就比較突出了
function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, friends : ["Shelby", "Court"], sayName : function () { alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
// 若我們的初衷就是像這樣在所有例項中共享一個數組,那麼這麼做是對的。可是,例項一般都是要有屬於自己的全部屬性的。而不是某個例項特有的屬性別的例項也有。
如何解決上述問題?
4.混合模式(建構函式模式+原型模式)
建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享的屬性。
function Person(name, age, job){ this.name = name; this.age = age; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29); var person2 = new Person("Greg", 27); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
建構函式定義,定義的是什麼?
是當前建構函式可生成的例項的屬性和方法
原型定義,定義的是什麼?
是原型的屬性和方法,共享於每個例項。
5.動態原型模式
把所有資訊都封裝在建構函式內,通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
注意:使用此模式,不能使用物件字面量重寫原型,這會切斷例項與現有原型的聯絡。function Person(name, age, job){ //屬性
this.name = name; this.age = age; //方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; }
//這裡只在 sayName()方法不存在的情況下,才會將它新增到原型中.
} var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName();
6.寄生建構函式模式
function Person(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ alert(this.name); }; return o; } var friend = new Person("Nicholas", 29); friend.sayName(); //"Nicholas
7.穩妥建構函式模式
穩妥物件,指的是沒有公共屬性,而且其方法也不引用 this 的物件 。
一是新建立物件的例項方法不引用 this;二是不使用 new 操作符呼叫建構函式 。
function Person(name, age, job){ //建立要返回的物件 var o = new Object(); //可以在這裡定義私有變數和函式 //新增方法 o.sayName = function(){ alert(name); }; //返回物件 return o; } var friend = Person("Nicholas", 29); friend.sayName(); //"Nicholas"
//變數 friend 中儲存的是一個穩妥物件,而除了呼叫 sayName()方法外,沒有別的方式可以訪問其資料成員
二、繼承
1.原型鏈
基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法
如果原型鏈沒有任何問題的話,繼承實際就是:所有的例項繼承其原型,或原型鏈上面的所有父原型。
但是,原型鏈有個問題。
原型中定義的屬性,會被所有例項共享,可以通過在例項物件裡覆蓋掉這個屬性。-針對基本資料型別
原型中定義的“引用型別的值”的屬性,會被所有例項共享。
那什麼是“引用型別的值”呢?
ECMAScript變數包含兩種資料型別的值:基本資料型別、引用資料型別。基本資料型別的值指的是簡單的資料段;引用資料型別值指的是那些可能由多個值構成的物件。引用型別的值是儲存在記憶體中的物件。與其他語言不同,JavaScript不允許直接訪問記憶體中的位置,也就是說不能直接操作物件的記憶體空間,在操作物件時,實際上操作的是引用而不是實際的物件,為此,引用型別的值是按引用訪問的。
換句話說,如果我們在原型裡定義一個屬性-陣列型別的,那例項物件繼承的這個屬性其實是這個屬性的引用,更改這個例項物件中的陣列,意味著更改引用,原型中的陣列也會變。
繼承:若a繼承b,實際是建立b的例項,並將其賦給a.prototype,即a.prototype=new B() 實現的本質是重寫原型物件,代之以一個新型別的例項。
注意:通過原型鏈實現繼承時,不能使用物件字面量建立原型方法
2.借用建構函式
借用建構函式,其實就是在孩子的建構函式中呼叫父親(原型)的建構函式。這樣,就把父親建構函式中定義的所有程式碼都在子類的建構函式中執行了一遍。
function Father(){ this.colors = ["red", "blue", "green"]; } function Child(){ //繼承了 SuperType Father.call(this); } var instance1 = new Child(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new Child(); alert(instance2.colors); //"red,blue,green"
//通過Child new一個物件例項,那這個物件例項就擁有了color這個屬性,而且是獨自擁有color的拷貝。
缺點:方法都定義在建構函式中,不可複用。
3.組合繼承
使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對例項屬性的繼承
function Father(name){ this.name = name; this.colors = ["red", "blue", "green"]; } function Child(name, age){ //繼承屬性 Father.call(this, name); this.age = age; } Father.prototype.sayName = function(){ alert(this.name); } //建立父建構函式
//建立子建構函式,並繼承父類中的name屬性
//給父類的原型新增一個方法sayName
Child.prototype=new Father() //給子型別新增一個原型,這個原型就是父型別的例項
Child.prototype.constructor=Child //確定通過子類生成的例項物件是Child型別
//到這裡,所有通過new Child()建立的例項物件,都擁有了sayName方法,各自擁有color,name,age屬性
4.原型式繼承/寄生式繼承
首先思考一個問題?
按照前面的那些方式,到底建立一個繼承於父類的物件例項的本質是什麼?
本質很簡單:按照父親創造出孩子,不僅要保證每個孩子都有自己的個性,還要保證每個孩子一樣的地方不需要重複創造,而且單個孩子的某個行動,不會影響到父親以至於波及到其他孩子。
逐條分析:
1.保證每個孩子有自己的個性。
孩子的建構函式就是幹這個事的,每個孩子都有自己的獨有屬性,這些獨有屬性就在建構函式裡寫,其他的都在父親(原型)中繼承。
2. 保證孩子一樣的地方不需要重複創造。
每個孩子都會說話、吃飯、睡覺,這些不必要在孩子的建構函式裡寫,只需要在父親(原型)裡寫就可以了。
3. 不會影響到父親波及其他孩子。
引用型別值的屬性。這些屬性如果是繼承的,那一個孩子更改了這個屬性,這個父親的所有孩子都會改變了。因為所有的孩子裡的這個屬性,都是引用,而不是值。
能不能通過現有的物件,直接建立一個新物件呢?
function child(FatherIns){ function F(){} F.prototype = FaherIns return new F() } //本質是建立一個把FahterIns當作原型的 建構函式 //然後通過這個建構函式建立一個孩子
其實這種繼承方式的本質是:物件的深拷貝。而並非嚴格的繼承。所以,這種繼承方式的前提是:1.有現成繼承的物件,2.不需要考慮型別 3.現有物件中如果存在引用型別值屬性,將會被所有孩子繼承。
於是,ES5為此給Object增添了一個新方法:Object.create()用來建立新物件,接收兩個引數:1.用作新物件原型的物件,2.一個為新物件定義額外屬性的物件。
能不能給生成的物件新增方法呢?
function child(fatherObj){ var tmp = Object.create(fatherObj,{ childPro:{ value:'xxxxx' } }) tmp.childMethod = function(){ ... } return tmp; }
5.寄生組合式繼承
回看一下 組合式繼承的思路:
1. 建立子型別的建構函式。
2. 在建構函式中,呼叫父類的建構函式。 //第二次呼叫
3. 定義完建構函式之後,外面還要給子型別指定原型:Child.prototype = new Father() //第一次呼叫
4. 我們都知道指定原型造成的弊端就是失去constructor。所以再指定一下constructor. Child.prototype.constructor = Child
5. 這時候繼承定義完成。
這時候我們發現,Father()這個建構函式呼叫了兩次。而且,Child的prototype我們其實是不關心它的型別的。並且,Child.prototype可不可以從一個現有的物件建立呢?完全可以啊。那這個現有的物件就是Father.prototype啊。
所以我們就可以把3、4步寫成:
var prototype = Object.create(Father.prototype) Child.prototype = prototype prototype.constructor = Child
這裡並沒有給Child一個通過Father()新建的例項,而是通過Father.prototype拷貝的例項。因為這個例項的型別並不是我們關心的。
三、函式
3.1執行環境和作用域
執行環境 定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。每個執行環境都有一個與之關聯的變數物件,環境中定義的所有變數和函式都儲存在這個物件中。
每個函式都有自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。
關鍵是:執行
function f(x,y){ var z = x+y console.log(z) }
這裡是函式定義。不涉及執行環境。
f(2,3)
這裡是函式執行。在執行這條語句時,會發生下面的事。
1. 建立一個函式執行環境和一個環境物件。 {argument:{x:,y:},z:}
2. 進入函式體,這裡面的x,y,z都是可訪問的,因為在環境物件裡有。如果這個執行環境裡沒有,就去更外面的執行環境裡找,比如window物件。
這時候就出來兩點:1.外面不能訪問執行環境裡面的物件屬性。2.執行環境不能向下搜尋,即如果在執行函式裡再定義一個函式,那裡面定義的變數,外面同樣不能訪問。
最重要的一點:函式執行完之後,執行環境銷燬,儲存變數的物件同樣被銷燬。 (執行環境會隨著函式的呼叫和返回,不斷地重建和銷燬。)
若變數物件在有變數引用(如閉包)的情況下,將留在記憶體中不被銷燬
3.2閉包
一個函式在執行完後一定會銷燬嗎?不一定。
function father(){ var tmp = 'hello' return function(){ console.log(tmp) } }
var result = father()
按照常理,這句話被執行完,father函式就結束了,意味著它裡面的tmp變數也是應該被銷燬的。如果這樣看,裡面的匿名函式是不能呼叫tmp的了。
但是。此時result這個變數,等價於什麼?等價於匿名函式的定義,即可以寫成如下形式:
var result = function(){ console.log(tmp) }
這樣寫和原來的father定義+father呼叫 看起來是等價的。
但是是等價的麼?不是,因為tmp。去哪找tmp啊?沒地方找。
所以,你看閉包這個概念被建立,是為了什麼?為了能讓函式A裡面的函式B,能訪問函式A裡面的變數。而且不是時時呼叫的,是依賴呼叫的。
也就是說,函式B依賴於函式A存在,並且B是不著急呼叫的,但是B要有。這時候你想到了什麼?
沒錯,物件!
function Person(name,age){ this.name=name this.age=age this.say = function(){ console.log(name+age) } }
這是一個建構函式的定義,建構函式的呼叫是:
var p=new Person('haha',20)
Person作為函式,沒有返回值,那p到底是什麼呢?很簡單,p是通過建構函式Person建立的例項物件,也可以說成是:Person函式返回的是——以Person作為建構函式的 原型 物件。其實就是this.
那閉包的寫法是什麼?
function Person(name,age){ function pConstructor(){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return pConstructor; }
這時候:
var pcons = Person('www',20)
這句話是把pcons指向pConstructor,而name和age都會被暫時儲存起來,只有當我們呼叫pcons時才會建立新物件例項。
var pinstance = pcons()
這時候建立一個物件還需要兩步,有沒有方法讓它變成一步呢?有,把原來的建構函式變成 匿名自呼叫函式 就可以了。
var Person = (function(){ function pConstructor(name,age){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return pConstructor; })();
此時,我們建立一個物件例項
var p = Person('ww',12)
這就是閉包。閉包具體指的是什麼?就是那個被包起來的函式。
也有的寫法是返回一個物件,其實不只是返回一個物件,可以返回任何型別。
var Person = (function(){ function pConstructor(name,age){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return { newObj:pConstructor } })(); var p = Person.newObj('eee',21)
閉包內的函式只屬於閉包。
若閉包內的變數與全域性變數衝突時,會呼叫閉包內的變數。
閉包需注意兩點:
1.閉包只能取得包含函式中任何變數的最終值