面向對象一、原型


title: 面向對象一、原型
date: 2017-06-16 17:01:01
tags: javascript筆記


面向對象的概念

面向對象是一種編程思想,核心是解決任何問題的時候首先試圖去找到一個對象來幫助解決問題

在編程中,面向對象是調度者,從根本上是將面向過程封裝,所以面向過程不可棄之不用。面向過程是執行者,執行順序一般情況不能打亂。

面向對象的優點:

  • 代碼的靈活度高,代碼執行順序可以打亂。面向過程的代碼不能打亂。

  • 可維護性高,出現bug只需要在對象上去調試。

  • 擴展性高,擴展時只需要維護局部模塊。

面向對象的缺點:

  • 可能會造成代碼復雜度提高

  • 代碼可讀性相對不好

js語言的特點

  1. 弱類型

  2. 多范式

  3. 基于對象的語言:

  • 在Javascript中,一切的根源都是對象,并沒有面向對象的一些概念,所以說是基于對象的語言,通常把構造函數當做一個模板,通過模板對建立對象

  • 在其他面向對象的語言中,有類這個概念,Javascript用構造函數來模擬類,類和構造函數都是起到了模板的作用

  • 不是面向對象的語言,只是用面對象向這種思想來模擬

  1. 基于原型的語言:
  • 弱類型的語言基本都有原型存在,在面向對象的語言中,是類和類之間繼承,而Javascript中只能讓對象和對象之間繼承

原型的介紹

原型的概念

就是一個函數的prototype屬性所引用的對象,原型是Javascript內置的,只要聲明了一個函數,那么原型就自動存在了。

function fn (){}    // 這是一個構造函數
console.log(fn.prototype)    // 這個構造函數的原型
console.log(fn.prototype.constructor)     //這個構造函數
// 所以構造函數和它的原型是能互相訪問的。

原型的意義

通過同一個構造函數創建出的所有對象都共享這個構造函數的原型,也就是說上述創建出的所有對象,可以直接訪問到原型上的任何成員(屬性和方法)。

function fn (){}
var f = new fn
f.constructor      //就是fn這個構造函數,所以在fn這個構造函數里增加屬性和方法對象f是可以直接訪問的

原型的本質

就是一個對象。在原型中創建方法就和給對象加方法一樣。

原型的好處

可以實現數據共享。用下面的代碼列舉問題來看一下原型的好處

function Preson () {
  this.talk = function(){ console.log("talk") }
}
var jim = new Preson;
jim.talk();
var john = new Preson;
john.talk();
// 問題:以上創建的兩個對象訪問了兩次構造函數的talk()方法,這樣對象每次訪都是創建一個新的只屬于這個對象的函數,每創建一個方法都是占用一塊內存,而方法中的邏輯實際上都是一樣的。這就相當于浪費了一塊內存的位置

// 解決方法:把建立在構造函數內的方法放在一個公共的地方,而這個公共的地方必須是該構造函數創建出來的,這樣對象才能訪問到,也就是該構造函數的原型上,實現數據共享

fn.prototype.talk = function(){ console.log("talk") }

獲取原型的方式:

函數:函數名.prototype

對象:對象.__proto__

對象的組成部分:

對象本身和它的原型組成

每個對象都有__proto__屬性,也就是說每個對象都有原型,所以說Javascript是基于原型的語言,

對象的類型:

就是該對象的構造函數的名字,Javascript雖然是弱類型語言,并不是沒有類型,而是不注重類型的存在,體現在所有對象用typeof去檢測都是object,所以也可以說所有對象的原型都是Object.prototype。

自定義一個數組對象的原型還是一個數組對象。他們的構造函數就是Array內置函數,包括Object,Array,Date都是內置函數。new后邊的都是函數。

原型的歸屬:

原型的屬性:給原型一個歸屬,也就是什么什么的原型,通常說原型是站在函數的角度去認識原型,那么站在函數的角度來說,原型可以被稱為該函數的原型屬性。

原型的對象:是站在對象的角度來看原型,此時原型可稱為是這個對象的原型對象。

這兩者只是稱謂不同,實際上都是同一個原型。

__proto__的兼容性處理:

兩個下劃線的屬性是有兼容性的,這不是W3C的標準屬性,只是瀏覽器給提供的便利的東西。

function getPrototype(obj) {
  // 判斷瀏覽器是否兼容__proto__
  if (obj.__proto__) { // 如果支持
    return obj.__proto__;
  } else { // 如果不支持
    // 獲取該對象的構造函數
    // 在通過此函數的prototype屬性獲取其原型對象
    return obj.constructor.prototype;
  }
}

// 三元表達式寫法
function getPrototype(obj) {
  return !!obj.__proto__ ? obj.__proto__ : obj.constructor.prototype;
}

標準構造函數寫法

主要就是要考慮哪些屬性應該保留在構造函數內部,哪些屬性提取出來放在原型上。

和對象息息相關的屬性,這些屬性都要寫在構造函數內部。像姓名、年齡這些屬性,是隨著對象不同而改變的,所以沒法放在原型上,而是要放到構造函數內部。

而那些為了共享的屬性并且是每個對象都具有的屬性,值也不會隨對象變化而變化是確定的值。可以寫在原型上。比如每個人都生活在地球上。

在一般情況下,方法被認為是所有對象所共有的。比如一般情況下人都會說話。所以所有方法都應該放在原型上。

原型的特性

動態性

給原型擴展成員會直接反應到已創建的對象身上。

function A() {}
A.prototype.color = 'black';
var a = new A;
var ad = new A;
// 已經創建對象之后再去擴展原型上的屬性,也會反應到對象身上
A.prototype.makefood = function (){ console.log('做飯')}
a.makefood(); // 做飯
ad.makefood(); // 做飯

置換原型對象,不會反映到已創建出來的對象。但是會直接影響之后創建出來的對象。

function A() {}
A.prototype.color = 'black';
var a = new A;
A.prototype = {
  constructor: A,
  makeup: function() {
    console.log('我會化妝.');
  }
};
var na = new A;
console.log(a.color);     // black,因為a是置換對象之前創建的,所以它的原型就是A置換之前的原型。
a.makeup();      // 報錯,因為a是在置換原型之前創建的對象。
na.makeup();     // 我會化妝.因為na是在置換原型之后創建的對象
console.log(na.color);       // undefined 因為這是在置換之前擴展的。擴展之后color屬性就沒了

唯一性

由同一函數創建出來的所有對象,共享同一個原型對象。

// 由同一個構造函數創建出的對象都共享同一個原型。
function A() {}
A.prototype.color = 'black';
var a = new A;
var ad = new A;
// 這兩個對象的原型對象全等,也就是同一個,并且共享原型對象的屬性。
console.log(a.__proto__ === ad.__proto__);    // true
console.log(ad.color);     // black
console.log(a.color);     // black

不可變性

對象是無法改變原型對象上的任何成員

function A() {}
A.prototype.color = 'black';
var a = new A;
// 更改a的color屬性只能改變自身不會改變其他成員,并不會改變它的原型上的color屬性,所以也不會改變ad的color。
a.color = 'goldyellow';
var ad = new A;
console.log(ad.color);     // black
console.log(A.prototype.color);     // black
console.log(a.color);      // goldyellow

繼承性

所有對象都繼承自它的原型對象

function A() {}
A.prototype.color = 'black';
var a = new A;      // a和na都是構造函數A創建出的對象,,都繼承了A的原型
var na = new A;
console.log(a.color);   // black
console.log(na.color);      // black

面向對象的三大特性

封裝性

把復雜的實現過程包裝并隱藏起來,然后提供一個接口來給用戶使用。

封裝的好處

  1. 實現代碼的重復利用。

  2. 實際使用中,只要出現重復代碼邏輯就要考慮封裝成一個函數,如果該函數和一些變量關聯性比較大,那么就可以將函數封裝成一個對象。

  3. 私密性(安全性),封裝后用戶看不到復雜的內部代碼,不會誤操作覆蓋封裝的變量。

  4. 封裝時盡量保持函數或對象功能的單一性,便于日后維護。

繼承性

  1. 概念:就是指一個對象有權去訪問另一個對象的屬性和方法,自己沒有的屬性和方法可以去訪問另一個對象去獲得,在js中只要讓一個對象去訪問另一個對象的屬性和方法的話就必須要建立繼承,任何對象都繼承自己的原型對象。

  2. 在js中繼承是對象與對象之間,其他面向對象語言(c,java,objectC等)都是類與類之間的繼承。類在其他語言里就相當于模板的意義,在js中模板是構造函數,那么通過同一個構造函數(模板)創建出來的對象都繼承函數里的屬性和方法(ES6之前的方式)

  3. 在實際開發中兩種繼承方式可以組合起來應用。

集成的實現方式1:基于原型

擴展原型:在原有的原型上進行相應的擴展,實現繼承。

在對象的構造函數的原型上進行擴展,那么該對象也就繼承了擴展的內容

function A () {}               
var a = new A;
// a本身沒有printA這個方法,但是它的模板創建了這個方法,所以a也繼承了這個方法
A.prototype.printA = function  () { console.log("擴展原型") }
a.printA();

置換原型:將要被繼承的對象,直接替換掉原有的原型。

假如b要繼承a,就把b構造函數的原型直接替換成new a(傳參)

// 首先創建了這個構造函數用parent代表 child要繼承自parent
function parent () {
  this.name = 'tom';
}
// 給這個模板的原型創建了一個方法
parent.prototype.printC = function  () {
  console.log("c");
  console.log(this.name);
}
// 又創建了一個構造函數。child代表繼承自parent。
function child () {}

// 讓child的原型 = 模板parent創建的對象。parent函數的name和printC也繼承給了child,同時傳參數也不影響parent函數
child.prototype = new parent();

//創建對象c,因為child的原型已經和函數parent一樣,所以用c可以直接訪問name和printC了
var c = new child;
c.printC();     // 打印c
console.log(c.name);      // 打印tom

集成的實現方式2: 拷貝繼承

拷貝繼承:將別的對象上的所有成員拷貝一份添加到的當前對象本身,拷貝繼承沒有任何對原型的操作。

// 創建一個對象parent
var parent = {
  print: function() {
    console.log('i am parent');
  },
  name: 'parent'
}
// 創建一個對象child
var child = {
  name: 'child'
}

// child沒有print方法,那么可以拷貝一份過來
// 拷貝步驟:
// 1、遍歷parent。
for (var k in parent) {
  //這樣就將parent的所有屬性都拷貝了過來,k就是name和print,child依次更改和創建了這兩個屬性,并且將parent對應的屬性值賦值給child.
  child[k] = parent[k];
}
child.print();      // 打印i am parent
// 拷貝繼承概念部分結束

下面是一個問題,就是代碼重復的問題,當child還要繼承parent1的時候就要再寫一遍遍歷,造成了代碼重復


var parent1 = {
  print1:function(){
    console.log('print1');
  }
}
for (var k in parent1) {
  child[k] = parent1[k];
}
child.print1();     // 打印print1,但是如果要拷貝多個parent那么代碼會重復

所以可以封裝成一個函數,封裝為child對象的一個方法extend,誰調用extend方法就是給誰實現繼承

// 新建一個child對象
var child = {}
// 給child創建一個名為extend方法,里邊的函數就是封裝的拷貝方法
child.extend = function(parent) {
  var k;
  // parent是傳的參數
  for (k in parent) {
    this[k] = parent[k]
  }
}
// 第一步封裝完成,但是問題是只能往里傳一個對象

// 實現繼承多個對象
child.extend = function() {
  // arguments是傳入參數的數據的數組,在這里也就是傳入的對象數量
  var args = arguments;
  //遍歷atguments上的所有對象
  //依次將遍歷的每個對象的成員添加到child
  for (var i = 0, l = args.length; i < l; i++) {
    //判斷傳入的是否為對象
    if (typeof obj === 'object') {
      for (var k in args[i]) {
        this[k] = args[i][k]
      }
    }
  }
}

// 調用這個對象的拷貝方法并且傳一個參數,參數是對象
child.extend({
  name: 'child',
  print: function() {
    console.log(this.name);
  }
})
child.print();      // 打印child

集成的實現方式3:對象冒充

對象冒充:在一個構造函數中可以動態的添加一個parent方法指向,用已有的構造函數,然后調用parent方法去實例化當前對象的一部分成員(或全部成員),這種方式被稱為對象冒充。

function parent (name,age,gender) {
  this.name = name;
  this.age = age;
  this.gender = gender
}

function child(name,age,gender) {
  this.parent = parent;
  this.parent(name,age,gender);
  delete this.parent;
  // child通過這個屬性冒充parent,通過這個構造函數創建對象也會有parent里的成員
  // 注意:child利用完parent屬性后記得刪除
}

集成的實現方式4:借調函數

function parent (name,age,gender) {
  // body...
  this.name = name;
  this.age = age;
  this.gender = gender;
}
// 和冒充類似,這是用call方法實現的
function child (name,age,gender,address) {
  // body...
  parent.call(this,name,age,gender);
  this.address = address;
}
var c= new child('tom',28,'男','yueqiu');

集成的實現方式5:Object.create(parent) (置換原型的原理)

// 方法的介紹 Object.create(parent); 返回一個對象并繼承自傳入參數parent
// 用基于原型:置換原型的方式來繼承

var obj = {
  name:'tom',
  print:function(){
    console.log(this.name);
  }
}

// 將obj當做參數傳進來,newObj就繼承了obj
// 聲明一個新變量來接收繼承自parent的對象
var newObj = Object.create(obj)
newObj.print();

// 下面是Object.create解決兼容性問題
if(!Object.create){
  Object.create = function(parent){
    function F () {
      F.prototype = parent;
      return new F;
    }
  }
}

多態性

體現在繼承中的概念。比如某對象A繼承自某對象B,B對象的某個方法在A中并不適用。然后A對象重寫該方法,那么這個就是多態性。

具體體現在子對象和父對象之間,在父對象中的同一方法在各個子對象中的實現行為不同。

原型鏈

原型鏈是從當前對象到Object.prototype之間,存在一條層次分明,逐級遞進的體現繼承關系的鏈式結構

所有對象都有__proto__屬性

原生對象繼承自Object.prototype,具有constructor屬性;如果置換了原型,記得要添加constructor屬性

函數具有prototype屬性

var o = {}; //Object對象,對象都有__proto__屬性

// 對象的__proto__是它的原型對象
console.log(o.__proto__)

// 對象的constructor是它的數據類型Object
console.log(o.constructor)

// 對象的原型就是Object.prototype
console.log(Object.prototype === o.__proto_);

console.log(o.__proto__.__proto__); 
// 也就是console.log(Object.prototype.__proto__);
// 返回null
// 所以 o -> Object.prototype -> null

// Object對象的繼承層次:
// obj -> Object.prototype -> null

// 以數組為例
var arr = [];
console.log(arr.constructor === Array);    // true
console.log(arr.__proto__ === Array.prototype);     // true
// arr ->  Array.prototype -> Object.prototype -> null

用一張圖來表示原型鏈:這是最簡單最基礎的原型鏈

image

用兩個例子來深入體會原型鏈:

function parent() {}
var p = new parent;
image
function A() {}
function B() {}
B.prototype = new A; // 這時B.prototype的構造函數就是A(),
var b = new B;
image

屬性搜索原則

當訪問對象成員時,首先在當前對象上查找,如果找到就直接返回(調用)并且停止查找

如果沒有找到就向其原型對象上去查找,如果找到就直接返回(調用)

如果還是沒有找到,就繼續向原型對象的原型對象上查找,直到Object.prototype。

如果找到了,就直接返回(調用),并停止查找,否則返回undefined(報錯:xxx is not a function)

注意:

  1. 如果訪問對象的某個屬性不存在的話,會搜索整個原型鏈,有可能會導致js性能降低。

  2. 在實際開發中盡量保持一個適度的原型鏈長度。

  3. 兼顧js性能以及代碼的可讀性和擴展性

Object.prototype介紹

constructor

就是自己的構造函數(function Object( ) { [native code] })

hasOwnProperty()

hasOwnProperty() 判斷指定的屬性是否為當前對象自己的(自己的就是指不是繼承過來的)

構造函數里的就是對象自己的,原型上的就是繼承的。

格式:obj.hasOwnProperty('屬性名')

var o = {name:'tom'};
console.log(o.hasOwnProperty('name')); //返回true
console.log(o.hasOwnProperty('toString')); //返回false

isPrototypeOf()

isPrototypeOf() 用來判斷當前對象是否是指定對象的原型對象

格式:obj1.isPrototypeOf(obj2)

// 只要對象A出現在B對象的原型鏈上就返回true,否則返回false

function A () {}
function B () {}

 var a = new A;
 var b = new B;
//現在沒有任何關系
 console.log(a.isPrototypeOf(b));  //返回false

var a = new A
B.prototype = a;
var b = new B
// b -> a -> a.prototype -> Object.prototype -> null

propertyIsEnumerable()

propertyIsEnumerable() 判斷對象指定的屬性是否可枚舉,并且指定的屬性必須是自己的,兩者都滿足才能返回 true

function foo(name, age, address) {
  // body...
  this.name = name;
  this.age = age;
  this.address = address;
}   
foo.prototype.talk = function(){
  console.log(this.name);
}
console.log(f.propertyIsEnumerable('name'));   // 返回true
console.log(f.propertyIsEnumerable('talk'));   // 返回false

// 可枚舉就是用 for in 遍歷出來的屬性都是可枚舉的。內置的屬性(__proto__)都是不可枚舉的

var obj = {
  name:'tom';
  age:18;
}

構造函數的執行過程

// 構造函數的執行過程

function Fn (name,age) {
  this.name = name;
  this.age = age;
}

var Fn = new Fn();
  1. 創建了一個空對象

  2. 將obj賦值給this (讓this指向上面創建空對象,也就是Fn)

  3. 將當前作用域交給this

  4. 執行構造函數內部的代碼

  5. 將this返回 new的時候函數內部會默認return this

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容