JavaScript中的原型和原型鍊無疑為重點和難點
如果這兩個知識點沒弄明白,要寫插件架構或者是看一些架構的源碼,肯定比較費力甚至看不懂
我覺得有必要先來回顧一下JavaScript中對象,我們在很多資料上都看到過類似的描述:
‘JavaScript中一切皆對象’、‘JavaScript中的所有事物都是對象’
我相信很多人并不贊同這樣的說法,或者不确定,或者有疑惑…
比如 var a, b = ‘str’; 難道 a b都是對象?(曾經某個面試官對我提出的疑問)
我個人認為:是的,都是對象
why ???
來看看JavaScript關于對象的定義:
‘對象是若幹屬性的集合’
‘對象是擁有屬性和方法的資料’
‘對象是由一組無序的名值對組成的’
不管是哪種描述,相信已經在研究原型和原型鍊的你都能了解對象的定義!
OK,既然 a b 是對象,它們有什麼屬性,有什麼方法?
好吧,a 沒有屬性方法,b 有屬性方法,a 顯然不符合對象的定義
BUT
再不上代碼我也解釋不下去了…
var n = ;
var s = 'abc';
var b = true;
var a = [];
var f = function (){};
var o = {};
var k = null;
var u;
// 運算符:typeof
// 傳回值:number、string、boolean、undefined、object、function
console.log(typeof n);// number
console.log(typeof s);// string
console.log(typeof b);// boolean
console.log(typeof a);// object
console.log(typeof f);// function
console.log(typeof o);// object
console.log(typeof k);// object
console.log(typeof u);// undefined
o = { } 對象是對象沒毛病吧,a = [ ] 數組是對象沒疑問吧,k = null 空對象的引用也是對象說得過去吧
(typeof null 傳回 object 其實這是JavaScript最初實作的一個錯誤,後來被ECMAScript沿用下來)
f = function (){} 在JavaScript中函數也屬于對象,相對于其他對象,函數對象比較特殊(後面再讨論這個問題)
那麼 n、s、b、u 這四個怎麼解釋呢?
咱們先來看看 u 吧
在JavaScript中,聲明變量不指派,函數沒有明确的 return 結果,這兩種情況值都為 undefined
undefined 作為JavaScript的一種資料類型,其值隻有一個為 undefined 本身
what ? 這兩貨狼狽為奸,居然傳回 true
ECMAScript認為:undefined是從null派生出來的,是以把它們定義為值相等
好了,ECMAScript規定了 undefined 是從 null 派生出來的,而且它們的值還是相等的
既然 null 是對象,那 undefined 為什麼不是呢?(如果還是覺得undefined不是對象,我無力辯解)
接下來我們來看看 n、s、b 這三個好基友
在ECMAScript中有三個比較特殊的資料類型即 Boolean、Number、String
Boolean、Number、String 是js中的基本資料類型,也叫特殊的引用類型,又叫基本包裝類型,還叫‘僞對象’
(當它們嘗試轉化為對象來時,背景會将其轉換成一個臨時的包裝類型對象,而完成這個通路後,臨時對象會被銷毀掉)
不管你習慣上面哪種稱呼,實際上它們是具有屬性和方法的,是符合對象的定義的!
好吧,感覺越跑越偏了…把對象和資料類型的問題先擱置後議!
但是,請先記住一句話:對象分為 普通對象 和 函數對象
屬性:prototype(原型)
每個函數對象(Function.prototype除外)都有一個prototype屬性,這個屬性指向一個對象即原型對象
var fn1 = function (){}; // 函數表達式
var fn2 = new Function(); // 執行個體化函數對象
function Cat(){}; // 函數聲明
console.log(fn1.prototype); // Object{}
console.log(fn2.prototype); // Object{}
console.log(Cat.prototype); // Object{}
// 這裡的 Object{} 就是我們所說的原型,它是一個對象也叫原型對象
為什麼 Function.prototype 除外呢?看代碼:
console.log(Number.prototype);
console.log(String.prototype);
console.log(Function.prototype);
console.log(Function.prototype.prototype);
// 結果看下圖
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CXxEleOJzYE5kMNpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39zNwIjN0ATMxEDMygDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
我們可以看到這些内置構造函數Number、String等,它們的原型對象都是一個普通對象(Number{ }和String{ })
而Function的原型對象則指向函數對象 function () { [native code] },就是原生代碼,二進制編譯的!
這個函數對象(Function.prototype)是沒有原型對象的,是以它的prototype傳回 undefined。
簡單說,(fn1.prototype)是一個原型對象,(Cat.prototype)是一個原型對象,(Number.prototype)是一個原型對象…
對于原型好像還是比較模糊?沒關系,我們繼續來了解它
function Cat(){};
Cat.prototype.name = '小白'; // 給原型對象添加屬性
Cat.prototype.color = 'black'; // 給原型對象添加屬性
Cat.prototype.sayHello = function (){ // 給原型對象添加方法
console.log('大家好,我的名字叫'+this.name);
}
var cat1 = new Cat(); // 執行個體對象
var obj = Cat.prototype;// 這個,是的這個就是原型對象
console.log(obj); // Object {name:"小白", color:"black", sayHello: function...}
console.log(cat1.constructor); // function Cat(){}
console.log(obj.constructor); // function Cat(){}
console.log(Cat.prototype === cat1.constructor.prototype);// true
屬性:constructor(構造器)
每個對象都有一個隐藏屬性constructor,該屬性指向對象的構造函數(類)
通過上面的代碼我們可以看到,執行個體對象cat1和原型對象obj 它們的構造器都是Cat !
結論:原型對象(Cat.prototype)也是 構造函數(Cat)的一個執行個體。
還是不好了解???我們換一種寫法:
function Cat(){}
Cat.prototype = {// 原型對象
name: '小白',
color: 'black',
sayHello: function (){
console.log('大家好,我的名字叫'+this.name);
}
}
var cat1 = new Cat();
這樣寫應該很直覺了吧,但是
console.log(Cat.prototype === cat1.constructor.prototype); // false
console.log(cat1.constructor===Object); // true
是不是剛剛感覺可以了解了,又懵逼了!這不是你的錯…
請記住,使用對象字面量方式定義的對象,其構造器(constructor)指向的是根構造器Object
那麼,原型有什麼用呢?
原型的主要作用是用于繼承
直接上代碼
var Person = function(name){
this.name = name;
};
Person.prototype.type = 'human';
Person.prototype.getName = function(){
console.log(this.name);
}
var p1 = new Person('jack');
var p2 = new Person('lucy');
p1.getName(); // jack
console.log(p1.type); // 'human'
p2.getName(); // lucy
console.log(p2.type); // 'human'
示例中通過給原型對象(Person.prototype)添加屬性方法
那麼由 Person 執行個體出來的普通對象(p1 p2)就繼承了這個屬性方法(type getName)
再看一個示例
Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
console.log('嗨~大家好');
}
String.prototype.pin = function (){
console.log(this+'&biubiu');
}
var str = 'yoyo';
var num = ;
var arr = [];
var boo = true;
str.sayHi(); // 嗨~大家好
num.sayHi(); // 嗨~大家好
arr.sayHi(); // 嗨~大家好
boo.sayHi(); // 嗨~大家好
console.log(str.jdk); // abc123
console.log(num.jdk); // abc123
console.log(arr.jdk); // abc123
console.log(boo.jdk); // abc123
str.pin(); // yoyo&biubiu
num.pin(); // 報錯 num.pin is not a function
arr.pin(); // 報錯 arr.pin is not a function
boo.pin(); // 報錯 boo.pin is not a function
看出點什麼了嗎?
所有對象都繼承了Object.prototype原型上的屬性方法(換句話說它們都是Object的執行個體)
str 還繼承了String.prototype原型上的屬性方法
再看一個比較實用的示例
Date.prototype.getWeek = function () {
var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
var index = this.getDay();//0-6
return arr[index];
}
var dates = new Date();
console.log(dates.getWeek()); // '星期一'
dates 日期對象繼承了 getWeek 方法
原型我們了解啦,具體是怎麼實作的繼承,就要講到原型鍊了
屬性:_ _ proto _ _(原型)
每個對象都有一個隐藏屬性_ _ proto _ _,用于指向建立它的構造函數的原型對象
懵逼……怎麼又一個原型???
别着急!!!
上面我們講prototype是針對每個函數對象,這個_ _ proto _ _是針對每個對象 有點差別的哦~
var n = ;
var s = 'jdk';
var b = true;
var a = [];
var f = function (){};
var o = {};
var k = null;
var u = undefined;
console.log(n.__proto__); // Number { ... }
console.log(n.__proto__ === Number.prototype);// true
console.log(s.__proto__ === String.prototype);// true
console.log(a.__proto__ === Array.prototype);// true
console.log(f.__proto__ === Function.prototype);// true function (){}
console.log(o.__proto__ === Object.prototype);// true
console.log(b.__proto__ === Boolean.prototype);// true
對象 通過_ _ proto _ _指向原型對象,函數對象 通過prototype指向原型對象
原型鍊呢,鍊在哪?
Object.prototype.jdk = 'abc123';
Object.prototype.sayHi = function (){
console.log('嗨~大家好');
}
var str = 'yoyo';
str.sayHi();// 嗨~大家好
console.log(str.jdk);// 'abc123'
// 前面寫的示例,我們來找找原型鍊
首先,str 是怎麼通路到 sayHi 方法和 jdk 屬性的呢
其次,了解一下方法 hasOwnProperty() ,用于判斷某個屬性是否為該對象本身的一個成員
最後,看看大緻的通路過程
console.log(str.hasOwnProperty('sayHi'));//false str自身沒有sayHi方法
console.log(str.proto__.hasOwnProperty('sayHi'));//false 原型對象也沒有sayHi方法
console.log(str.proto__.proto__.hasOwnProperty('sayHi'));//true 原型的原型有sayHi方法并執行
‘str -> str._ _ proto _ _ -> str._ _ proto _ _ . _ _ proto _ _’ 感覺到什麼了?
我們來描述一下執行過程:
str.sayHi() –> 自身查找 –> 沒有sayHi方法 –> 查找原型鍊 str._ _ proto _ _ –> 指向 String.prototype –> 沒有sayHi方法
–> 查找原型鍊 String.prototype._ _ proto _ _ –> 指向Object.prototype –> 找到sayHi方法 –> 執行()
環環相扣,是不是像鍊條一樣呢?是的,這個就是我們所說的 原型鍊
再看看下面的示例:
原型鍊的最後是 null
如果還沒暈,恭喜你似乎領悟到了某些人生的哲學:
《易經》– ‘太極生兩儀,兩儀生四象,四象生八卦’
《道德經》– ‘無,名天地之始’
是不是很熟悉,是不是很意外!
OK,下面我們來了解了解繼承
繼承有多種實作方式,各種方式各種資料上的稱呼也不一樣,怎麼叫無所謂,你高興就好…
// demo1 對象冒充繼承
function Cat(n,c){ // 貓 類
this.name=n;
this.color=c;
this.trait=function (){
console.log('賣萌~');
}
}
Cat.prototype.skill=function (){// 原型上的屬性方法
console.log('抓老鼠');
}
// 需求:狗要賣萌,狗要多管閑事-抓老鼠
function Dog(n,c,f){ // 狗 類
this.food=f;
Cat.call(this,n,c); // 用狗來通路貓的屬性方法
}
var dog1=new Dog('二哈','yellow','shi');// 執行個體對象
console.log(dog1.name); // 二哈
dog1.trait(); // 賣萌
dog1.skill(); // 報錯 dog1.skill is not a function
我們看到這種繼承方式有局限性,父類原型上的屬性方法無法繼承,是以二哈沒有抓老鼠的技能
// demo2 原型鍊繼承
function Cat(n,c){ // 貓 類
this.name=n;
this.color=c;
this.trait=function (){
console.log('賣萌~');
}
}
Cat.prototype.skill=function (){// 原型上的屬性方法
console.log('抓老鼠');
}
function Dog(n,c,f){ // 狗 類
this.food=f;
}
Dog.prototype=new Cat(); // 在狗的原型對象上添加貓的執行個體
var dog1=new Dog('二哈','yellow','shi');
console.log(dog1.name); // undefined
console.log(dog1.food); // shi
dog1.trait(); // 賣萌~
dog1.skill(); // 抓老鼠
console.log(dog1.constructor); // Cat
問題一:
執行個體化對象的時候不能給父類傳參,導緻通路dog1.name沒有值
問題二:
有句台詞:‘人是人媽生的,妖是妖媽生的 ’
現在 dog1.constructor 指向 Cat,意味着 二哈 是貓媽生的!很顯然這不合理也不環保…
// demo3 對象冒充繼承與原型鍊繼承結合
function Cat(n,c){
this.name=n;
this.color=c;
this.trait=function (){
console.log('賣萌~');
}
}
Cat.prototype.skill=function (){
console.log('抓老鼠');
}
function Dog(n,c,f){
this.food=f;
Cat.call(this,n,c);// 對象冒充繼承
}
Dog.prototype=new Cat();// 原型鍊繼承
Dog.prototype.constructor=Dog;// 指正構造器
var dog1=new Dog('二哈','yellow','shi');
console.log(dog1.name);// 二哈
console.log(dog1.food);// shi
dog1.trait();// 賣萌~
dog1.skill();// 抓老鼠
console.log(dog1.constructor);// Dog
兩種方式結合可以實作相對比較完美的繼承
别忘了指正構造器,不能認賊作父!
好了,簡單總結一下吧
原型、原型鍊其實真不難了解,涉及的屬性方法也不多
當你了解了這些,你會發現閉包應用和回調地獄比原型、原型鍊更難…
如何檢驗你是否真了解對象、原型、原型鍊、繼承?
我有個方法:去看看JQ源碼,會看到很多關于prototype、constructor的一些操作
最後,送上一張自己畫的對象關系圖:
不知道自己有沒有被繞進去,如有不準确的地方,希望指出交流。