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運算符來驗證對象和實例對象之間的關系。

構(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對象, 因此提高了運行效率。

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

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

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

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.

注意, 每一個實例都有一個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。因此, 上面的代碼是有問題的。

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

(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)用父對象 的方法(添加該屬性以備后用)。