作為一門被長期誤解的編程語言,javascript一直被人所詬病.但是如果你真正的了解它之后,你會深深的愛上它.
首先,javascript是一個面向對象的編程語言,而且是一個純粹的面向對象.雖然很多人不能理解,因為在他們眼中,只有像java,c++這樣的編程語言才能稱之為面向對象.但是,我只想說,你誤解我,是因為你不懂我.
JavaScrip秘密花園在JavaScrip中,一切變量皆對象,除了兩個特殊值undefined 和 null.
什么是面向對象?
- 一切事物皆對象
- 對象具有封裝和繼承特性
- 對象與對象之間使用消息通信,各自存在信息隱藏
以這三點做為依據,C++ 是半面向對象半面向過程語言,因為,雖然他實現了類的封裝、繼承和多態,但存在非對象性質的全局函數和變量。Java、C# 是完全的面向對象語言,它們通過類的形式組織函數和變量,使之不能脫離對象存在。但這里函數本身是一個過程,只是依附在某個類上。
然而,面向對象僅僅是一個概念或者編程思想而已,它不應該依賴于某個語言存在。比如 Java 采用面向對象思想構造其語言,它實現了類、繼承、派生、多態、接口等機制。但是這些機制,只是實現面向對象編程的一種手段,而非必須。換言之,一門語言可以根據其自身特性選擇合適的方式來實現面向對象。所以,由于大多數程序員首先學習或者使用的是類似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但一般做為編譯型來講解),因而先入為主地接受了“類”這個面向對象實現方式,從而在學習腳本語言的時候,習慣性地用類式面向對象語言中的概念來判斷該語言是否是面向對象語言,或者是否具備面向對象特性。這也是阻礙程序員深入學習并掌握 JavaScript 的重要原因之一。
實際上,JavaScript語言是通過一種叫做原型(prototype)的方式來實現面向對象編程的。它和其他的面向對象類編程語言一樣,只是它的實現方式不同而已,或者說他們采用了不同的面向對象設計哲學。
在基于類的面向對象方式中,對象(object)依靠類(class)來產生。而在基于原型的面向對象方式中,對象(object)則是依靠 構造器(constructor)利用 原型(prototype)構造出來的。
舉個客觀世界的例子來說明二種方式認知的差異。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設計規定這輛車應該如何制造。這里的工程圖紙就好比是語言中的 類 (class),而車就是按照這個 類(class)制造出來的;另一方面,工人和機器 ( 相當于 constructor)利用各種零部件如發動機,輪胎,方向盤 ( 相當于 prototype 的各個屬性 ) 將汽車構造出來。
在這里我不想討論太多這兩種面向對象究竟孰優孰劣的問題,這個討論來討論去也不會有定論.但是以為個人觀點我更傾向于基于原型的面向對象.因為一切事物皆對象.現實世界中的所有對象也都是由其他對象構造出來的,而緊緊依靠圖紙是沒法產生一個現實中的汽車的.這個設計更符合現實世界的客觀規律.
JavaScript的面向對象來源于‘self’這個牛逼但短命的編程語言。
Self語言把概念上的精簡作為設計原則。它取消了類的概念,只有對象的概念,同時把消息作為最基本的操作。把對象的屬性理解為獲取或更改屬性這兩種方法,從而把屬性的概念簡化為方法;取消了變量和賦值,并以通過消息來讀槽和寫槽的方式代之。
在 JavaScript 中,prototype 是函數的一個屬性,同時也是由構造函數創建的對象的一個屬性。 函數的原型為對象。 它主要在函數用作構造函數時使用。
在編程語言中,目前存在兩種繼承方式:類繼承和原型繼承.
類繼承:
原型繼承
而由于java等主流編程語言的支持使得類繼承被大眾所普遍接受
那么,JavaScript中是如何實現的基于原型的面向對象?
要理解原型繼承,首先得先熟悉幾個概念,咱們一步步說起:
如何生成對象?
-1.聲明對象直接量:JSON
var obj = {
name: "jack",
eat: "bread"
}
console.log(typeof obj);
-2.使用構造函數生成一個新的對象
//構造函數
var Foo = function(name){
this.name = name; //私有屬性
}
//原型方法和屬性,被繼承時候才會調用
Foo.prototype.run = function(){
alert("I'm running so fast that can't stop at all!");
}
var kick = new Foo("kick");
console.log(typeof kick);
console.log(kick.name);
kick.run();
-3.使用使用Object.create創建對象
ECMAScript 5中引入了一個新方法: Object.create. 可以調用這個方法來創建一個新對象. 新對象的原型就是調用create方法時傳入的第一個參數:
先來看一下create方法是如何實現的,該方法來源于Douglas Crockford,現在已被ECMAScript 5引入:
Object.create = function (parent) {
function F() {}
F.prototype = parent;
return new F();
};
這個看起來很簡潔,而且能夠完全代替new的用法,畢竟new關鍵字并不真正的屬于JavaScrip的原型模式.它先是聲明了一個構造器,然后將其原型設置為你想要的值,最后返回生成的新對象.其實就是封裝了new.
下面這段代碼就是真正的原型繼承了.look:
var Point = {
x: 0,
y: 0,
print: function () { console.log(this.x, this.y); }
};
var p = Object.create(Point); //new一個對象
p.x = 10;
p.y = 20;
p.print(); // 10 20
code:
function Plant(name,year){
this.name = name;
this.year = year || 0;
}
var tree.prototype = new Plant('tree');
tree.prototype.grow = function(){
this.year ++;
}
tree.prototype.old = functiono(){
console.log(this.year);
}
上面這段代碼使用原型實現了一個簡單的對象繼承.下面來分析下上面這段代碼
首先是聲明了一個構造函數,構造函數和普通函數有什么區別?構造函數可以使用new調用,生成一個新的對象.
如果想要在對象上添加方法,可以將方法寫在對象的原型上.
子類繼承父類,只需要把父對象復制給自對象的原型上即可.
JavaScrip原型鏈(prototype chain)
下面這段是ECMAScript關于原型的解釋
ECMAScript does not contain proper classes such as those in C++, Smalltalk, or Java, but rather, supports constructors which create objects by executing code that allocates storage for the objects and initialises all or part of them by assigning initial values to their properties. All constructors are objects, but not all objects are constructors. Each constructor has a Prototype property that is used to implement prototype-based inheritance and shared properties. Objects are created by using constructors in new expressions; for example, new String("A String") creates a new String object. Invoking a constructor without using new has consequences that depend on the constructor. For example, String("A String") produces a primitive string, not an object.
ECMAScript supports prototype-based inheritance. Every constructor has an associated prototype, and every object created by that constructor has an implicit reference to the prototype (called the object's prototype) associated with its constructor. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain. When a reference is made to a property in an object, that reference is to the property of that name in the first object in the prototype chain that contains a property of that name. In other words, first the object mentioned directly is examined for such a property; if that object contains the named property, that is the property to which the reference refers; if that object does not contain the named property, the prototype for that object is examined next; and so on.
依據我的理解就是說:
JavaScrip可以采用構造器(constructor)生成一個新的對象,每個構造器都擁有一個prototype屬性,而每個通過此構造器生成的對象都有一個指向該構造器原型(prototype)的內部私有的鏈接(proto),而這個prototype因為是個對象,它也擁有自己的原型,這么一級一級指導原型為null,這就構成了原型鏈.
這里我們涉及到了一個隱匿屬性proto,那么proto和prototype究竟有什么區別嘞?
注: proto 是一個不應在你代碼中出現的非正規的用法,這里僅僅用它來解釋JavaScript原型繼承的工作原理。
知道了JavaScrip原型鏈的存在之后,讓我們來看下它的實現,下面這段代碼展示了原型鏈是如何工作的.
function getProperty(obj, prop) {
if (obj.hasOwnProperty(prop)) //首先查找自身屬性,如果有則直接返回
return obj[prop]
else if (obj.__proto__ !== null)
return getProperty(obj.__proto__, prop) //如何不是私有屬性,就在原型鏈上一步步向上查找,直到找到,如果找不到就返回undefind
else
return undefined
}
So,如果proto可以使用的話,我們可以通過下面這種方式實現繼承:
var person = {
city: "Beijing",
hate: function(){
alert("I really hate the PM2.5 and the foggy wether!");
}
}
var lee = {
name: "lee",
age: "18",
__proto__: person
}
console.log(lee);
lee.hate();
這都什么玩意兒,不是要用new嗎.事實上,事情不是這么簡單滴, 為了和主流的類繼承扯上那么一點兒關系,JavaScrip引入了'new'關鍵字,引入了構造函數.所以通常我們看到的是下面這樣的:
var Person = function(name,age){
this.name = name;
this.age = age;
};
Person.prototype = {
city: "Beijing",
hate: function(){
alert("I really hate the PM2.5 and the foggy wether!");
}
}
var lee = new Person('lee',18);
console.log(lee.name);
lee.hate();
我們需要一個像類一樣的東西,于是有了構造函數,我們得有一個通過類生成實例的過程,于是又出現了new.這么一來JavaScrip的原型繼承似乎就變得不倫不類了.雖然JavaScrip的原型繼承來源于'self',但是卻追隨了類繼承的形式.罪過,不過話說回來,也許就是因為這種妥協才讓JavaScrip能夠流行起來,并成為了現在最流行的原型繼承語言,而self,說實話,它獨特寫法確實挺難讓人接受的.
var Foo = function(){
this.name = "foo";
}
Foo.prototype.say = function(){
alert("Hello World!");
}
var foo = new Foo();
console.log(foo.__proto__); //私有鏈接,指向構造函數的原型
console.log(Foo.prototype);
console.log(foo.__proto__ === Foo.prototype); //true
console.log(foo.__proto__.constructor === Foo); //true
// 聲明 Animal 對象構造器
function Animal(name) {
this.name = name;
}
// 將 Animal 的 prototype 屬性指向一個對象,
// 亦可直接理解為指定 Animal 對象的原型
Animal.prototype = {
weight: 0,
eat: function() {
alert( "Animal is eating!" );
}
}
// 聲明 Mammal 對象構造器
function Mammal() {
this.name = "mammal";
}
// 指定 Mammal 對象的原型為一個 Animal 對象。
// 實際上此處便是在創建 Mammal 對象和 Animal 對象之間的原型鏈
Mammal.prototype = new Animal("animal");
// 聲明 Horse 對象構造器
function Horse( height, weight ) {
this.name = "horse";
this.height = height;
this.weight = weight;
}
// 將 Horse 對象的原型指定為一個 Mamal 對象,繼續構建 Horse 與 Mammal 之間的原型鏈
Horse.prototype = new Mammal();
// 重新指定 eat 方法 , 此方法將覆蓋從 Animal 原型繼承過來的 eat 方法
Horse.prototype.eat = function() {
alert( "Horse is eating grass!" );
}
// 驗證并理解原型鏈
var horse = new Horse( 100, 300 );
console.log( horse.__proto__ === Horse.prototype );
console.log( Horse.prototype.__proto__ === Mammal.prototype );
console.log( Mammal.prototype.__proto__ === Animal.prototype );
//原型鏈
Horse-->Mammal的實例
Mammal-->Animal的實例
Animal -->Object.prototype
在 ECMAScript 中,每個由構造器創建的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之為 原型(prototype)。進一步,每個原型可以擁有指向自己原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的 原型鏈(prototype chain) 參考資源。在具體的語言實現中,每個對象都有一個 proto 屬性來實現對原型的 隱式引用。
我們已經了解了JS原型繼承是什么,以及JS如何用特定的方式來實現之。然而使用真正的原型繼承(如 Object.create 以及 proto)還是存在以下缺點:
- 標準性差:proto 不是一個標準用法,甚至是一個不贊成使用的用法。同時原生態的 Object.create 和道爺寫的原版也不盡相同。
- 優化性差: 不論是原生的還是自定義的 Object.create ,其性能都遠沒有 new 的優化程度高,前者要比后者慢高達10倍。
到了這里我們基本對JavaScrip的原型繼承有了一個更深層的認識了.通過歷史回溯我們也了解了為什么JavaScrip會變成現在這個不倫不類的樣子.
JavaScrip是一個完全的面向對象函數式編程語言,采用原型繼承,雖然寫法類似類繼承.但是我們不能因此就認為它不是面向對象的編程語言.而且nodejs的出現,又讓JavaScrip在編程語言界火了一把.所以是時候擁抱JavaScrip了.