天天看點

JavaScript中的對象、原型、原型鍊、繼承

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);
// 結果看下圖
           
JavaScript中的對象、原型、原型鍊、繼承

我們可以看到這些内置構造函數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方法 –> 執行()

環環相扣,是不是像鍊條一樣呢?是的,這個就是我們所說的 原型鍊

再看看下面的示例:

JavaScript中的對象、原型、原型鍊、繼承

原型鍊的最後是 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的一些操作

最後,送上一張自己畫的對象關系圖:

JavaScript中的對象、原型、原型鍊、繼承

不知道自己有沒有被繞進去,如有不準确的地方,希望指出交流。

繼續閱讀