JavaScript 模擬面向?qū)ο缶幊?/h2>

JavaScript是一種基于對象的語言, 所有的東西幾乎都是對象。 但它又不是一種真正的面向?qū)ο缶幊陶Z言, 因為它的語法中沒有class類。

如果我們想在JavaScript中模擬面向?qū)ο缶幊蹋?要怎么做?

1. “封裝”數(shù)據(jù)和方法

(1. 構(gòu)造函數(shù)模式

所謂構(gòu)造函數(shù), 其實就是一個普通的函數(shù), 但是內(nèi)部使用了this變量。 對構(gòu)造函數(shù)使用new關鍵字, 就能生成實例, 并且this變量會綁定到實例對象上。

function Dog(name, color){
  this.name = name; 
  this.color = color;
}

// 實例對象 
var dog1 = new Dog('dog11', 'red');

var dog2 = new Dog('dog22', 'blue');

console.log(
  dog1.name,
  dog1.color,
  dog2.name,
  dog.color
);

此時 dog1和dog2會自動有一個constructor屬性, 指向它們的構(gòu)造函數(shù)

dog1.constructor === Dog
dog2.constructor === Dog

JavaScript提供了一個instanceof運算符來驗證對象和實例對象之間的關系。

image.png

構(gòu)造函數(shù)模式的問題

構(gòu)造函數(shù)方法很好用, 但是存在一個浪費內(nèi)存的問題。
如果我們?yōu)镈og對象添加一些不變的屬性并再添加一些方法, 那么原型對象Dog可能會變成下面這樣:

functiont Dog(name,color){

    this.name = name;

    this.color = color;

    this.type = "動物";

    this.eat = function(){alert("吃屎!")};
  }

若還是像之前的一樣去生成實例, 有一個很大的問題。 那就是每個實例對象, type屬性和eat方法都是一樣的內(nèi)容, 每一次生成一個實例, 都必須為重復, 多占用一些內(nèi)存,這肯定是有問題的。
那么, 有沒有可能讓type屬性和eat()方法成為公共的屬性和方法, 只在內(nèi)存中生成一次呢?然后所有的實例地址都指向那個內(nèi)存地址去引用?這里就需要使用到prototype模式(即原型&原型鏈)。

(2. Prototype模式

JavaScript中規(guī)定, 每一個構(gòu)造函數(shù)都有一個prototype屬性, 指向另一個對象。 這個對象的所有屬性和方法, 都會被構(gòu)造函數(shù)的實例繼承。
因此, 上面的代碼就可以改造成如下:

function Dog(name,color){

    this.name = name;

    this.color = color;
  }
Dog.prototype.type = "動物";
Dog.prototype.eat = function(){alert("吃屎!")};

然后生成實例時, 所有的實例type屬性和eat()方法, 其實都是同一個內(nèi)存地址, 指向prototype對象, 因此提高了運行效率。


image.png

isPrototypeOf() 判斷對象和實例之間的關系

image.png

hasOwnProperty()

每個實例對象有一個hasOwnProperty()方法, 用來判斷某一個屬性是不是本地屬性, 還是繼承自prototype對象屬性。


image.png

in運算符

in運算符可以用來判斷, 某個實例是否含有某個屬性, 不管是不是本地屬性。


image.png

2. 繼承

現(xiàn)在有兩個構(gòu)造函數(shù), 如何實現(xiàn)Dog繼承自Animal呢?

function Animal(){
  this.type = "動物";
}
Animal.prototype.eat = function(){
  console.log('eat!!!');
}
function Dog(name, color){
  this.name = name;
  this.color = color;
}

(1. prototype模式

如果Dog的prototype對象, 指向一個Animal實例, 那么所有Dog的實例, 就能繼承Animal了。

Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
var dog1 = new Dog('dog1', 'yellow');
console.log(dog1.type, dog1.eat());

代碼的第一行, 將Dog的prototype對象指向了Animal的實例。
它相當于完全刪除了prototype對象原先的值, 然后賦予了一個新值。

第二行Dog.prototype.constructor = Dog;是什么意思呢?
這里其他是將Dog的構(gòu)造函數(shù)指向了Dog.

image.png

注意, 每一個實例都有一個constructor屬性, 默認調(diào)用prototype對象的constructor屬性。

因此, 在執(zhí)行Dog.prototype = new Animal();時, Dog.prototype的constructor是指向Animal的。這顯然導致了繼承鏈的錯亂, 因此在第二行進行了糾正。這是很重要的一點, 因此在JavaScript模擬面向?qū)ο缶幊虝r要注意。

(2. 直接繼承prototype

改進下上面的代碼如下:

Dog.prototype = Animal.prototype;
Dog.prototype.constructor = Dog;
var dog1 = new Dog('dog1', 'yellow');
console.log(dog1.type, dog1.eat());

與前面相比, 這段代碼效率更高點(不用執(zhí)行和建立Animal實例)。 但缺點是Dog.prototype和Animal.prototype現(xiàn)在都指向了同一個對象, 那么對Dog.prototype的修改, 都會被反映到Animal.prototype。因此, 上面的代碼是有問題的。


image.png

所以, 第二行代碼Dog.prototype.constructor = Dog, 其實也把Animal.prototype.constructor屬性的值也改掉了。

image.png

(3. 利用空對象作為中介

由于”直接繼承prototype"存在上述缺點, 因此我們繼承改造, 利用一個空對象作為中介。

var F = function(){};
F.prototyoe = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;

因為F是空對象, 所以幾乎不占用內(nèi)存, 這時修改Dog的prototype對象, 就不會影響到Animal.prototype。

于是, 我們將上面的方法封裝成一個函數(shù), 便于以后使用:

function extend(child, parent) {
  var F = function(){};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  Child.uber = Parent.prototype;
}

使用方法如下:

extend(Dog, Animal);
var dog1 = new Dog('dog1', 'yellow');
console.log(dog1.type);

另外, 說明 一下, 函數(shù)最后一行: Child.uber = Parent.prototype; 意思是為子對象設置一個uber屬性, 這個屬性直接指向父對象的prototype, 這等于在子對象上打開一條通道, 可以直接調(diào)用父對象 的方法(添加該屬性以備后用)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容