面向對象把構成問題的 transaction 分解成各個對象,而建立對象的目的也不是為了完成一個個步驟,而是為了描述某個事物在解決整個問題的過程中所發生的行為,意在寫出通用代碼,加強代碼重用,屏蔽差異性。
一、什麼是面向對象編程#
js 是基於原型
的,基於面向對象編程
面向對象就是把數據和對數據的操作方法放在一起,作為一個整體 —— 對象。對同類對象抽象出其共性,形成類
1. 面向過程程序設計#
將一個項目(或者一個事件)從頭到尾按順序,一步一步完成,先做什麼,後做什麼,一直到結束,也是我們人做事的方法。
自上而下,先確定一個整體的框架,然後添磚加瓦,逐步實現想要得到的效果,適用於簡單的系統,容易理解。但是難以應對複雜的系統,不易維護擴展,難以復用
面向過程是分析解決問題的步驟,然後用函數把這些步驟一步一步的實現,然後在使用的時候一一調用則可。強調的是完成這件事兒的動作,更接近我們日常處理事情的思維。
2. 面向對象程序設計#
將一個項目(或者一個事件)分成更小的項目,每一個部分負責一方面的功能,最後由這些部分組成一個整體,先設計組件,在完成拼裝,適用於大型複雜的系統
面向對象把構成問題的 transaction 分解成各個對象,而建立對象的目的也不是為了完成一個個步驟,而是為了描述某個事物在解決整個問題的過程中所發生的行為,意在寫出通用代碼,加強代碼重用,屏蔽差異性。
想要弄明白面向對象,需要先理解類和對象的概念
二、創建對象的方法#
1. 創建字面量和實例#
window.onload = function() {
// 實例
var person = new Object();
person.name = '小明';
person.age = 22;
person.year = function() {
console.log(this.name + '今年' + this.age + '歲了!')
};
person.year();
// 字面量
var student = {
name: '小明',
age: 22,
year: function () {
console.log(this.name + '今年' + this.age + '歲了!')
}
}
student.year();
}
// 小明今年22歲了!
兩者輸出的結果是一樣的,控制台輸出:
缺點:重複實例化對象,代碼冗餘高
2. 工廠模式#
window.onload = function() {
function createObj(name, age) {
var obj = new Object();
obj.name = name,
obj.age = age,
obj.year = function() {
console.log(this.name + '今年' + this.age + '歲了!')
}
return obj;
}
var obj = createObj('小明', 22);
obj.year();
}
// 小明今年22歲了!
優點:解決重複實例化對象的問題
缺點:無法識別對象的類型,因為所有的實例都指向一個原型
3. 構造函數#
window.onload = function() {
function Person(name, age) {
this.name = name;
this.age = age;
this.year = function() {
console.log(this.name + '今年' + this.age + '歲了!')
}
}
var student = new Person('小明', 22);
student.year();
}
// 小明今年22歲了!
優點:可以識別對象的類型
缺點:多個實例重複創建方法,無法共享
4. 原型模式#
window.onload = function() {
function Par() {}
Par.prototype = {
constructor: 'Par',
name: '小明',
age: 22,
year: function() {
console.log(this.name + '今年' + this.age + '歲了!')
}
};
var son = new Par();
son.year();
}
// 小明今年22歲了!
缺點:所有實例共享他的屬性和方法,不能傳參和初始化屬性值
5. 混合模式 (推薦使用)#
是構造函數和原型模式混合的寫法,擁有各自的優點,構造函數共享實例屬性,原型模式共享方法和想要共享的屬性,可以傳參和初始化屬性值
先用構造函數定義對象的屬性方法,然後用原型模式創建方法,使用的屬性通過 prototype 獲取,有一個 constructor 屬性,可以指向要操作的函數對象(構造函數)
比如constructor: Par
,就代表下面這個原型方法指向Par()
對象(構造函數)
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + '今年' + this.age + '歲了!');
}
};
var son = new Par('小明', 22)
son.year();
}
// 小明今年22歲了!
三、原型,原型鏈#
1. 原型對象#
- 函數對象都具有
prototype
屬性,它指向函數的原型對象 (瀏覽器內存創建的對象),原型對象都具有constructor
屬性,它指向prototype
屬性所在的函數對象 (構造函數)
window.onload = function() {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
// constructor指向對象
constructor: Par,
year: function() {
console.log(this.name + '今年' + this.age + '歲了!');
}
};
var son = new Par('小明', 22)
son.year();
/*********************************************/
console.log(Par.prototype)
console.log(Par.prototype.constructor)
/*********************************************/
}
通過控制台可以看到
構造函數的prototypr
屬性指向原型對象
原型對象的construcyor
屬性指向構造函數
- 當調用構造函數創建一個實例後,該實例會有一個隱藏屬性
__proto__
,它指向構造函數的原型對象
console.log(son.__proto__ === Par.prototype)
// true
- 所有的構造函數的 prototype 都是 object 類型
console.log(typeof Par.prototype)
// object
- Function 的 prototype 是一個空函數,所有內置函數的__proto__屬性都指向這個空函數
console.log(Math.__proto__)
- 如果構造函數實例和原型對象中同時定義了一個屬性,在調用時,會屏蔽原型對象中的屬性,如果想要訪問原型對象中的屬性值,需要通過
delete
方法將同名屬性在實例(構造函數)中徹底刪除
window.onload = function () {
function Par(name) {
this.name = name;
}
Par.prototype.name = "張三";
var son = new Par("李四");
console.log(son.name); // 李四
console.log(son.__proto__.name); // 張三
// 使用delete刪除實例的同名屬性值
console.log(delete son.name); // true
console.log(son.name); // 張三
}
- 通過
hasOwnProperty(屬性名)
可以判斷一個屬性存在於構造函數中,還是原型對象中
true
表示存在構造函數中;false
表示存在原型對象中
console.log(Par.hasOwnProperty(name)); // false
- 操作符
in
,可以判斷一個屬性是否存在(存在於構造函數和原型對象中皆可)
window.onload = function () {
function Par(name, age) {
this.name = name;
this.age = age;
}
Par.prototype = {
constructor: Par,
year: function() {
console.log(this.name + this.age)
}
};
var son = new Par('xm', '22')
son.year();
console.log('name' in Par); // true
console.log('age' in Par); // false
}
同樣的兩個屬性,判斷其是否存在於實例或者原型對象中,輸出的結果不一樣
參考:《對象中是否有某一個屬性 in》https://www.cnblogs.com/IwishIcould/p/12333739.html
2.__proto__和 prototype 的區別#
-
prototype
屬性只有函數對象上才有,而__proto__
屬性所有對象都有 -
prototype
是由函數對象指向原型對象,而__proto__
是由實例指向函數對象的原型對象 -
原型鏈,將父類型的實例作為子類型的原型對象,這種鏈式關係叫做
原型鏈
3. 繼承#
- 原型鏈繼承
優點:父類原型定義的屬性和方法可以復用
缺點:子類實例沒有自己的屬性,不能向父類傳遞參數
function test1() {
function SuperType() {
this.city = [ "北京", "上海", "天津" ];
this.property = true;
}
SuperType.prototype = {
constructor : SuperType, // 保持構造函數和原型對象的完整性
age : 15,
getSuperValue : function() {
return this.property;
}
};
function SonType() {
this.property = false;
}
// 重寫子類的原型指向父類的實例:繼承父類的原型
SubType.prototype = new SuperType();
SubType.prototype = {
constructor : SubType,
getSonType : function() {
return this.property;
}
};
// 優點驗證
let son = new SubType();
console.log(son.age); // 15
console.log(son.getSuperValue()); // false
// 缺點驗證
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津", "重慶"]
}
// test1();
- 構造函數繼承
優點:子類實例有自己的屬性,可以向父類傳遞參數,解決原型鏈繼承的缺點
缺點:父類原型的屬性和方法不可復用
function test2() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
age : 18,
showInfo : function() {
return this.name;
}
};
function SubType() {
// 父類調用call()或者apply()方法和子類共用同一個this,實現子類實例屬性的繼承
SuperType.call(this, "張三");
}
// 優點驗證
let instance = new SubType();
instance.city.push("重慶");
console.log(instance.city); // ["北京", "上海", "天津", "重慶"]
let instance1 = new SubType();
console.log(instance1.city); // ["北京", "上海", "天津"]
// 缺點驗證
console.log(instance.age); // undefined
instance.showInfo(); // son.showInfo is not a function
}
// test2();
- 組合繼承(推薦)
優點:原型的屬性和方法可以復用,每個子類實例都有自己的屬性
缺點:父類構造函數調用了兩次,子類原型中的父類實例屬性被子類實例覆蓋
function test3() {
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ]
}
SuperType.prototype = {
constructor : SuperType,
showInfo : function() {
console.log(this.name + "今年" + this.age + "歲了");
}
};
function SubType(name, age) {
// 1. 透過構造方法繼承實現實例屬性的繼承
SuperType.call(this, name);
this.age = age;
}
// 2. 透過原型鏈繼承實現原型方法的繼承
SubType.prototype = new SuperType();
// 優點驗證
let instance = new SubType("張三", 15);
instance.showInfo(); // 張三今年15歲了
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test3();
- 寄生組合繼承(推薦)
優點:解決了組合繼承的缺點,效率高
缺點:基本沒有
function test4() {
function inheritPrototype(subType, superType) {
// 1. 繼承父類的原型
var prototype = Object.create(superType.prototype);
// 2. 重寫被污染的construct
prototype.constructor = subType;
// 3. 重寫子類的原型
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.city = [ "北京", "上海", "天津" ];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// 將父類原型指向子類
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
// 優點驗證
let instance = new SubType("張三", 15);
instance.sayName(); // 張三
let instance1 = new SubType();
instance1.city.push("重慶");
console.log(instance1.city); // ["北京", "上海", "天津", "重慶"]
let instance2 = new SubType();
console.log(instance2.city); // ["北京", "上海", "天津"]
}
// test4();
4.ES6 新方法--class#
新的關鍵字class
在 es6 開始被引入到 javascript 中來,class
的目的就是讓定義類更簡單
用函數方法實現:
function Person(name) {
this.name = name;
}
Person.prototype.hello = function () {
console.log('Hello, ' + this.name + '!');
}
var son = new Person('xm')
son.hello(); // Hello, xm!
用class
來實現:
class Person {
constructor(name) {
this.name = name;
}
hello() {
console.log('Hello, ' + this.name + '!');
}
}
var son = new person('xm')
son.hello(); // Hello, xm!
可以在看到,在定義class
中,直接包含了構造函數constructor
屬性,和原型對象上的函數hello()
方法,省略掉了function
關鍵字
需要注意:原來的寫法是,構造函數和原型對象分散開來寫,現在用class
可以直接把兩者串在一個對象中,只有最後傳參和調用方法時寫法是相同的
class 繼承
用class
定義對象的另一個巨大的好處是繼承更方便了。想一想我們從Person
派生一個PrimaryPerson
需要編寫的代碼量。現在,原型繼承的中間對象,原型對象的構造函數等等都不需要考慮了,直接通過extends
來實現:
class PrimaryPerson extends Person {
constructor(name, grade) {
super(name); // 記得用super調用父類的構造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
注意PrimaryPerson
的定義也是通過 class 關鍵字實現的,而extends
則表示原型鏈對象來自Person
,子類的構造函數可能會和父類的不太相同
例如,PrimaryPerson
需要name
和grade
兩個參數,並且需要通過super(name)
來調用父類的構造函數,否則父類的name
屬性無法正常初始化。
PrimaryPerson
已經自動獲得了父類Person
的hello方法
,我們又在子類中定義了新的myGrade
方法。
ES6 引入的class
和原有的JavaScript原型繼承
有什麼區別呢?
實際上它們沒有任何區別,class
的作用就是讓 JavaScript 引擎去實現原來需要我們自己編寫的原型鏈代碼。簡而言之,用class
的好處就是極大地簡化了原型鏈代碼。
但是!
目前並不是所有的瀏覽器都支持class
,所以在選擇的時候一定要慎重!